Skip to content

Migrate content_kinds.py to spec + code generation with metadata support #184

@rtibbles

Description

@rtibbles

This issue is not open for contribution. Visit Contributing guidelines to learn about the contributing process and how to find suitable issues.

Overview

Migrate le_utils/constants/content_kinds.py from the legacy JSON-as-data approach to the modern spec + code generation system. This issue also enhances the generation script to support metadata-driven code generation for the MAPPING dict.

Context

Currently, le_utils/constants/content_kinds.py uses the legacy approach:

  • Loads resources/kindlookup.json at runtime
  • Manual Python constants (TOPIC = "topic", VIDEO = "video", etc.)
  • Manual MAPPING dict mapping file formats to content kinds (not from JSON!)
  • No JavaScript export available

Current Structure

File: le_utils/resources/kindlookup.json
(Only has kind names, no mapping data)

{
  "topic": {"name": "topic"},
  "video": {"name": "video"},
  ...
}

Python module has manual MAPPING:

MAPPING = {
    file_formats.MP4: VIDEO,
    file_formats.WEBM: VIDEO,
    file_formats.MP3: AUDIO,
    file_formats.PDF: DOCUMENT,
    file_formats.EPUB: DOCUMENT,
    file_formats.PERSEUS: EXERCISE,
    file_formats.HTML5: HTML5,
    file_formats.H5P: H5P,
    file_formats.ZIM: ZIM,
    file_formats.BLOOMPUB: DOCUMENT,
    file_formats.BLOOMD: DOCUMENT,
}

Target Spec Format

Create spec/constants-content_kinds.json with associated_formats metadata:

{
  "namedtuple": {
    "name": "Kind",
    "fields": ["id", "name"]
  },
  "constants": {
    "topic": {
      "name": "topic"
    },
    "video": {
      "name": "video",
      "associated_formats": ["mp4", "webm"]
    },
    "audio": {
      "name": "audio",
      "associated_formats": ["mp3"]
    },
    "exercise": {
      "name": "exercise",
      "associated_formats": ["perseus"]
    },
    "document": {
      "name": "document",
      "associated_formats": ["pdf", "epub", "bloompub", "bloomd"]
    },
    "html5": {
      "name": "html5",
      "associated_formats": ["zip"]
    },
    "slideshow": {
      "name": "slideshow"
    },
    "h5p": {
      "name": "h5p",
      "associated_formats": ["h5p"]
    },
    "zim": {
      "name": "zim",
      "associated_formats": ["zim"]
    },
    "quiz": {
      "name": "quiz"
    }
  }
}

Note: associated_formats is metadata (not a namedtuple field) used to auto-generate the MAPPING dict.

Generation Script Enhancement

Update scripts/generate_from_specs.py:

  1. Detect metadata fields (fields not in namedtuple definition)
  2. Generate MAPPING dict from associated_formats:
    • Import file_formats module
    • Create dict mapping format constants to kind constants
    • Python: MAPPING = {file_formats.MP4: VIDEO, ...}
    • JavaScript: export const KindsMapping = { mp4: "video", ... }

Generated Output Example

Python (le_utils/constants/content_kinds.py):

# Generated by scripts/generate_from_specs.py
from collections import namedtuple

from le_utils.constants import file_formats

class Kind(namedtuple("Kind", ["id", "name"])):
    pass

TOPIC = "topic"
VIDEO = "video"
AUDIO = "audio"
# ...

choices = (
    (TOPIC, "Topic"),
    (VIDEO, "Video"),
    # ...
)

KINDLIST = [
    Kind(id="topic", name="topic"),
    Kind(id="video", name="video"),
    # ...
]

# File Format to Content Kind mapping (generated from associated_formats metadata)
MAPPING = {
    file_formats.MP4: VIDEO,
    file_formats.WEBM: VIDEO,
    file_formats.MP3: AUDIO,
    file_formats.PDF: DOCUMENT,
    file_formats.EPUB: DOCUMENT,
    file_formats.PERSEUS: EXERCISE,
    file_formats.HTML5: HTML5,
    file_formats.H5P: H5P,
    file_formats.ZIM: ZIM,
    file_formats.BLOOMPUB: DOCUMENT,
    file_formats.BLOOMD: DOCUMENT,
}

JavaScript (js/ContentKinds.js):

// Generated by scripts/generate_from_specs.py

export default {
    TOPIC: "topic",
    VIDEO: "video",
    AUDIO: "audio",
    // ...
};

export const KindsList = [
    { id: "topic", name: "topic" },
    { id: "video", name: "video" },
    // ...
];

export const KindsMap = new Map(
    KindsList.map(kind => [kind.id, kind])
);

// Format to Kind mapping
export const KindsMapping = {
    mp4: "video",
    webm: "video",
    mp3: "audio",
    pdf: "document",
    epub: "document",
    perseus: "exercise",
    zip: "html5",
    h5p: "h5p",
    zim: "zim",
    bloompub: "document",
    bloomd: "document",
};

Testing Updates

File: tests/test_kinds.py

Update to test against spec:

spec_path = os.path.join(os.path.dirname(__file__), "..", "spec", "constants-content_kinds.json")
with open(spec_path) as f:
    spec = json.load(f)
    kindlookup = spec["constants"]

# Verify MAPPING is generated correctly from associated_formats

How to Run Tests

pytest tests/test_kinds.py -v
pytest tests/ -v

Acceptance Criteria

  • spec/constants-content_kinds.json created with associated_formats metadata
  • scripts/generate_from_specs.py enhanced to generate MAPPING from metadata
  • make build successfully generates Python and JavaScript files
  • Generated le_utils/constants/content_kinds.py has:
    • Kind namedtuple
    • Uppercase constants (TOPIC, VIDEO, etc.)
    • choices tuple
    • KINDLIST with Kind namedtuples
    • Auto-generated MAPPING dict from associated_formats
  • Generated js/ContentKinds.js has:
    • Default export with constants
    • KindsList with full data
    • KindsMap for lookups
    • KindsMapping for format→kind lookups
  • tests/test_kinds.py updated to test against spec
  • All tests pass
  • resources/kindlookup.json deleted

Disclosure

🤖 This issue was written by Claude Code, under supervision, review and final edits by @rtibbles 🤖

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions