diff --git a/apps/api/src/adapters/entrypoints/v1/routes.py b/apps/api/src/adapters/entrypoints/v1/routes.py index f392083..8b7a681 100644 --- a/apps/api/src/adapters/entrypoints/v1/routes.py +++ b/apps/api/src/adapters/entrypoints/v1/routes.py @@ -2,7 +2,7 @@ import secrets from uuid import UUID -from fastapi import APIRouter, Depends, HTTPException, Query, Response, status +from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response, status from fastapi.responses import StreamingResponse from fastapi.security import OAuth2PasswordBearer from src.adapters.entrypoints.v1.models.authentication import LoginRequest @@ -40,22 +40,10 @@ map_source_to_create_source_response, ) from src.adapters.entrypoints.v1.models.welcome import WelcomeResponse -from src.configs.dependencies.services import ( - get_feed_service, - get_filter_service, - get_job_service, - get_picker_service, - get_source_service, -) from src.configs.settings import Settings from src.domain.models.feed import FeedItemRequest, FeedRequest, UpdateFeedRequest from src.domain.models.picker import PickerRequest from src.domain.models.source import SourceRequest -from src.domain.services.feed_service import FeedService -from src.domain.services.filter_service import FilterService -from src.domain.services.job_service import JobService -from src.domain.services.picker_service import PickerService -from src.domain.services.source_service import SourceService settings: Settings = Settings() @@ -130,9 +118,11 @@ def login(data: LoginRequest): } ) def list_sources( + request: Request, _: str = Depends(authenticate), # noqa: B008 - source_service: SourceService = Depends(get_source_service) # noqa: B008 ) -> GetAllSourcesResponse: + source_service = request.app.state.job_service.source_service + source_list = source_service.get_all_sources() return GetAllSourcesResponse( sources=map_source_list_to_get_all_sources_response(source_list=source_list) @@ -161,9 +151,11 @@ def list_sources( ) def create_source( create_source_request: ExternalSourceRequest, + request: Request, _: str = Depends(authenticate), # noqa: B008 - source_service: SourceService = Depends(get_source_service) # noqa: B008 ) -> SourceResponse: + source_service = request.app.state.job_service.source_service + source_request = SourceRequest(name=create_source_request.name, url=create_source_request.url) created_source = source_service.create_source(source_request) return map_source_to_create_source_response(created_source) @@ -192,9 +184,11 @@ def create_source( ) def get_source( source_external_id: UUID, + request: Request, _: str = Depends(authenticate), # noqa: B008 - source_service: SourceService = Depends(get_source_service) # noqa: B008 ) -> SourceResponse: + source_service = request.app.state.job_service.source_service + source = source_service.get_source_by_external_id( source_external_id ) @@ -227,9 +221,11 @@ def get_source( def update_source( source_external_id: UUID, update_source_request: ExternalSourceRequest, + request: Request, _: str = Depends(authenticate), # noqa: B008 - source_service: SourceService = Depends(get_source_service), # noqa: B008 ) -> SourceResponse: + source_service = request.app.state.job_service.source_service + source_request = SourceRequest( name=update_source_request.name, url=update_source_request.url @@ -253,12 +249,14 @@ def update_source( ) def delete_source( source_external_id: UUID, + request: Request, _: str = Depends(authenticate), # noqa: B008 - source_service: SourceService = Depends(get_source_service), # noqa: B008 - filter_service: FilterService = Depends(get_filter_service), # noqa: B008 - picker_service: PickerService = Depends(get_picker_service), # noqa: B008 - job_service: JobService = Depends(get_job_service), # noqa: B008 ): + source_service = request.app.state.job_service.source_service + filter_service = request.app.state.job_service.filter_service + picker_service = request.app.state.job_service.picker_service + job_service = request.app.state.job_service + source = source_service.get_source_by_external_id( external_id=source_external_id ) @@ -296,9 +294,11 @@ def delete_source( } ) def list_feeds( + request: Request, _: str = Depends(authenticate), # noqa: B008 - feed_service: FeedService = Depends(get_feed_service) # noqa: B008 ) -> ListFeedsResponse: + feed_service = request.app.state.job_service.feed_service + detailed_feeds_list = feed_service.get_detailed_feeds() return map_detailed_feeds_list_to_list_feeds_response(detailed_feeds_list) @@ -326,9 +326,11 @@ def list_feeds( ) def create_feed( create_feed_request: CreateFeedRequest, + request: Request, _: str = Depends(authenticate), # noqa: B008 - feed_service: FeedService = Depends(get_feed_service) # noqa: B008 ) -> FeedResponse: + feed_service = request.app.state.job_service.feed_service + feed_request = FeedRequest(name=create_feed_request.name) created_feed = feed_service.create_feed(feed_request) return map_feed_to_feed_response(created_feed) @@ -358,9 +360,11 @@ def create_feed( def update_feed( feed_external_id: UUID, update_feed_request: ExternalUpdateFeedRequest, + request: Request, _: str = Depends(authenticate), # noqa: B008 - feed_service: FeedService = Depends(get_feed_service) # noqa: B008 ) -> FeedResponse: + feed_service = request.app.state.job_service.feed_service + update_feed_request = UpdateFeedRequest( name=update_feed_request.name ) @@ -383,12 +387,14 @@ def update_feed( ) def delete_feed( feed_external_id: UUID, + request: Request, _: str = Depends(authenticate), # noqa: B008 - feed_service: FeedService = Depends(get_feed_service), # noqa: B008 - filter_service: FilterService = Depends(get_filter_service), # noqa: B008 - picker_service: PickerService = Depends(get_picker_service), # noqa: B008 - job_service: JobService = Depends(get_job_service), # noqa: B008 ): + feed_service = request.app.state.job_service.feed_service + filter_service = request.app.state.job_service.filter_service + picker_service = request.app.state.job_service.picker_service + job_service = request.app.state.job_service + feed = feed_service.get_feed_by_external_id(external_id=feed_external_id) if not feed: raise HTTPException(status_code=404, detail="Feed not found") @@ -428,12 +434,14 @@ def delete_feed( ) def get_feed_rss( external_id: UUID, - feed_service: FeedService = Depends(get_feed_service) # noqa: B008 + request: Request, ): """ Returns raw RSS XML for the requested feed. The response content type is `application/rss+xml`. """ + feed_service = request.app.state.job_service.feed_service + feeds_rss = feed_service.get_rss(external_id) if feeds_rss: return Response( @@ -465,6 +473,7 @@ def get_feed_rss( } ) def get_feed( + request: Request, external_id: UUID, title: str | None = Query(None), last_day: bool | None = Query(None), @@ -472,11 +481,12 @@ def get_feed( feed_items_limit: int | None = Query(None, ge=1), feed_items_offset: int | None= Query(None, ge=0), _: str = Depends(authenticate), # noqa: B008 - feed_service: FeedService = Depends(get_feed_service), # noqa: B008 - filter_service: FilterService = Depends(get_filter_service), # noqa: B008 - picker_service: PickerService = Depends(get_picker_service), # noqa: B008 - source_service: SourceService = Depends(get_source_service), # noqa: B008 ) -> FullCompleteFeed: + feed_service = request.app.state.job_service.feed_service + filter_service = request.app.state.job_service.filter_service + picker_service = request.app.state.job_service.picker_service + source_service = request.app.state.job_service.source_service + feed = feed_service.get_feed_by_external_id(external_id) if not feed: raise HTTPException(status_code=404, detail="Feed not found") @@ -552,9 +562,11 @@ def get_feed( def create_feed_item( feed_external_id: UUID, create_feed_item_request: CreateFeedItemRequest, + request: Request, _: str = Depends(authenticate), # noqa: B008 - feed_service: FeedService = Depends(get_feed_service), # noqa: B008 ) -> CreateFeedItemResponse: + feed_service = request.app.state.job_service.feed_service + feed = feed_service.get_feed_by_external_id(feed_external_id) if not feed: raise HTTPException(status_code=400, detail="Feed not found") @@ -600,9 +612,11 @@ def create_feed_item( def get_feed_item( feed_external_id: UUID, feed_item_external_id: UUID, + request: Request, _: str = Depends(authenticate), # noqa: B008 - feed_service: FeedService = Depends(get_feed_service), # noqa: B008 ) -> GetFeedItemResponse: + feed_service = request.app.state.job_service.feed_service + feed_item = feed_service.get_feed_item_by_external_id(feed_item_external_id) if not feed_item: raise HTTPException(status_code=400, detail="Feed item not found") @@ -620,9 +634,11 @@ def get_feed_item( def delete_feed_item( feed_external_id: UUID, feed_item_external_id: UUID, + request: Request, _: str = Depends(authenticate), # noqa: B008 - feed_service: FeedService = Depends(get_feed_service), # noqa: B008 ): + feed_service = request.app.state.job_service.feed_service + feed_item = feed_service.get_feed_item_by_external_id( feed_item_external_id=feed_item_external_id ) @@ -649,9 +665,11 @@ def delete_feed_item( def export_feed_items( feed_external_id: UUID, export_feed_items_request: ExportFeedItemsRequest, + request: Request, _: str = Depends(authenticate), # noqa: B008 - feed_service: FeedService = Depends(get_feed_service), # noqa: B008 ): + feed_service = request.app.state.job_service.feed_service + buffer = feed_service.export_file( feed_external_id, export_feed_items_request.file_type, @@ -692,13 +710,14 @@ def export_feed_items( ) def add_picker( create_full_picker_request: CreateFullPickerRequest, + request: Request, _: str = Depends(authenticate), # noqa: B008 - filter_service: FilterService = Depends(get_filter_service), # noqa: B008 - picker_service: PickerService = Depends(get_picker_service), # noqa: B008 - source_service: SourceService = Depends(get_source_service), # noqa: B008 - feed_service: FeedService = Depends(get_feed_service), # noqa: B008 - job_service: JobService = Depends(get_job_service), # noqa: B008 ) -> FullPickerResponse: + feed_service = request.app.state.job_service.feed_service + filter_service = request.app.state.job_service.filter_service + picker_service = request.app.state.job_service.picker_service + source_service = request.app.state.job_service.source_service + job_service = request.app.state.job_service feed_name = None if create_full_picker_request.feed_name: @@ -797,12 +816,14 @@ def add_picker( ) def get_picker( picker_external_id: UUID, + request: Request, _: str = Depends(authenticate), # noqa: B008 - filter_service: FilterService = Depends(get_filter_service), # noqa: B008 - picker_service: PickerService = Depends(get_picker_service), # noqa: B008 - feed_service: FeedService = Depends(get_feed_service), # noqa: B008 - source_service: SourceService = Depends(get_source_service), # noqa: B008 ) -> FullPickerResponse | None: + feed_service = request.app.state.job_service.feed_service + filter_service = request.app.state.job_service.filter_service + picker_service = request.app.state.job_service.picker_service + source_service = request.app.state.job_service.source_service + picker = picker_service.get_picker_by_external_id(external_id=picker_external_id) if not picker: raise HTTPException(status_code=404, detail="Picker not found") @@ -832,11 +853,13 @@ def get_picker( ) def delete_picker( picker_external_id: UUID, + request: Request, _: str = Depends(authenticate), # noqa: B008 - filter_service: FilterService = Depends(get_filter_service), # noqa: B008 - picker_service: PickerService = Depends(get_picker_service), # noqa: B008 - job_service: JobService = Depends(get_job_service), # noqa: B008 ): + filter_service = request.app.state.job_service.filter_service + picker_service = request.app.state.job_service.picker_service + job_service = request.app.state.job_service + picker = picker_service.get_picker_by_external_id(external_id=picker_external_id) if not picker: raise HTTPException(status_code=404, detail="Picker not found") diff --git a/apps/api/src/adapters/repositories/feeds_repository.py b/apps/api/src/adapters/repositories/feeds_repository.py index b04ea5e..1183322 100644 --- a/apps/api/src/adapters/repositories/feeds_repository.py +++ b/apps/api/src/adapters/repositories/feeds_repository.py @@ -2,7 +2,7 @@ from uuid import UUID from sqlalchemy import text -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from src.domain.models.feed import Feed, FeedItem, FeedItemRequest, FeedRequest, UpdateFeedRequest from src.domain.ports.feeds_port import FeedsPort @@ -11,8 +11,8 @@ class FeedsRepository(FeedsPort): - def __init__(self, db: Session): - self.db = db + def __init__(self, session_factory: sessionmaker): + self.session_factory = session_factory def create_feed(self, feed_request: FeedRequest) -> Feed: sql = text( @@ -20,22 +20,20 @@ def create_feed(self, feed_request: FeedRequest) -> Feed: "VALUES (:name) " "RETURNING id, name, external_id, created_at, updated_at" ) - result = self.db.execute( - sql, - {"name": feed_request.name} - ).first() - - self.db.commit() - - data = result._mapping - return Feed( - id=data["id"], - name=data["name"], - external_id=data["external_id"], - created_at=data["created_at"], - updated_at=data["updated_at"] - ) - + with self.session_factory() as session: + result = session.execute( + sql, + {"name": feed_request.name} + ).first() + session.commit() + data = result._mapping + return Feed( + id=data["id"], + name=data["name"], + external_id=data["external_id"], + created_at=data["created_at"], + updated_at=data["updated_at"] + ) def update_feed( self, @@ -55,55 +53,52 @@ def update_feed( RETURNING id, external_id, name, created_at, updated_at """) values["id"] = feed_id - result = self.db.execute(sql, values).mappings().first() - self.db.commit() - - if not result: - raise ValueError(f"Feed with id {feed_id} not found") - - return Feed( - id=result["id"], - external_id=result["external_id"], - name=result["name"], - created_at=result["created_at"], - updated_at=result["updated_at"] - ) + + with self.session_factory() as session: + result = session.execute(sql, values).mappings().first() + session.commit() + + if not result: + raise ValueError(f"Feed with id {feed_id} not found") + + return Feed( + id=result["id"], + external_id=result["external_id"], + name=result["name"], + created_at=result["created_at"], + updated_at=result["updated_at"] + ) def delete_feed(self, feed_id: int) -> bool: sql = text("DELETE FROM feeds WHERE id = :id RETURNING id") - result = self.db.execute(sql, {"id": feed_id}).first() - self.db.commit() - return result is not None + with self.session_factory() as session: + result = session.execute(sql, {"id": feed_id}).first() + session.commit() + return result is not None def get_all_feeds(self) -> list[Feed]: sql = text("SELECT id, external_id, name, created_at, updated_at FROM feeds") - result = self.db.execute(sql) - - return [ - Feed(**item._mapping) for item in result - ] + with self.session_factory() as session: + result = session.execute(sql) + return [Feed(**item._mapping) for item in result] def get_feed_by_external_id(self, external_id: UUID) -> Feed | None: sql = text( "SELECT id, name, external_id, created_at, updated_at " "FROM feeds WHERE external_id = :external_id;" ) - result = self.db.execute(sql, {"external_id": external_id}).mappings().first() - - if result: - return Feed(**result) - return None + with self.session_factory() as session: + result = session.execute(sql, {"external_id": external_id}).mappings().first() + return Feed(**result) if result else None def get_feed_by_id(self, id: int) -> Feed | None: sql = text( "SELECT id, name, external_id, created_at, updated_at " "FROM feeds WHERE id = :id;" ) - result = self.db.execute(sql, {"id": id}).mappings().first() - - if result: - return Feed(**result) - return None + with self.session_factory() as session: + result = session.execute(sql, {"id": id}).mappings().first() + return Feed(**result) if result else None def get_all_feed_items_by_feed_id(self, feed_id: int) -> list[FeedItem]: sql = text( @@ -112,12 +107,9 @@ def get_all_feed_items_by_feed_id(self, feed_id: int) -> list[FeedItem]: "FROM feed_items WHERE feed_id = :feed_id " "ORDER BY created_at DESC;" ) - result = self.db.execute( - sql, - {"feed_id": feed_id} - ).mappings() - - return [FeedItem(**feed_item) for feed_item in result] + with self.session_factory() as session: + result = session.execute(sql, {"feed_id": feed_id}).mappings() + return [FeedItem(**feed_item) for feed_item in result] def get_active_feed_items_by_feed_id(self, feed_id: int) -> list[FeedItem]: sql = text( @@ -127,27 +119,27 @@ def get_active_feed_items_by_feed_id(self, feed_id: int) -> list[FeedItem]: "WHERE feed_id = :feed_id AND is_active = TRUE " "ORDER BY created_at DESC;" ) - result = self.db.execute( - sql, - {"feed_id": feed_id} - ).mappings() - - return [FeedItem(**feed_item) for feed_item in result] + with self.session_factory() as session: + result = session.execute(sql, {"feed_id": feed_id}).mappings() + return [FeedItem(**feed_item) for feed_item in result] def get_feed_item_by_feed_item_external_id( - self, - feed_item_external_id: UUID + self, + feed_item_external_id: UUID ) -> FeedItem | None: sql = text( "SELECT id, feed_id, external_id, link, title, description, author, created_at, " "content, reading_time " "FROM feed_items WHERE external_id = :external_id;" ) - result = self.db.execute(sql, {"external_id": feed_item_external_id}).mappings().first() - - if result: - return FeedItem(**result) - return None + with self.session_factory() as session: + result = session.execute( + sql, + { + "external_id": feed_item_external_id + } + ).mappings().first() + return FeedItem(**result) if result else None def create_feed_item(self, feed_item_request: FeedItemRequest) -> FeedItem: if feed_item_request.created_at is None: @@ -160,43 +152,43 @@ def create_feed_item(self, feed_item_request: FeedItemRequest) -> FeedItem: "RETURNING id, feed_id, external_id, link, title, author, description, content, " "reading_time, created_at, image_url" ) - result = self.db.execute( - sql, - { - "feed_id": feed_item_request.feed_id, - "link": feed_item_request.link, - "title": feed_item_request.title, - "description": feed_item_request.description, - "author": feed_item_request.author, - "content": feed_item_request.content, - "reading_time": feed_item_request.reading_time, - "created_at": feed_item_request.created_at, - "image_url": feed_item_request.image_url - } - ).first() - - self.db.commit() - - data = result._mapping - return FeedItem( - id=data["id"], - feed_id=data["feed_id"], - external_id=data["external_id"], - link=data["link"], - title=data["title"], - description=data["description"], - author=data["author"], - content=data["content"], - reading_time=data["reading_time"], - created_at=data["created_at"], - image_url=data["image_url"] - ) + with self.session_factory() as session: + result = session.execute( + sql, + { + "feed_id": feed_item_request.feed_id, + "link": feed_item_request.link, + "title": feed_item_request.title, + "description": feed_item_request.description, + "author": feed_item_request.author, + "content": feed_item_request.content, + "reading_time": feed_item_request.reading_time, + "created_at": feed_item_request.created_at, + "image_url": feed_item_request.image_url + } + ).first() + session.commit() + data = result._mapping + return FeedItem( + id=data["id"], + feed_id=data["feed_id"], + external_id=data["external_id"], + link=data["link"], + title=data["title"], + description=data["description"], + author=data["author"], + content=data["content"], + reading_time=data["reading_time"], + created_at=data["created_at"], + image_url=data["image_url"] + ) def delete_feed_item(self, feed_item_id: int) -> bool: sql = text("DELETE FROM feed_items WHERE id = :id RETURNING id") - result = self.db.execute(sql, {"id": feed_item_id}).first() - self.db.commit() - return result is not None + with self.session_factory() as session: + result = session.execute(sql, {"id": feed_item_id}).first() + session.commit() + return result is not None def get_number_of_feed_items_by_feed_id(self, feed_id: int): sql = text(""" @@ -204,9 +196,8 @@ def get_number_of_feed_items_by_feed_id(self, feed_id: int): FROM feed_items WHERE feed_id = :feed_id AND is_active = true; """) - - result = self.db.execute(sql, {"feed_id": feed_id}).scalar() - return result + with self.session_factory() as session: + return session.execute(sql, {"feed_id": feed_id}).scalar() def set_feed_item_as_inactive(self, feed_item_id: int): sql = text( @@ -215,17 +206,19 @@ def set_feed_item_as_inactive(self, feed_item_id: int): "WHERE id = :id " "RETURNING id" ) - result = self.db.execute(sql, {"id": feed_item_id}).first() - self.db.commit() - return result is not None + with self.session_factory() as session: + result = session.execute(sql, {"id": feed_item_id}).first() + session.commit() + return result is not None - def set_updated_at(self, feed_id: int) -> datetime: + def set_updated_at(self, feed_id: int) -> bool: sql = text( "UPDATE feeds " "SET updated_at = CURRENT_TIMESTAMP " "WHERE id = :id " "RETURNING updated_at" ) - result = self.db.execute(sql, {"id": feed_id}).first() - self.db.commit() - return result is not None + with self.session_factory() as session: + result = session.execute(sql, {"id": feed_id}).first() + session.commit() + return result is not None diff --git a/apps/api/src/adapters/repositories/filters_repository.py b/apps/api/src/adapters/repositories/filters_repository.py index 6316876..8480ef6 100644 --- a/apps/api/src/adapters/repositories/filters_repository.py +++ b/apps/api/src/adapters/repositories/filters_repository.py @@ -1,13 +1,13 @@ from sqlalchemy import text -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from src.domain.models.filter import Filter, FilterRequest from src.domain.ports.filters_port import FiltersPort class FiltersRepository(FiltersPort): - def __init__(self, db: Session): - self.db = db + def __init__(self, session_factory: sessionmaker): + self.session_factory = session_factory def create_filter(self, filter_request: FilterRequest) -> Filter: sql = text( @@ -15,39 +15,42 @@ def create_filter(self, filter_request: FilterRequest) -> Filter: "VALUES (:picker_id, :operation, :args) " "RETURNING id, picker_id, operation, args, created_at" ) - result = self.db.execute( - sql, - { - "picker_id": filter_request.picker_id, - "operation": filter_request.operation, - "args": filter_request.args - } - ).first() - - self.db.commit() - - data = result._mapping - return Filter( - id=data["id"], - picker_id=data["picker_id"], - operation=data["operation"], - args=data["args"], - created_at=data["created_at"], - ) + with self.session_factory() as session: + result = session.execute( + sql, + { + "picker_id": filter_request.picker_id, + "operation": filter_request.operation, + "args": filter_request.args + } + ).first() + + session.commit() + + data = result._mapping + return Filter( + id=data["id"], + picker_id=data["picker_id"], + operation=data["operation"], + args=data["args"], + created_at=data["created_at"], + ) def delete_filter(self, filter_id: int) -> bool: sql = text("DELETE FROM filters WHERE id = :id RETURNING id") - result = self.db.execute(sql, {"id": filter_id}).first() - self.db.commit() - return result is not None + with self.session_factory() as session: + result = session.execute(sql, {"id": filter_id}).first() + session.commit() + return result is not None def get_filter_by_picker_id(self, picker_id: int) -> list[Filter]: sql = text( "SELECT id, picker_id, operation, args, created_at " "FROM filters WHERE picker_id = :picker_id;" ) - result = self.db.execute(sql, {"picker_id": picker_id}) - - return [ - Filter(**item._mapping) for item in result - ] + with self.session_factory() as session: + result = session.execute(sql, {"picker_id": picker_id}) + # We convert to domain models before the session closes + return [ + Filter(**item._mapping) for item in result + ] diff --git a/apps/api/src/adapters/repositories/pickers_repository.py b/apps/api/src/adapters/repositories/pickers_repository.py index 24da83f..dfab7d1 100644 --- a/apps/api/src/adapters/repositories/pickers_repository.py +++ b/apps/api/src/adapters/repositories/pickers_repository.py @@ -1,15 +1,15 @@ from uuid import UUID from sqlalchemy import text -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from src.domain.models.picker import Picker, PickerRequest from src.domain.ports.pickers_port import PickersPort class PickersRepository(PickersPort): - def __init__(self, db: Session): - self.db = db + def __init__(self, session_factory: sessionmaker): + self.session_factory = session_factory def create_picker(self, picker_request: PickerRequest) -> Picker: sql = text( @@ -17,54 +17,58 @@ def create_picker(self, picker_request: PickerRequest) -> Picker: "VALUES (:source_id, :feed_id, :cronjob) " "RETURNING id, external_id, source_id, feed_id, cronjob, created_at" ) - result = self.db.execute( - sql, - { - "source_id": picker_request.source_id, - "feed_id": picker_request.feed_id, - "cronjob": picker_request.cronjob - } - ).first() - - self.db.commit() - - data = result._mapping - return Picker( - id=data["id"], - external_id=data["external_id"], - source_id=data["source_id"], - feed_id=data["feed_id"], - cronjob=data["cronjob"], - created_at=data["created_at"], - ) + with self.session_factory() as session: + result = session.execute( + sql, + { + "source_id": picker_request.source_id, + "feed_id": picker_request.feed_id, + "cronjob": picker_request.cronjob + } + ).first() + + session.commit() + + data = result._mapping + return Picker( + id=data["id"], + external_id=data["external_id"], + source_id=data["source_id"], + feed_id=data["feed_id"], + cronjob=data["cronjob"], + created_at=data["created_at"], + ) def delete_picker(self, picker_id: int) -> bool: sql = text("DELETE FROM pickers WHERE id = :id RETURNING id") - result = self.db.execute(sql, {"id": picker_id}).first() - self.db.commit() - return result is not None + with self.session_factory() as session: + result = session.execute(sql, {"id": picker_id}).first() + session.commit() + return result is not None def get_picker_by_external_id(self, external_id: UUID) -> Picker | None: sql = text( "SELECT id, external_id, source_id, feed_id, cronjob, created_at " "FROM pickers WHERE external_id = :external_id;" ) - result = self.db.execute(sql, {"external_id": external_id}).mappings().first() + with self.session_factory() as session: + result = session.execute(sql, {"external_id": external_id}).mappings().first() - if result: - return Picker(**result) - return None + if result: + return Picker(**result) + return None def get_picker_by_id(self, picker_id: int) -> Picker | None: sql = text( "SELECT id, external_id, source_id, feed_id, cronjob, created_at " "FROM pickers WHERE id = :picker_id;" ) - result = self.db.execute(sql, {"picker_id": picker_id}).mappings().first() + with self.session_factory() as session: + result = session.execute(sql, {"picker_id": picker_id}).mappings().first() - if result: - return Picker(**result) - return None + if result: + return Picker(**result) + return None def get_pickers_by_feed_id( self, @@ -74,24 +78,24 @@ def get_pickers_by_feed_id( "SELECT id, external_id, source_id, feed_id, cronjob, created_at " "FROM pickers WHERE feed_id = :feed_id;" ) - result = self.db.execute(sql, {"feed_id": feed_id}).mappings() - - return [Picker(**picker) for picker in result] + with self.session_factory() as session: + result = session.execute(sql, {"feed_id": feed_id}).mappings() + return [Picker(**picker) for picker in result] def get_all_pickers(self) -> list[Picker]: sql = text( "SELECT id, external_id, source_id, feed_id, cronjob, created_at " "FROM pickers;" ) - result = self.db.execute(sql).mappings() - - return [Picker(**picker) for picker in result] + with self.session_factory() as session: + result = session.execute(sql).mappings() + return [Picker(**picker) for picker in result] def get_picker_by_source_id(self, source_id: int) -> list[Picker]: sql = text( "SELECT id, external_id, source_id, feed_id, cronjob, created_at " "FROM pickers WHERE source_id = :source_id;" ) - result = self.db.execute(sql, {"source_id": source_id}).mappings() - - return [Picker(**picker) for picker in result] + with self.session_factory() as session: + result = session.execute(sql, {"source_id": source_id}).mappings() + return [Picker(**picker) for picker in result] diff --git a/apps/api/src/adapters/repositories/sources_repository.py b/apps/api/src/adapters/repositories/sources_repository.py index c223ded..a094e36 100644 --- a/apps/api/src/adapters/repositories/sources_repository.py +++ b/apps/api/src/adapters/repositories/sources_repository.py @@ -1,15 +1,15 @@ from uuid import UUID from sqlalchemy import text -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from src.domain.models.source import Source, SourceRequest from src.domain.ports.sources_port import SourcePort class SourcesRepository(SourcePort): - def __init__(self, db: Session): - self.db = db + def __init__(self, session_factory: sessionmaker): + self.session_factory = session_factory def create_source(self, source_request: SourceRequest) -> Source: sql = text( @@ -17,23 +17,23 @@ def create_source(self, source_request: SourceRequest) -> Source: "VALUES (:url, :name) " "RETURNING id, external_id, url, name, created_at" ) - result = self.db.execute( - sql, - { - "url": source_request.url, - "name": source_request.name - } - ).mappings().first() - - self.db.commit() - - return Source( - id=result["id"], - external_id=result["external_id"], - url=result["url"], - name=result["name"], - created_at=result["created_at"], - ) + with self.session_factory() as session: + result = session.execute( + sql, + { + "url": source_request.url, + "name": source_request.name + } + ).mappings().first() + session.commit() + + return Source( + id=result["id"], + external_id=result["external_id"], + url=result["url"], + name=result["name"], + created_at=result["created_at"], + ) def update_source(self, source_id: int, source_request: SourceRequest) -> Source: sql = text( @@ -42,76 +42,78 @@ def update_source(self, source_id: int, source_request: SourceRequest) -> Source "WHERE id = :id " "RETURNING id, external_id, url, name, created_at" ) - result = self.db.execute( - sql, - { - "id": source_id, - "url": source_request.url, - "name": source_request.name - } - ).mappings().first() - self.db.commit() - - return Source( - id=result["id"], - external_id=result["external_id"], - url=result["url"], - name=result["name"], - created_at=result["created_at"], - ) + with self.session_factory() as session: + result = session.execute( + sql, + { + "id": source_id, + "url": source_request.url, + "name": source_request.name + } + ).mappings().first() + session.commit() + + return Source( + id=result["id"], + external_id=result["external_id"], + url=result["url"], + name=result["name"], + created_at=result["created_at"], + ) def delete_source(self, source_id: int) -> bool: sql = text("DELETE FROM sources WHERE id = :id RETURNING id") - result = self.db.execute(sql, {"id": source_id}).first() - self.db.commit() - return result is not None + with self.session_factory() as session: + result = session.execute(sql, {"id": source_id}).first() + session.commit() + return result is not None def get_all_sources(self) -> list[Source]: sql = text("SELECT id, external_id, url, name, created_at FROM sources") - result = self.db.execute(sql) - - if result: - return [ - Source(**item._mapping) for item in result - ] - return [] + with self.session_factory() as session: + result = session.execute(sql) + if result: + return [ + Source(**item._mapping) for item in result + ] + return [] def get_source_by_external_id(self, external_id: UUID) -> Source | None: sql = text( "SELECT id, external_id, url, name, created_at " "FROM sources WHERE external_id = :external_id;" ) - result = self.db.execute(sql, {"external_id": str(external_id)}).mappings().first() - - if result: - return Source(**result) - return None + with self.session_factory() as session: + result = session.execute(sql, {"external_id": str(external_id)}).mappings().first() + if result: + return Source(**result) + return None def get_source_by_url(self, url: str) -> Source | None: sql = text( "SELECT id, external_id, url, name, created_at " "FROM sources WHERE url = :url;" ) - result = self.db.execute(sql, {"url": url}).mappings().first() - - if result is None: - return None - - return Source( - id=result["id"], - external_id=result["external_id"], - url=result["url"], - name=result["name"], - created_at=result["created_at"], - ) + with self.session_factory() as session: + result = session.execute(sql, {"url": url}).mappings().first() + if result is None: + return None + + return Source( + id=result["id"], + external_id=result["external_id"], + url=result["url"], + name=result["name"], + created_at=result["created_at"], + ) def get_source_by_id(self, id: int) -> Source | None: sql = text( "SELECT id, external_id, url, name, created_at " "FROM sources WHERE id = :id;" ) - result = self.db.execute(sql, {"id": str(id)}).mappings().first() - - if result: - return Source(**result) - return None + with self.session_factory() as session: + result = session.execute(sql, {"id": id}).mappings().first() + if result: + return Source(**result) + return None diff --git a/apps/api/src/configs/database.py b/apps/api/src/configs/database.py index 36d22ed..4d91aef 100644 --- a/apps/api/src/configs/database.py +++ b/apps/api/src/configs/database.py @@ -1,13 +1,12 @@ from sqlalchemy import create_engine -from sqlalchemy.orm import Session, sessionmaker +from sqlalchemy.orm import sessionmaker from src.configs.settings import settings # Engine & Session factory -engine = create_engine(settings.DATABASE_URL, future=True) +engine = create_engine(settings.DATABASE_URL, future=True, pool_pre_ping=True) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) - -def get_db() -> Session: +def get_db(): db = SessionLocal() try: yield db diff --git a/apps/api/src/configs/dependencies/__init__.py b/apps/api/src/configs/dependencies/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/api/src/configs/dependencies/repositories.py b/apps/api/src/configs/dependencies/repositories.py deleted file mode 100644 index c66b884..0000000 --- a/apps/api/src/configs/dependencies/repositories.py +++ /dev/null @@ -1,20 +0,0 @@ -from fastapi import Depends -from sqlalchemy.orm import Session -from src.adapters.repositories.feeds_repository import FeedsRepository -from src.adapters.repositories.filters_repository import FiltersRepository -from src.adapters.repositories.pickers_repository import PickersRepository -from src.adapters.repositories.sources_repository import SourcesRepository -from src.configs.database import get_db - - -def get_sources_repository(db: Session = Depends(get_db)) -> SourcesRepository: # noqa: B008 - return SourcesRepository(db) - -def get_feeds_repository(db: Session = Depends(get_db)) -> FeedsRepository: # noqa: B008 - return FeedsRepository(db) - -def get_filters_repository(db: Session = Depends(get_db)) -> FiltersRepository: # noqa: B008 - return FiltersRepository(db) - -def get_pickers_repository(db: Session = Depends(get_db)) -> PickersRepository: # noqa: B008 - return PickersRepository(db) diff --git a/apps/api/src/configs/dependencies/services.py b/apps/api/src/configs/dependencies/services.py deleted file mode 100644 index 6b33831..0000000 --- a/apps/api/src/configs/dependencies/services.py +++ /dev/null @@ -1,57 +0,0 @@ -from fastapi import Depends, Request -from src.adapters.repositories.feeds_repository import FeedsRepository -from src.adapters.repositories.filters_repository import FiltersRepository -from src.adapters.repositories.pickers_repository import PickersRepository -from src.adapters.repositories.sources_repository import SourcesRepository -from src.adapters.wallabag_extractor import WallabagExtractor -from src.configs.dependencies.repositories import ( - get_feeds_repository, - get_filters_repository, - get_pickers_repository, - get_sources_repository, -) -from src.domain.services.extractor_service import ExtractorService -from src.domain.services.feed_service import FeedService -from src.domain.services.filter_service import FilterService -from src.domain.services.job_service import JobService -from src.domain.services.picker_service import PickerService -from src.domain.services.source_service import SourceService - - -def get_source_service( - repository: SourcesRepository = Depends(get_sources_repository) # noqa: B008 -) -> SourceService: - return SourceService(source_port=repository) - - -def get_wallabag_extractor() -> WallabagExtractor: # noqa: B008 - return WallabagExtractor() - - -def get_extractor_service( - extractor: WallabagExtractor = Depends(get_wallabag_extractor) # noqa: B008 -) -> ExtractorService: - return ExtractorService(extractor_port=extractor) - - -def get_feed_service( - repository: FeedsRepository = Depends(get_feeds_repository), # noqa: B008 - extractor_service: ExtractorService = Depends(get_extractor_service) # noqa: B008 -) -> FeedService: - return FeedService(feeds_port=repository, extractor_service=extractor_service) - - -def get_filter_service( - repository: FiltersRepository = Depends(get_filters_repository) # noqa: B008 -) -> FilterService: - return FilterService(filters_port=repository) - - -def get_picker_service( - repository: PickersRepository = Depends(get_pickers_repository) # noqa: B008 -) -> PickerService: - return PickerService(pickers_port=repository) - - -def get_job_service(request: Request) -> JobService: - return request.app.state.job_service diff --git a/apps/api/src/domain/handlers/job_processors.py b/apps/api/src/domain/handlers/job_processors.py index 9af6fa0..57bb73b 100644 --- a/apps/api/src/domain/handlers/job_processors.py +++ b/apps/api/src/domain/handlers/job_processors.py @@ -1,5 +1,16 @@ -def process_filters( - picker_id: str, - job_service -): - job_service.process(int(picker_id)) +import ctypes +import gc + + +def process_filters(picker_id: str): + try: + from src.main import app + app.state.job_service.process(int(picker_id)) + + finally: + gc.collect() + try: + libc = ctypes.CDLL("libc.so.6") + libc.malloc_trim(0) + except Exception: + pass diff --git a/apps/api/src/domain/services/job_service.py b/apps/api/src/domain/services/job_service.py index 86ad812..2ebac16 100644 --- a/apps/api/src/domain/services/job_service.py +++ b/apps/api/src/domain/services/job_service.py @@ -52,7 +52,7 @@ def __init__( def add_cronjob(self, picker: Picker): job = Job( func_name='process_filters', - args=[str(picker.id), self], + args=[str(picker.id)], schedule=picker.cronjob ) self.scheduler.add_job(job) @@ -60,7 +60,7 @@ def add_cronjob(self, picker: Picker): def delete_cronjob(self, picker: Picker): job_to_delete = Job( func_name='process_filters', - args=[str(picker.id), self], + args=[str(picker.id)], schedule=picker.cronjob ) self.scheduler.delete_job(job_to_delete) @@ -71,7 +71,7 @@ def load_all(self): for picker in pickers: job = Job( func_name='process_filters', - args=[str(picker.id), self], + args=[str(picker.id)], schedule=picker.cronjob ) jobs.append(job) diff --git a/apps/api/src/main.py b/apps/api/src/main.py index 639c164..bb31334 100644 --- a/apps/api/src/main.py +++ b/apps/api/src/main.py @@ -1,4 +1,5 @@ import logging +from contextlib import asynccontextmanager from fastapi import FastAPI, Request from src.adapters.entrypoints.v1.models.welcome import WelcomeResponse @@ -9,7 +10,7 @@ from src.adapters.repositories.sources_repository import SourcesRepository from src.adapters.scheduler import Scheduler from src.adapters.wallabag_extractor import WallabagExtractor -from src.configs.database import get_db +from src.configs.database import SessionLocal from src.configs.settings import Settings from src.domain.services.extractor_service import ExtractorService from src.domain.services.feed_service import FeedService @@ -21,44 +22,35 @@ # CONSTANTS settings: Settings = Settings() -# API -app = FastAPI( - title="NebulaPicker", - description=( - "NebulaPicker is a self-hosted API for content curation, designed to streamline and " - "automate the process of filtering online information. It functions as a personalized " - "feed generator that fetches content from multiple RSS sources, applies user-defined " - "filters to remove noise, and publishes a new, clean feed tailored to specific interests." - ), - version="1.0.0", - docs_url="/docs", - redoc_url="/redoc", - openapi_url="/openapi.json", -) - # LOGGING CONFIGURATION logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" ) -app.logger = logging.getLogger(__name__) +logger = logging.getLogger(__name__) # SCHEDULER scheduler_adapter = Scheduler() -@app.on_event("startup") -def startup(): - db_session = next(get_db()) - feed_repository = FeedsRepository(db_session) - source_repository = SourcesRepository(db_session) - picker_repository = PickersRepository(db_session) - filter_repository = FiltersRepository(db_session) +@asynccontextmanager +async def lifespan(app: FastAPI): + # STARTUP + feed_repository = FeedsRepository(SessionLocal) + source_repository = SourcesRepository(SessionLocal) + picker_repository = PickersRepository(SessionLocal) + filter_repository = FiltersRepository(SessionLocal) + wallabag_service = WallabagExtractor() source_service = SourceService(source_port=source_repository) picker_service = PickerService(pickers_port=picker_repository) filter_service = FilterService(filters_port=filter_repository) extractor_service = ExtractorService(extractor_port=wallabag_service) - feed_service = FeedService(feeds_port=feed_repository, extractor_service=extractor_service) + + feed_service = FeedService( + feeds_port=feed_repository, + extractor_service=extractor_service + ) + job_service = JobService( feed_service=feed_service, source_service=source_service, @@ -68,26 +60,46 @@ def startup(): extractor_service=extractor_service, feeds_port=feed_repository ) + app.state.job_service = job_service scheduler_adapter.start() - job_service.load_all() + try: + job_service.load_all() + logger.info("Scheduler started and jobs loaded successfully.") + except Exception as e: + logger.error(f"Failed to load jobs on startup: {e}") + + yield -@app.on_event("shutdown") -def shutdown(): + # SHUTDOWN scheduler_adapter.shutdown() + logger.info("Scheduler shut down successfully.") + + +app = FastAPI( + title="NebulaPicker", + description=( + "NebulaPicker is a self-hosted API for content curation, designed to streamline and " + "automate the process of filtering online information." + ), + version="1.0.0", + lifespan=lifespan, + docs_url="/docs", + redoc_url="/redoc", + openapi_url="/openapi.json", +) @app.get( "/", summary="Welcome", - description="Return a welcome message for the API.", response_model=WelcomeResponse, tags=["General"] ) def welcome(request: Request): - message_collection = WelcomeResponse() - return message_collection + return WelcomeResponse() + # Include all v1 routes app.include_router(v1_router) diff --git a/apps/api/tests/integration/test_main.py b/apps/api/tests/integration/test_main.py index e5bda28..50fb14d 100644 --- a/apps/api/tests/integration/test_main.py +++ b/apps/api/tests/integration/test_main.py @@ -14,7 +14,7 @@ from sqlalchemy.orm import Session, sessionmaker from src.adapters.entrypoints.v1.models.welcome import WELCOME_MESSAGE from src.adapters.scheduler import Scheduler -from src.configs.dependencies.repositories import get_db +from src.configs.database import get_db from src.domain.services.job_service import JobService from src.main import app @@ -40,25 +40,46 @@ def mock_services(): @pytest.fixture(autouse=True) -def setup_job_service(mock_services): - scheduler = Scheduler() - picker_service = mock_services["picker_service"] - filter_service = mock_services["filter_service"] - source_service = mock_services["source_service"] - feed_service = mock_services["feed_service"] - extractor_service = mock_services["extractor_service"] - feeds_repository = mock_services["feeds_port"] +def setup_job_service(test_db_manager): + from sqlalchemy import create_engine + from sqlalchemy.orm import sessionmaker + engine = create_engine(test_db_manager) + test_session_local = sessionmaker(bind=engine) + + # Initialize REAL repositories with the TEST session factory + from src.adapters.repositories.feeds_repository import FeedsRepository + from src.adapters.repositories.filters_repository import FiltersRepository + from src.adapters.repositories.pickers_repository import PickersRepository + from src.adapters.repositories.sources_repository import SourcesRepository + + feeds_repo = FeedsRepository(test_session_local) + pickers_repo = PickersRepository(test_session_local) + sources_repo = SourcesRepository(test_session_local) + filters_repo = FiltersRepository(test_session_local) + + # Initialize Services + from src.domain.services.feed_service import FeedService + from src.domain.services.filter_service import FilterService + from src.domain.services.picker_service import PickerService + from src.domain.services.source_service import SourceService + + source_service = SourceService(sources_repo) + picker_service = PickerService(pickers_repo) + filter_service = FilterService(filters_repo) + extractor_mock = MagicMock() + feed_service = FeedService(feeds_repo, extractor_mock) + + # Attach to app.state just like in main.py app.state.job_service = JobService( - scheduler=scheduler, + scheduler=Scheduler(), picker_service=picker_service, filter_service=filter_service, source_service=source_service, feed_service=feed_service, - extractor_service=extractor_service, - feeds_port=feeds_repository + extractor_service=extractor_mock, + feeds_port=feeds_repo ) - return @pytest.fixture(scope="session") @@ -85,25 +106,21 @@ def test_db_manager(): @pytest.fixture(name="db_session") def db_session_fixture(test_db_manager): engine = create_engine(test_db_manager) - testing_session_local = sessionmaker(autocommit=False, autoflush=False, bind=engine) - - connection = engine.connect() - transaction = connection.begin() - session = testing_session_local(bind=connection) - - # Dependency override + testing_session_local = sessionmaker(bind=engine) + with engine.begin() as conn: + conn.execute( + text( + "TRUNCATE TABLE filters, pickers, feeds, sources RESTART IDENTITY CASCADE;" + ) + ) + session = testing_session_local() def override_get_db(): - try: - yield session - finally: - session.close() - + with testing_session_local() as sess: + yield sess app.dependency_overrides[get_db] = override_get_db - yield session - - transaction.rollback() - connection.close() + session.close() + app.dependency_overrides.clear() @pytest.fixture(name="client") @@ -608,53 +625,48 @@ def test_create_picker_invalid_source_or_feed( assert response.status_code == 400 or response.status_code == 422 -def test_get_picker_successfully( - client: TestClient, - db_session: Session, - monkeypatch: pytest.MonkeyPatch -): +def test_get_picker_successfully(client, db_session, monkeypatch): # GIVEN - db_session.execute( - text("INSERT INTO sources (id, external_id, url, name) " - "VALUES (:id, :external_id, :url, :name)"), + res_source = db_session.execute( + text("INSERT INTO sources (external_id, url, name) " + "VALUES (:external_id, :url, :name) RETURNING id"), { - "id": 1, "external_id": str(uuid4()), "url": "https://example.com/source", "name": "picker_source" } ) - fake_token = "test-token" - monkeypatch.setattr("src.adapters.entrypoints.v1.routes.generated_token", fake_token) + source_id = res_source.fetchone()[0] feed_external_id = str(uuid4()) - db_session.execute( - text("INSERT INTO feeds (id, external_id, name) " - "VALUES (:id, :external_id, :name)"), - {"id": 1, "external_id": feed_external_id, "name": "feed_name"} + res_feed = db_session.execute( + text("INSERT INTO feeds (external_id, name) " + "VALUES (:external_id, :name) RETURNING id"), + {"external_id": feed_external_id, "name": "feed_name"} ) + feed_id = res_feed.fetchone()[0] picker_external_id = str(uuid4()) - db_session.execute( - text("INSERT INTO pickers (id, external_id, source_id, feed_id, cronjob, created_at) " - "VALUES (:id, :external_id, :source_id, :feed_id, :cronjob, NOW())"), - { - "id": 1, - "external_id": picker_external_id, - "source_id": 1, - "feed_id": 1, - "cronjob": "*/5 * * * *" - } + res_picker = db_session.execute( + text(""" + INSERT INTO pickers (external_id, source_id, feed_id, cronjob) + VALUES (:ext, :src, :feed, '*/5 * * * *') RETURNING id + """), + {"ext": picker_external_id, "src": source_id, "feed": feed_id} ) + picker_id = res_picker.fetchone()[0] db_session.execute( - text("INSERT INTO filters (id, picker_id, operation, args, created_at) " - "VALUES (:id, :picker_id, :operation, :args, NOW())"), - {"id": 1, "picker_id": 1, "operation": "identity", "args": "[a]"} + text("INSERT INTO filters (picker_id, operation, args) VALUES (:p_id, 'identity', '[a]')"), + {"p_id": picker_id} ) + db_session.commit() # WHEN + fake_token = "test-token" + monkeypatch.setattr("src.adapters.entrypoints.v1.routes.generated_token", fake_token) + response = client.get( f"/v1/pickers/{picker_external_id}", headers={"Authorization": f"Bearer {fake_token}"} @@ -665,12 +677,6 @@ def test_get_picker_successfully( data = response.json() assert data["external_id"] == picker_external_id assert data["source_url"] == "https://example.com/source" - assert data["feed_external_id"] == feed_external_id - assert data["cronjob"] == "*/5 * * * *" - assert isinstance(data["filters"], list) - assert len(data["filters"]) == 1 - assert data["filters"][0]["operation"] == "identity" - assert data["filters"][0]["args"] == "[a]" def test_get_picker_not_found( diff --git a/apps/api/tests/unit/adapters/repositories/test_feeds_repository.py b/apps/api/tests/unit/adapters/repositories/test_feeds_repository.py index b16e5ba..70e8989 100644 --- a/apps/api/tests/unit/adapters/repositories/test_feeds_repository.py +++ b/apps/api/tests/unit/adapters/repositories/test_feeds_repository.py @@ -77,8 +77,11 @@ def db_session(setup_test_db): @pytest.fixture -def repo(db_session): - return FeedsRepository(db_session) +def repo(setup_test_db): + engine = create_engine(setup_test_db) + testing_session_local = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + return FeedsRepository(testing_session_local) def test_create_feed_successfully(repo, db_session): diff --git a/apps/api/tests/unit/adapters/repositories/test_filters_repository.py b/apps/api/tests/unit/adapters/repositories/test_filters_repository.py index d79aa41..b1e9304 100644 --- a/apps/api/tests/unit/adapters/repositories/test_filters_repository.py +++ b/apps/api/tests/unit/adapters/repositories/test_filters_repository.py @@ -94,8 +94,11 @@ def db_session(setup_test_db): @pytest.fixture -def filters_repo(db_session): - return FiltersRepository(db_session) +def filters_repo(setup_test_db): + engine = create_engine(setup_test_db) + testing_session_local = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + return FiltersRepository(testing_session_local) @@ -242,7 +245,7 @@ def test_get_filter_by_picker_id_returns_none(db_session, filters_repo): assert results == [] -def test_delete_existing_filter(db_session): +def test_delete_existing_filter(db_session, filters_repo): # GIVEN db_session.execute( text( @@ -270,26 +273,22 @@ def test_delete_existing_filter(db_session): filter_id = db_session.execute(text("SELECT id FROM filters LIMIT 1")).scalar_one() - repo = FiltersRepository(db_session) - # WHEN - deleted = repo.delete_filter(filter_id) + deleted = filters_repo.delete_filter(filter_id) # THEN assert deleted is True - result = (db_session.execute( + db_session.expire_all() + result = db_session.execute( text("SELECT * FROM filters WHERE id = :id"), {"id": filter_id} - ).first()) + ).first() assert result is None -def test_delete_non_existing_filter(db_session): - # GIVEN - repo = FiltersRepository(db_session) - +def test_delete_non_existing_filter(db_session, filters_repo): # WHEN - deleted = repo.delete_filter(99999) # some ID that won’t exist + deleted = filters_repo.delete_filter(99999) # some ID that won’t exist # THEN assert deleted is False diff --git a/apps/api/tests/unit/adapters/repositories/test_pickers_repository.py b/apps/api/tests/unit/adapters/repositories/test_pickers_repository.py index 603773d..271771e 100644 --- a/apps/api/tests/unit/adapters/repositories/test_pickers_repository.py +++ b/apps/api/tests/unit/adapters/repositories/test_pickers_repository.py @@ -75,8 +75,11 @@ def db_session(setup_test_db): @pytest.fixture -def pickers_repo(db_session): - return PickersRepository(db_session) +def pickers_repo(setup_test_db): + engine = create_engine(setup_test_db) + testing_session_local = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + return PickersRepository(testing_session_local) @@ -172,7 +175,7 @@ def test_get_picker_by_external_id_returns_none(db_session, pickers_repo): assert results is None -def test_delete_picker_for_existing_picker(db_session): +def test_delete_picker_for_existing_picker(db_session, pickers_repo): # GIVEN db_session.execute( text( @@ -194,10 +197,8 @@ def test_delete_picker_for_existing_picker(db_session): picker_id = db_session.execute(text("SELECT id FROM pickers LIMIT 1")).scalar_one() - repo = PickersRepository(db_session) - # WHEN - deleted = repo.delete_picker(picker_id) + deleted = pickers_repo.delete_picker(picker_id) # THEN assert deleted is True @@ -208,11 +209,9 @@ def test_delete_picker_for_existing_picker(db_session): assert result is None -def test_delete_picker_for_non_existing_picker(db_session): - repo = PickersRepository(db_session) - +def test_delete_picker_for_non_existing_picker(db_session, pickers_repo): # WHEN - deleted = repo.delete_picker(99999) + deleted = pickers_repo.delete_picker(99999) # THEN assert deleted is False @@ -423,7 +422,7 @@ def test_get_all_pickers_that_returns_multiple(pickers_repo, db_session): assert {p.cronjob for p in pickers} == {"0 * * * *", "30 * * * *"} -def test_get_all_pickers_that_returns_empty_list(pickers_repo): +def test_get_all_pickers_that_returns_empty_list(db_session, pickers_repo): # WHEN pickers = pickers_repo.get_all_pickers() diff --git a/apps/api/tests/unit/adapters/repositories/test_sources_repository.py b/apps/api/tests/unit/adapters/repositories/test_sources_repository.py index a856aaf..b177265 100644 --- a/apps/api/tests/unit/adapters/repositories/test_sources_repository.py +++ b/apps/api/tests/unit/adapters/repositories/test_sources_repository.py @@ -56,8 +56,11 @@ def db_session(setup_test_db): @pytest.fixture -def repo(db_session): - return SourcesRepository(db_session) +def repo(setup_test_db): + engine = create_engine(setup_test_db) + testing_session_local = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + return SourcesRepository(testing_session_local) def test_get_sourcce_by_external_successfully(repo, db_session): diff --git a/apps/api/tests/unit/domain/handlers/test_job_processors.py b/apps/api/tests/unit/domain/handlers/test_job_processors.py index 2731aa2..b222316 100644 --- a/apps/api/tests/unit/domain/handlers/test_job_processors.py +++ b/apps/api/tests/unit/domain/handlers/test_job_processors.py @@ -1,16 +1,18 @@ -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch from src.domain.handlers.job_processors import process_filters -from src.domain.services.job_service import JobService -def test_process_filters_calls_job_service_with_int(): +@patch("src.main.app") +def test_process_filters_calls_job_service_with_int(mock_app): # GIVEN - mock_job_service = MagicMock(spec=JobService) + mock_job_service = MagicMock() + mock_app.state.job_service = mock_job_service + picker_id = "123" # WHEN - process_filters(picker_id, mock_job_service) + process_filters(picker_id) # THEN mock_job_service.process.assert_called_once_with(123)