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

    No fields configured for Task.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions