diff --git a/src/infra/publication.py b/src/infra/publication.py index fb28357..b07df6d 100644 --- a/src/infra/publication.py +++ b/src/infra/publication.py @@ -1,25 +1,16 @@ from datetime import timedelta import logging from sqlalchemy import String, Text, ForeignKey, Engine, Interval, UniqueConstraint -from sqlalchemy.orm import Mapped, mapped_column, relationship, Session, joinedload +from sqlalchemy.orm import Mapped, mapped_column, relationship, Session from src.infra.db import Base -from src.services.publications import ( - Publication, - Order, - Entry, - Sequence, - PublicationProfile, - PublicationRepo, - OrderProfile, -) +from src.services.publications import Publication, Order, Entry, Sequence, PublicationProfile, PublicationRepo class PublicationEntryModel(Base): __tablename__ = 'publication_entry' id: Mapped[int] = mapped_column(primary_key=True) - title: Mapped[str] = mapped_column(String(256)) text: Mapped[str] = mapped_column(Text()) sequences: Mapped[list['PublicationSequenceModel']] = relationship(back_populates='entry') @@ -64,13 +55,13 @@ class PublicationModel(Base): def model_to_entry(db_pub_entry: PublicationEntryModel) -> Entry: - return Entry(id=db_pub_entry.id, text=db_pub_entry.text, title=db_pub_entry.title) + return Entry(id=db_pub_entry.id, text=db_pub_entry.text) def model_to_sequence(db_pub_position: PublicationSequenceModel) -> Sequence: return Sequence( position=db_pub_position.position, - entry=model_to_entry(db_pub_position.entry).to_profile(), + entry=model_to_entry(db_pub_position.entry), duration=db_pub_position.duration, ) @@ -83,20 +74,13 @@ def model_to_order(db_pub_order: PublicationOrderModel) -> Order: ) -def model_to_order_profile(db_pub_order: PublicationOrderModel) -> OrderProfile: - return OrderProfile( - id=db_pub_order.id, - title=db_pub_order.title, - ) - - def model_to_publication(db_pub: PublicationModel) -> Publication: return Publication( id=db_pub.id, title=db_pub.title, description=db_pub.description, by_line=db_pub.by_line, - orders=list(map(lambda m: model_to_order_profile(m), db_pub.orders)), + orders=list(map(model_to_order, db_pub.orders)), ) @@ -112,7 +96,7 @@ class PublicationRepoImpl(PublicationRepo): def get_publication_by_id(self, pub_id) -> Publication | None: with Session(self._db) as session: - publication = session.get(PublicationModel, pub_id, options=[joinedload(PublicationModel.orders)]) + publication = session.get(PublicationModel, pub_id) if publication: return model_to_publication(publication) @@ -127,22 +111,3 @@ class PublicationRepoImpl(PublicationRepo): def get_all_publications(self) -> list[PublicationProfile]: with Session(self._db) as session: return list(map(model_to_publication_profile, session.query(PublicationModel).all())) - - def get_full_order(self, order_id: int) -> Order | None: - with Session(self._db) as session: - order = session.get( - PublicationOrderModel, order_id, options=[joinedload(PublicationOrderModel.sequence_entries)] - ) - - if order: - return model_to_order(order) - - def get_full_entry(self, entry_id: int) -> Entry | None: - with Session(self._db) as session: - order = session.get( - PublicationEntryModel, - entry_id, - ) - - if order: - return model_to_entry(order) diff --git a/src/infra/subscription.py b/src/infra/subscription.py deleted file mode 100644 index 40da20f..0000000 --- a/src/infra/subscription.py +++ /dev/null @@ -1,100 +0,0 @@ -from src.infra.publication import model_to_order -from typing import TYPE_CHECKING -from datetime import datetime -from src.infra.users import UserModel, model_to_user -from sqlalchemy.orm import Mapped, mapped_column, relationship, Session, joinedload -from src.infra.db import Base -from sqlalchemy import Engine, ForeignKey, CheckConstraint, DateTime, func, UniqueConstraint, select -from src.services.subscription import ( - SubscriptionRepo, - Subscription, - SubscriptionId, - UpdateSubscription, - AvailableSubscriptionEntry, -) - -if TYPE_CHECKING: - from src.infra.publication import PublicationOrderModel - - -class SubscriptionModel(Base): - __tablename__ = 'subscription' - __table_args__ = ( - # Don't allow any weird avlues for sequence_seen - CheckConstraint('sequence_seen >= 1', name='sequence_seen_gte_1'), - # Ensure a user can't create multiple subscriptions to the same order - UniqueConstraint('order_id', 'user_id'), - ) - - id: Mapped[SubscriptionId] = mapped_column(primary_key=True) - - # The user that the subscription belongs to - user_id: Mapped[int] = mapped_column(ForeignKey('user.id')) - user: Mapped['UserModel'] = relationship(back_populates='subscriptions') - - # The order that the user is subscribing to - order_id: Mapped[int] = mapped_column(ForeignKey('publication_order.id')) - order: Mapped['PublicationOrderModel'] = relationship() - - # Which position into the sequence is available to the user - sequence_seen: Mapped[int] = mapped_column(server_default='1', default=1) - - # When the user's subscription began. - start: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) - - -def model_to_subscription(db_sub: SubscriptionModel) -> Subscription: - return Subscription( - id=db_sub.id, - user=model_to_user(db_sub.user).to_profile(), - publication_order=model_to_order(db_sub.order).to_profile(), - sequence_seen=db_sub.sequence_seen, - start=db_sub.start, - ) - - -class SubscriptionRepoImpl(SubscriptionRepo): - def __init__(self, db: Engine): - self._db = db - - def get_subscriptions_for_user(self, user_id: int) -> list[Subscription]: - with Session(self._db) as session: - subs = ( - session.scalars( - select(SubscriptionModel) - .where(SubscriptionModel.user_id == user_id) - .options( - joinedload(SubscriptionModel.order), - joinedload(SubscriptionModel.user), - ) - ) - .unique() - .all() - ) - return list(map(model_to_subscription, subs)) - - def get_subscription_by_id(self, sub_id: SubscriptionId) -> Subscription | None: - with Session(self._db) as session: - db_sub = session.get( - SubscriptionModel, - sub_id, - options=[joinedload(SubscriptionModel.order), joinedload(SubscriptionModel.user)], - ) - - if db_sub: - return model_to_subscription(db_sub) - - def create_subscription_for_user(self, user_id: int, order_id: int) -> Subscription: - pass - - def delete_subscription(self, subscription_id: SubscriptionId): - pass - - def update_subscription(self, update: UpdateSubscription) -> Subscription: - pass - - def get_available_entries(self, user_id: int) -> dict[SubscriptionId, list[AvailableSubscriptionEntry]]: - return {} - - def get_unseen_available_subscription_entries(self, user_id: int) -> list[AvailableSubscriptionEntry]: - pass diff --git a/src/infra/users.py b/src/infra/users.py index f358acb..0150631 100644 --- a/src/infra/users.py +++ b/src/infra/users.py @@ -1,18 +1,14 @@ -from typing import TYPE_CHECKING import logging from argon2.exceptions import VerifyMismatchError from src.utils.secret import SecretBox from src.config.email import EmailAddress from sqlalchemy import String, VARCHAR, Engine, select -from sqlalchemy.orm import mapped_column, Mapped, Session, relationship +from sqlalchemy.orm import mapped_column, Mapped, Session from argon2 import PasswordHasher from src.infra.db import Base from src.services.users.data import UserDTO, User from src.services.users.repo import UserRepo -if TYPE_CHECKING: - from src.infra.subscription import SubscriptionModel - class UserModel(Base): __tablename__ = 'user' @@ -21,8 +17,6 @@ class UserModel(Base): email_confirmed: Mapped[bool] = mapped_column() password_hash: Mapped[str] = mapped_column(VARCHAR(255)) - subscriptions: Mapped[list['SubscriptionModel']] = relationship(back_populates='user') - def model_to_user(db_user: UserModel) -> User: return User( diff --git a/src/services/publications/__init__.py b/src/services/publications/__init__.py index b19e688..451db29 100644 --- a/src/services/publications/__init__.py +++ b/src/services/publications/__init__.py @@ -1,14 +1,5 @@ -from .data import Publication, PublicationProfile, Order, Sequence, Entry, OrderProfile +from .data import Publication, PublicationProfile, Order, Sequence, Entry from .service import PublicationService from .repo import PublicationRepo -__all__ = [ - 'Publication', - 'PublicationProfile', - 'Order', - 'OrderProfile', - 'Sequence', - 'Entry', - 'PublicationService', - 'PublicationRepo', -] +__all__ = ['Publication', 'PublicationProfile', 'Order', 'Sequence', 'Entry', 'PublicationService', 'PublicationRepo'] diff --git a/src/services/publications/data.py b/src/services/publications/data.py index c29a563..7d2bf59 100644 --- a/src/services/publications/data.py +++ b/src/services/publications/data.py @@ -2,44 +2,25 @@ from datetime import timedelta from dataclasses import dataclass, field -@dataclass -class EntryProfile: - id: int - title: str - - @dataclass class Entry: id: int - title: str text: str - def to_profile(self) -> EntryProfile: - return EntryProfile(self.id, self.title) - @dataclass class Sequence: position: int - entry: EntryProfile + entry: Entry duration: timedelta | None -@dataclass -class OrderProfile: - id: int - title: str - - @dataclass class Order: id: int title: str sequence_entries: list[Sequence] - def to_profile(self) -> OrderProfile: - return OrderProfile(id=self.id, title=self.title) - @dataclass class PublicationProfile: @@ -54,7 +35,7 @@ class Publication: title: str description: str by_line: str - orders: list[OrderProfile] = field(default_factory=list) + orders: list[Order] = field(default_factory=list) # Rather than using a subclass I think this makes sense def to_profile(self) -> PublicationProfile: diff --git a/src/services/publications/repo.py b/src/services/publications/repo.py index 0494fec..56502c6 100644 --- a/src/services/publications/repo.py +++ b/src/services/publications/repo.py @@ -1,6 +1,6 @@ import abc -from .data import Publication, PublicationProfile, Order, Entry +from .data import Publication, PublicationProfile class PublicationRepo(abc.ABC): @@ -15,11 +15,3 @@ class PublicationRepo(abc.ABC): @abc.abstractmethod def get_all_publications(self) -> list[PublicationProfile]: pass - - @abc.abstractmethod - def get_full_order(self, order_id: int) -> Order | None: - pass - - @abc.abstractmethod - def get_full_entry(self, entry_id: int) -> Entry | None: - pass diff --git a/src/services/subscription.py b/src/services/subscription.py deleted file mode 100644 index 25f70a7..0000000 --- a/src/services/subscription.py +++ /dev/null @@ -1,105 +0,0 @@ -import abc -from datetime import datetime -from src.services.publications import ( - OrderProfile as PublicationOrderProfile, - Sequence as PublicationSequence, - Publication, -) -from src.services.users.data import UserProfile -from dataclasses import dataclass - -SubscriptionId = int - - -@dataclass -class SubscriptionCreateParams: - user_id: int - order_id: int - - -@dataclass -class Subscription: - id: SubscriptionId - user: UserProfile - publication_order: PublicationOrderProfile - sequence_seen: int - start: datetime - - -@dataclass -class UpdateSubscription: - id: int - user_id: int - publication_order_id: int | None = None - sequence_seen: int | None = None - start: datetime | None = None - - -@dataclass -class AvailableSubscriptionEntry: - publication: Publication - available_since: datetime - order: PublicationOrderProfile - sequence: PublicationSequence - - -class SubscriptionRepo(abc.ABC): - @abc.abstractmethod - def get_subscriptions_for_user(self, user_id: int) -> list[Subscription]: - pass - - @abc.abstractmethod - def get_subscription_by_id(self, sub_id: SubscriptionId) -> Subscription | None: - pass - - @abc.abstractmethod - def create_subscription_for_user(self, user_id: int, order_id: int) -> Subscription: - pass - - @abc.abstractmethod - def delete_subscription(self, subscription_id: SubscriptionId): - pass - - @abc.abstractmethod - def update_subscription(self, subscription: UpdateSubscription) -> Subscription: - pass - - @abc.abstractmethod - def get_available_entries(self, user_id: int) -> dict[SubscriptionId, list[AvailableSubscriptionEntry]]: - pass - - @abc.abstractmethod - def get_unseen_available_subscription_entries(self, user_id: int) -> list[AvailableSubscriptionEntry]: - """ - Get all publication entries that _should_ be available to the user given the timeline but are newer than the - `sequence_seen` value from the subscription - """ - pass - - -class SubscriptionService: - def __init__(self, repo: SubscriptionRepo): - self._repo = repo - - def get_subscriptions(self, user_id: int) -> list[Subscription]: - return self._repo.get_subscriptions_for_user(user_id) - - def get_updates(self, user_id: int) -> list[AvailableSubscriptionEntry]: - return self._repo.get_unseen_available_subscription_entries(user_id) - - def get_all_available_entries(self, user_id: int) -> dict[SubscriptionId, list[AvailableSubscriptionEntry]]: - return self._repo.get_available_entries(user_id) - - def create_suscription(self, params: SubscriptionCreateParams) -> Subscription: - return self._repo.create_subscription_for_user(params.user_id, params.order_id) - - def delete_subcription(self, user_id: int, subscription_id: SubscriptionId): - subscription = self._repo.get_subscription_by_id(subscription_id) - if subscription is None: - # TODO Domain error types - raise RuntimeError('not found') - if subscription.user.id != user_id: - # TODO: Domain error types - raise RuntimeError('Cannot delete a subscription for another user') - - self._repo.delete_subscription(subscription_id)