feat: catalog Record model and validation

This commit is contained in:
Ben Stull
2026-06-04 01:33:50 -07:00
parent 1379751dda
commit 8b3d17a6aa
2 changed files with 142 additions and 0 deletions
+71
View File
@@ -1 +1,72 @@
"""Catalog data model, validation, and JSONL IO."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Optional
MODES = frozenset({"audio", "video", "av"})
LICENSES = frozenset({"public_domain", "cc0", "cc_by", "cc_by_nc"})
REVIEW_STATUSES = frozenset({"proposed", "approved"})
COORD_FIELDS = ("left", "right", "dark", "light")
COORD_MIN = 0
COORD_MAX = 4
ATTRIBUTION_LICENSES = frozenset({"cc_by", "cc_by_nc"})
class CatalogError(ValueError):
"""Raised when a catalog record is structurally invalid."""
@dataclass
class Record:
id: str
title: str
source_url: str
source_archive: str
license: str
mode: str
left: int
right: int
dark: int
light: int
duration_s: int
file_path: str
review_status: str = "proposed"
attribution: str = ""
resolution: str = ""
dominant_color: str = ""
rationale: str = ""
reviewed_at: Optional[str] = None
notes: str = ""
def validate(record: Record) -> None:
"""Raise CatalogError if the record is structurally invalid."""
if not record.id:
raise CatalogError("record id must be non-empty")
if record.mode not in MODES:
raise CatalogError(
f"invalid mode {record.mode!r}; expected one of {sorted(MODES)}"
)
if record.license not in LICENSES:
raise CatalogError(
f"invalid license {record.license!r}; expected one of {sorted(LICENSES)}"
)
if record.review_status not in REVIEW_STATUSES:
raise CatalogError(
f"invalid review_status {record.review_status!r}; "
f"expected one of {sorted(REVIEW_STATUSES)}"
)
for axis in COORD_FIELDS:
value = getattr(record, axis)
if isinstance(value, bool) or not isinstance(value, int):
raise CatalogError(f"coordinate {axis} must be an int, got {value!r}")
if not (COORD_MIN <= value <= COORD_MAX):
raise CatalogError(
f"coordinate {axis}={value} out of range {COORD_MIN}..{COORD_MAX}"
)
if record.duration_s < 0:
raise CatalogError("duration_s must be non-negative")
if record.license in ATTRIBUTION_LICENSES and not record.attribution:
raise CatalogError(f"license {record.license} requires attribution")
+71
View File
@@ -0,0 +1,71 @@
import pytest
from hef.catalog import Record, validate, CatalogError
def make_record(**overrides):
base = dict(
id="nasa-apollo-earthrise",
title="Earthrise",
source_url="https://archive.org/details/earthrise",
source_archive="internet_archive",
license="public_domain",
mode="video",
left=1,
right=4,
dark=1,
light=3,
duration_s=600,
file_path="/media/earthrise.mp4",
)
base.update(overrides)
return Record(**base)
def test_valid_record_passes():
validate(make_record()) # must not raise
def test_defaults_are_set():
record = make_record()
assert record.review_status == "proposed"
assert record.attribution == ""
assert record.reviewed_at is None
def test_invalid_mode_raises():
with pytest.raises(CatalogError):
validate(make_record(mode="hologram"))
def test_invalid_license_raises():
with pytest.raises(CatalogError):
validate(make_record(license="all_rights_reserved"))
def test_coordinate_out_of_range_raises():
with pytest.raises(CatalogError):
validate(make_record(left=5))
with pytest.raises(CatalogError):
validate(make_record(dark=-1))
def test_boolean_coordinate_rejected():
with pytest.raises(CatalogError):
validate(make_record(right=True))
def test_cc_by_requires_attribution():
with pytest.raises(CatalogError):
validate(make_record(license="cc_by", attribution=""))
validate(make_record(license="cc_by", attribution="Jane Doe, CC BY 4.0"))
def test_empty_id_raises():
with pytest.raises(CatalogError):
validate(make_record(id=""))
def test_negative_duration_raises():
with pytest.raises(CatalogError):
validate(make_record(duration_s=-1))