Skip to content
This repository has been archived by the owner on Sep 6, 2024. It is now read-only.

Commit

Permalink
Merge pull request #56 from ItsDrike/db-models
Browse files Browse the repository at this point in the history
🗃️ Add basic models
  • Loading branch information
ItsDrike authored Jul 25, 2024
2 parents 425c327 + 7843e45 commit e4ce403
Show file tree
Hide file tree
Showing 5 changed files with 236 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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 ###
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,10 @@ reportUnknownMemberType = false
reportUnknownParameterType = false
reportUnknownLambdaType = false

executionEnvironments = [
{ root = "src/db_tables", reportImportCycles = false },
]

[tool.pytest.ini_options]
minversion = "6.0"
asyncio_mode = "auto"
Expand Down
38 changes: 38 additions & 0 deletions src/db_tables/media.py
Original file line number Diff line number Diff line change
@@ -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"))
19 changes: 19 additions & 0 deletions src/db_tables/user.py
Original file line number Diff line number Diff line change
@@ -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")
102 changes: 102 additions & 0 deletions src/db_tables/user_list.py
Original file line number Diff line number Diff line change
@@ -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")

0 comments on commit e4ce403

Please sign in to comment.