From 7843e45b202b75fde5aced80918246a8776ec18d Mon Sep 17 00:00:00 2001 From: Paillat Date: Wed, 24 Jul 2024 21:10:51 +0200 Subject: [PATCH] :card_file_box: Add basic models --- ...7_25_1814-8fc4b07d9adc_add_basic_models.py | 73 +++++++++++++ pyproject.toml | 4 + src/db_tables/media.py | 38 +++++++ src/db_tables/user.py | 19 ++++ src/db_tables/user_list.py | 102 ++++++++++++++++++ 5 files changed, 236 insertions(+) create mode 100644 alembic-migrations/versions/2024_07_25_1814-8fc4b07d9adc_add_basic_models.py create mode 100644 src/db_tables/media.py create mode 100644 src/db_tables/user.py create mode 100644 src/db_tables/user_list.py diff --git a/alembic-migrations/versions/2024_07_25_1814-8fc4b07d9adc_add_basic_models.py b/alembic-migrations/versions/2024_07_25_1814-8fc4b07d9adc_add_basic_models.py new file mode 100644 index 0000000..c8071b4 --- /dev/null +++ b/alembic-migrations/versions/2024_07_25_1814-8fc4b07d9adc_add_basic_models.py @@ -0,0 +1,73 @@ +"""Add basic models + +Revision ID: 8fc4b07d9adc +Revises: c55da3c62644 +Create Date: 2024-07-25 18:14:19.322905 +""" + +from collections.abc import Sequence + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "8fc4b07d9adc" +down_revision: str | None = "c55da3c62644" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table("movies", sa.Column("tvdb_id", sa.Integer(), nullable=False), sa.PrimaryKeyConstraint("tvdb_id")) + op.create_table("shows", sa.Column("tvdb_id", sa.Integer(), nullable=False), sa.PrimaryKeyConstraint("tvdb_id")) + op.create_table( + "users", sa.Column("discord_id", sa.Integer(), nullable=False), sa.PrimaryKeyConstraint("discord_id") + ) + op.create_table( + "episodes", + sa.Column("tvdb_id", sa.Integer(), nullable=False), + sa.Column("show_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["show_id"], + ["shows.tvdb_id"], + ), + sa.PrimaryKeyConstraint("tvdb_id"), + ) + op.create_table( + "user_lists", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("user_id", sa.Integer(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("item_kind", sa.Enum("SHOW", "MOVIE", "EPISODE", "MEDIA", "ANY", name="itemkind"), nullable=False), + sa.ForeignKeyConstraint( + ["user_id"], + ["users.discord_id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("user_id", "name", name="unique_user_list_name"), + ) + op.create_index("ix_user_lists_user_id_name", "user_lists", ["user_id", "name"], unique=True) + op.create_table( + "user_list_items", + sa.Column("list_id", sa.Integer(), nullable=False), + sa.Column("tvdb_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["list_id"], + ["user_lists.id"], + ), + sa.PrimaryKeyConstraint("list_id", "tvdb_id"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("user_list_items") + op.drop_index("ix_user_lists_user_id_name", table_name="user_lists") + op.drop_table("user_lists") + op.drop_table("episodes") + op.drop_table("users") + op.drop_table("shows") + op.drop_table("movies") + # ### end Alembic commands ### diff --git a/pyproject.toml b/pyproject.toml index a0c58fd..bcbd5d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -214,6 +214,10 @@ reportUnknownMemberType = false reportUnknownParameterType = false reportUnknownLambdaType = false +executionEnvironments = [ + { root = "src/db_tables", reportImportCycles = false }, +] + [tool.pytest.ini_options] minversion = "6.0" asyncio_mode = "auto" diff --git a/src/db_tables/media.py b/src/db_tables/media.py new file mode 100644 index 0000000..d8d0fee --- /dev/null +++ b/src/db_tables/media.py @@ -0,0 +1,38 @@ +"""This file houses all tvdb media related database tables. + +Some of these tables only have one column (`tvdb_id`), which may seem like a mistake, but is intentional. +That's because this provides better type safety and allows us to define proper foreign key relationships that +refer to these tables instead of duplicating that data. +It also may become useful if at any point we would +want to store something extra that's global to each movie / show / episode. +""" + +from sqlalchemy import ForeignKey +from sqlalchemy.orm import Mapped, mapped_column + +from src.utils.database import Base + + +class Movie(Base): + """Table to store movies.""" + + __tablename__ = "movies" + + tvdb_id: Mapped[int] = mapped_column(primary_key=True) + + +class Series(Base): + """Table to store series.""" + + __tablename__ = "series" + + tvdb_id: Mapped[int] = mapped_column(primary_key=True) + + +class Episode(Base): + """Table to store episodes of series.""" + + __tablename__ = "episodes" + + tvdb_id: Mapped[int] = mapped_column(primary_key=True) + series_id: Mapped[int] = mapped_column(ForeignKey("series.tvdb_id")) diff --git a/src/db_tables/user.py b/src/db_tables/user.py new file mode 100644 index 0000000..e5c2a84 --- /dev/null +++ b/src/db_tables/user.py @@ -0,0 +1,19 @@ +from typing import TYPE_CHECKING + +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from src.utils.database import Base + +# Prevent circular imports for relationships +if TYPE_CHECKING: + from src.db_tables.user_list import UserList + + +class User(Base): + """Table to store users.""" + + __tablename__ = "users" + + discord_id: Mapped[int] = mapped_column(primary_key=True) + + lists: Mapped[list["UserList"]] = relationship("UserList", back_populates="user") diff --git a/src/db_tables/user_list.py b/src/db_tables/user_list.py new file mode 100644 index 0000000..c0d6c4f --- /dev/null +++ b/src/db_tables/user_list.py @@ -0,0 +1,102 @@ +from enum import Enum +from typing import ClassVar, TYPE_CHECKING + +from sqlalchemy import ForeignKey, Index, UniqueConstraint +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from src.utils.database import Base + +# Prevent circular imports for relationships +if TYPE_CHECKING: + from src.db_tables.media import Episode, Movie, Series + from src.db_tables.user import User + + +class ItemKind(Enum): + """Enum to represent the kind of item in a user list.""" + + SERIES = "series" + MOVIE = "movie" + EPISODE = "episode" + MEDIA = "media" # either series or movie + ANY = "any" + + +class UserList(Base): + """Table to store user lists. + + This provides a dynamic way to store various lists of media for the user, such as favorites, to watch, + already watched, ... all tracked in the same table, instead of having to define tables for each such + structure. + """ + + __tablename__ = "user_lists" + + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + user_id: Mapped[int] = mapped_column(ForeignKey("users.discord_id"), nullable=False) + name: Mapped[str] = mapped_column(nullable=False) + item_kind: Mapped[ItemKind] = mapped_column(nullable=False) + + user: Mapped["User"] = relationship("User", back_populates="lists") + items: Mapped[list["UserListItem"]] = relationship("UserListItem", back_populates="user_list") + + __table_args__ = ( + UniqueConstraint("user_id", "name", name="unique_user_list_name"), + Index( + "ix_user_lists_user_id_name", + "user_id", + "name", + unique=True, + ), + ) + + +class UserListItem(Base): + """Base class for items in a user list.""" + + __tablename__ = "user_list_items" + list_id: Mapped[int] = mapped_column(ForeignKey("user_lists.id"), primary_key=True) + tvdb_id: Mapped[int] = mapped_column(primary_key=True) + + user_list: Mapped["UserList"] = relationship("UserList", back_populates="items") + + __mapper_args__: ClassVar = {"polymorphic_on": tvdb_id, "polymorphic_identity": "base"} + + +class UserListItemSeries(UserListItem): + """Represents a reference to a series in a user list.""" + + __mapper_args__: ClassVar = { + "polymorphic_identity": "series", + } + + tvdb_id: Mapped[int] = mapped_column( + ForeignKey("series.tvdb_id"), nullable=False, use_existing_column=True, primary_key=True + ) + series: Mapped["Series"] = relationship("Series") + + +class UserListItemMovie(UserListItem): + """Represents a reference to a movie in a user list.""" + + __mapper_args__: ClassVar = { + "polymorphic_identity": "movie", + } + + tvdb_id: Mapped[int] = mapped_column( + ForeignKey("movies.tvdb_id"), nullable=False, use_existing_column=True, primary_key=True + ) + movie: Mapped["Movie"] = relationship("Movie") + + +class UserListItemEpisode(UserListItem): + """Represents a reference to an episode in a user list.""" + + __mapper_args__: ClassVar = { + "polymorphic_identity": "episode", + } + + tvdb_id: Mapped[int] = mapped_column( + ForeignKey("episodes.tvdb_id"), nullable=False, use_existing_column=True, primary_key=True + ) + episode: Mapped["Episode"] = relationship("Episode")