Spaces:
Running
Running
deploy: main@3f5fd0f (works + reading + passage/multi-span notes; superset of f1c0d35)
bb36f39 verified | """Typed models for the labels domain. | |
| A `Label` is a row in the labels table β a user-defined (or auto-derived) | |
| tag. A `Group` is a *resolved* set of book_ids; the unit of comparison. | |
| Groups are always built by a Resolver, never instantiated by hand from | |
| the UI. | |
| """ | |
| from __future__ import annotations | |
| from dataclasses import dataclass | |
| from typing import Literal | |
| LabelKind = Literal["manual", "derived"] | |
| class Label: | |
| """One row of the labels table.""" | |
| label_id: str | |
| display_ar: str | |
| display_en: str | None | |
| description: str | None | |
| color: str | None | |
| kind: LabelKind | |
| created_at: str | None = None | |
| def display(self) -> str: | |
| """Best-effort display string. Arabic preferred per project default.""" | |
| return self.display_ar or self.display_en or self.label_id | |
| def is_derived(self) -> bool: | |
| return self.kind == "derived" | |
| class LabelWithCount: | |
| """Label augmented with how many books currently carry it. Used in pickers.""" | |
| label: Label | |
| book_count: int | |
| class GroupMember: | |
| """One member of a Group β a whole book, or one work (page range) inside it. | |
| Compare's per-work scoping axis. ``work_id`` set means this member is | |
| scoped to that work's page range within ``book_id`` (``page_start``/ | |
| ``page_end`` mirror the work's own range, echoed here so downstream | |
| strategy code never needs a repo round-trip). ``display`` optionally | |
| overrides the member's label β set to the work's ``display_title`` when | |
| ``work_id`` is set. A plain whole-book member is | |
| ``GroupMember(book_id=b)`` with everything else left at its default. | |
| """ | |
| book_id: str | |
| work_id: str | None = None | |
| page_start: int | None = None | |
| page_end: int | None = None | |
| display: str | None = None | |
| class Group: | |
| """A resolved set of books β the unit of comparison. | |
| `group_id` is a stable handle the UI / cache / saved-comparison-set | |
| can reference: 'label:first_millennium', 'book:deskolia_v3', | |
| 'manual:my_three_picks', 'union:label:antiochene+label:cappadocian', etc. | |
| `members` is the per-work scoping axis (optional). Every existing | |
| resolver (label/manual/book/filter/union/intersection) only ever sets | |
| `book_ids` and leaves `members` at its default `()` β `effective_members` | |
| derives one whole-book member per book_id for them, so their behavior is | |
| unchanged. Only the Compare wire path (`_group_from_wire`) populates | |
| `members` explicitly, when the caller picked specific works. | |
| """ | |
| group_id: str | |
| display_ar: str | |
| display_en: str | None | |
| book_ids: tuple[str, ...] | |
| members: tuple[GroupMember, ...] = () | |
| def display(self) -> str: | |
| return self.display_ar or self.display_en or self.group_id | |
| def effective_members(self) -> tuple[GroupMember, ...]: | |
| """`members` if set, else one whole-book `GroupMember` per `book_ids`.""" | |
| return self.members if self.members else tuple(GroupMember(book_id=b) for b in self.book_ids) | |
| def size(self) -> int: | |
| return len(self.effective_members) | |
| def is_empty(self) -> bool: | |
| return not self.effective_members | |