diff --git a/unwind/models.py b/unwind/models.py index 0bf489d..964ac01 100644 --- a/unwind/models.py +++ b/unwind/models.py @@ -19,6 +19,9 @@ from typing import ( get_origin, ) +from sqlalchemy import Column, ForeignKey, Integer, String, Table +from sqlalchemy.orm import registry + from .types import ULID JSON = int | float | str | None | list["JSON"] | dict[str, "JSON"] @@ -26,6 +29,8 @@ JSONObject = dict[str, JSON] T = TypeVar("T") +mapper_registry = registry() + def annotations(tp: Type) -> tuple | None: return tp.__metadata__ if hasattr(tp, "__metadata__") else None @@ -198,8 +203,19 @@ def utcnow(): return datetime.utcnow().replace(tzinfo=timezone.utc) +@mapper_registry.mapped @dataclass class Progress: + __table__ = Table( + "progress", + mapper_registry.metadata, + Column("id", String, primary_key=True), # ULID + Column("type", String, nullable=False), + Column("state", String, nullable=False), # JSON {"percent": ..., "error": ...} + Column("started", String, nullable=False), # datetime + Column("stopped", String), + ) + _table: ClassVar[str] = "progress" id: ULID = field(default_factory=ULID) @@ -237,8 +253,26 @@ class Progress: self._state = state +@mapper_registry.mapped @dataclass class Movie: + __table__ = Table( + "movies", + mapper_registry.metadata, + Column("id", String, primary_key=True), # ULID + Column("title", String, nullable=False), + Column("original_title", String), + Column("release_year", Integer, nullable=False), + Column("media_type", String, nullable=False), + Column("imdb_id", String, nullable=False, unique=True), + Column("imdb_score", Integer), + Column("imdb_votes", Integer), + Column("runtime", Integer), + Column("genres", String, nullable=False), + Column("created", String, nullable=False), # datetime + Column("updated", String, nullable=False), # datetime + ) + _table: ClassVar[str] = "movies" id: ULID = field(default_factory=ULID) @@ -295,8 +329,21 @@ The contents of the Relation are ignored or discarded when using Relation = Annotated[T | None, _RelationSentinel] +@mapper_registry.mapped @dataclass class Rating: + __table__ = Table( + "ratings", + mapper_registry.metadata, + Column("id", String, primary_key=True), # ULID + Column("movie_id", ForeignKey("movies.id"), nullable=False), # ULID + Column("user_id", ForeignKey("users.id"), nullable=False), # ULID + Column("score", Integer, nullable=False), + Column("rating_date", String, nullable=False), # datetime + Column("favorite", Integer), # bool + Column("finished", Integer), # bool + ) + _table: ClassVar[str] = "ratings" id: ULID = field(default_factory=ULID) @@ -337,8 +384,19 @@ class UserGroup(TypedDict): access: Access +@mapper_registry.mapped @dataclass class User: + __table__ = Table( + "users", + mapper_registry.metadata, + Column("id", String, primary_key=True), # ULID + Column("imdb_id", String, nullable=False, unique=True), + Column("name", String, nullable=False), + Column("secret", String, nullable=False), + Column("groups", String, nullable=False), # JSON array + ) + _table: ClassVar[str] = "users" id: ULID = field(default_factory=ULID) @@ -366,8 +424,17 @@ class GroupUser(TypedDict): name: str +@mapper_registry.mapped @dataclass class Group: + __table__ = Table( + "groups", + mapper_registry.metadata, + Column("id", String, primary_key=True), # ULID + Column("name", String, nullable=False), + Column("users", String, nullable=False), # JSON array + ) + _table: ClassVar[str] = "groups" id: ULID = field(default_factory=ULID)