From 2339471956b5a1c5b26d05ef2f920638f36215e7 Mon Sep 17 00:00:00 2001 From: Artem Tsyrulnikov Date: Mon, 15 Dec 2025 23:18:59 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=BF=D0=B0=D0=B3=D0=B8=D0=BD=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 1 + src/controller/http/analytics.py | 16 +++++---- src/controller/http/channels.py | 6 ++-- src/controller/http/creatives.py | 6 ++-- src/controller/http/placements.py | 6 ++-- src/controller/http/projects.py | 6 ++-- src/controller/http/purchase_plans.py | 6 ++-- src/controller/http/views.py | 6 ++-- src/controller/http/workspace_invites.py | 6 ++-- src/controller/http/workspace_members.py | 6 ++-- src/controller/http/workspaces.py | 6 ++-- src/main.py | 3 ++ .../analytics/get_channel_analytics.py | 6 ++-- .../analytics/get_creatives_analytics.py | 4 +-- .../analytics/get_placements_analytics.py | 6 ++-- src/usecase/channel/get_channels.py | 22 ++++++------ src/usecase/creative/get_creatives.py | 30 ++++++++-------- src/usecase/placement/get_placements.py | 4 +-- src/usecase/project/get_workspace_projects.py | 24 ++++++------- .../get_purchase_plan_channels.py | 34 +++++++++---------- src/usecase/views/get_views_history.py | 26 +++++++------- .../workspace/get_workspace_invites.py | 4 +-- .../workspace/get_workspace_members.py | 4 +-- src/usecase/workspace/get_workspaces.py | 18 +++++----- uv.lock | 16 +++++++++ 25 files changed, 151 insertions(+), 121 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3f6e781..3606d9e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ dependencies = [ "asyncpg>=0.30.0", "beautifulsoup4>=4.14.2", "fastapi>=0.121.0", + "fastapi-pagination>=0.15.3", "httpx>=0.28.1", "lxml>=6.0.2", "pydantic-settings>=2.11.0", diff --git a/src/controller/http/analytics.py b/src/controller/http/analytics.py index 8519039..81bb42c 100644 --- a/src/controller/http/analytics.py +++ b/src/controller/http/analytics.py @@ -4,6 +4,7 @@ from typing import Annotated from fastapi import Depends from fastapi.routing import APIRouter +from fastapi_pagination import Page, paginate from src import deps, dto from src.adapter.jwt import JWTPayload @@ -16,13 +17,14 @@ async def get_placements_analytics( workspace_id: uuid.UUID, current_user: Annotated[JWTPayload, Depends(deps.get_current_user)], project_id: uuid.UUID | None = None, -) -> dto.GetPlacementsAnalyticsOutput: +) -> Page[dto.PlacementAnalyticsOutput]: input = dto.GetPlacementsAnalyticsInput( user_id=current_user.user_id, workspace_id=workspace_id, project_id=project_id, ) - return await deps.get_usecase().get_placements_analytics(input) + result = await deps.get_usecase().get_placements_analytics(input) + return paginate(result) # type: ignore[no-any-return] @analytics_router.get('/creatives') @@ -30,13 +32,14 @@ async def get_creatives_analytics( workspace_id: uuid.UUID, current_user: Annotated[JWTPayload, Depends(deps.get_current_user)], project_id: uuid.UUID | None = None, -) -> dto.GetCreativesAnalyticsOutput: +) -> Page[dto.CreativeAnalyticsOutput]: input = dto.GetCreativesAnalyticsInput( user_id=current_user.user_id, workspace_id=workspace_id, project_id=project_id, ) - return await deps.get_usecase().get_creatives_analytics(input) + result = await deps.get_usecase().get_creatives_analytics(input) + return paginate(result) # type: ignore[no-any-return] @analytics_router.get('/channels') @@ -44,13 +47,14 @@ async def get_channel_analytics( workspace_id: uuid.UUID, current_user: Annotated[JWTPayload, Depends(deps.get_current_user)], project_id: uuid.UUID | None = None, -) -> dto.GetChannelAnalyticsOutput: +) -> Page[dto.ChannelAnalyticsOutput]: input = dto.GetChannelAnalyticsInput( user_id=current_user.user_id, workspace_id=workspace_id, project_id=project_id, ) - return await deps.get_usecase().get_channel_analytics(input) + result = await deps.get_usecase().get_channel_analytics(input) + return paginate(result) # type: ignore[no-any-return] @analytics_router.get('/spending') diff --git a/src/controller/http/channels.py b/src/controller/http/channels.py index 2d8d210..30e9429 100644 --- a/src/controller/http/channels.py +++ b/src/controller/http/channels.py @@ -2,6 +2,7 @@ from typing import Annotated from fastapi import Depends from fastapi.routing import APIRouter +from fastapi_pagination import Page, paginate from src import deps, dto from src.adapter.jwt import JWTPayload @@ -13,6 +14,7 @@ channels_router = APIRouter(prefix='/channels', tags=['channels']) async def get_channels( _: Annotated[JWTPayload, Depends(deps.get_current_user)], username: str | None = None, -) -> dto.GetChannelsOutput: +) -> Page[dto.ChannelOutput]: input = dto.GetChannelsInput(username=username) - return await deps.get_usecase().get_channels(input=input) + result = await deps.get_usecase().get_channels(input=input) + return paginate(result) # type: ignore[no-any-return] diff --git a/src/controller/http/creatives.py b/src/controller/http/creatives.py index 93b286a..a32c08d 100644 --- a/src/controller/http/creatives.py +++ b/src/controller/http/creatives.py @@ -3,6 +3,7 @@ from typing import Annotated from fastapi import Depends, Query from fastapi.routing import APIRouter +from fastapi_pagination import Page, paginate from src import deps, dto from src.adapter.jwt import JWTPayload @@ -16,7 +17,7 @@ async def list_creatives( current_user: Annotated[JWTPayload, Depends(deps.get_current_user)], project_id: uuid.UUID | None = None, include_archived: bool = False, -) -> dto.GetCreativesOutput: +) -> Page[dto.CreativeOutput]: input = dto.GetCreativesInput( user_id=current_user.user_id, workspace_id=workspace_id, @@ -24,7 +25,8 @@ async def list_creatives( include_archived=include_archived, ) - return await deps.get_usecase().get_creatives(input=input) + result = await deps.get_usecase().get_creatives(input=input) + return paginate(result) # type: ignore[no-any-return] @creatives_router.get('/{creative_id}') diff --git a/src/controller/http/placements.py b/src/controller/http/placements.py index a83872f..b014838 100644 --- a/src/controller/http/placements.py +++ b/src/controller/http/placements.py @@ -3,6 +3,7 @@ from typing import Annotated from fastapi import Depends from fastapi.routing import APIRouter +from fastapi_pagination import Page, paginate from src import deps, dto from src.adapter.jwt import JWTPayload @@ -18,7 +19,7 @@ async def list_placements( placement_channel_id: uuid.UUID | None = None, creative_id: uuid.UUID | None = None, include_archived: bool = False, -) -> dto.GetPlacementsOutput: +) -> Page[dto.PlacementOutput]: input = dto.GetPlacementsInput( user_id=current_user.user_id, workspace_id=workspace_id, @@ -28,7 +29,8 @@ async def list_placements( include_archived=include_archived, ) - return await deps.get_usecase().get_placements(input=input) + result = await deps.get_usecase().get_placements(input=input) + return paginate(result) # type: ignore[no-any-return] @placements_router.get('/{placement_id}') diff --git a/src/controller/http/projects.py b/src/controller/http/projects.py index 9015c40..0d9a777 100644 --- a/src/controller/http/projects.py +++ b/src/controller/http/projects.py @@ -3,6 +3,7 @@ from typing import Annotated from fastapi import Depends from fastapi.routing import APIRouter +from fastapi_pagination import Page, paginate from src import deps, dto from src.adapter.jwt import JWTPayload @@ -14,10 +15,11 @@ projects_router = APIRouter(prefix='/workspaces/{workspace_id}/projects', tags=[ async def get_workspace_projects( workspace_id: uuid.UUID, current_user: Annotated[JWTPayload, Depends(deps.get_current_user)], -) -> dto.GetWorkspaceProjectsOutput: +) -> Page[dto.ProjectOutput]: input = dto.GetWorkspaceProjectsInput( user_id=current_user.user_id, workspace_id=workspace_id, ) - return await deps.get_usecase().get_workspace_projects(input=input) + result = await deps.get_usecase().get_workspace_projects(input=input) + return paginate(result) # type: ignore[no-any-return] diff --git a/src/controller/http/purchase_plans.py b/src/controller/http/purchase_plans.py index f981281..84bb4ac 100644 --- a/src/controller/http/purchase_plans.py +++ b/src/controller/http/purchase_plans.py @@ -3,6 +3,7 @@ from typing import Annotated from fastapi import Depends from fastapi.routing import APIRouter +from fastapi_pagination import Page, paginate from src import deps, dto from src.adapter.jwt import JWTPayload @@ -18,13 +19,14 @@ async def get_purchase_plan_channels( workspace_id: uuid.UUID, project_id: uuid.UUID, current_user: Annotated[JWTPayload, Depends(deps.get_current_user)], -) -> dto.GetPurchasePlanChannelsOutput: +) -> Page[dto.PurchasePlanChannelOutput]: input = dto.GetPurchasePlanChannelsInput( user_id=current_user.user_id, workspace_id=workspace_id, project_id=project_id, ) - return await deps.get_usecase().get_purchase_plan_channels(input=input) + result = await deps.get_usecase().get_purchase_plan_channels(input=input) + return paginate(result) # type: ignore[no-any-return] @purchase_plan_channels_router.post('') diff --git a/src/controller/http/views.py b/src/controller/http/views.py index ba78df2..feaf478 100644 --- a/src/controller/http/views.py +++ b/src/controller/http/views.py @@ -4,6 +4,7 @@ from typing import Annotated from fastapi import Depends from fastapi.routing import APIRouter +from fastapi_pagination import Page, paginate from src import deps, dto from src.adapter.jwt import JWTPayload @@ -18,7 +19,7 @@ async def get_views_history( current_user: Annotated[JWTPayload, Depends(deps.get_current_user)], from_date: datetime.datetime | None = None, to_date: datetime.datetime | None = None, -) -> dto.GetViewsHistoryOutput: +) -> Page[dto.PostViewsHistoryOutput]: input_data = dto.GetViewsHistoryInput( placement_id=placement_id, user_id=current_user.user_id, @@ -27,4 +28,5 @@ async def get_views_history( to_date=to_date, ) - return await deps.get_usecase().get_views_history(input=input_data) + result = await deps.get_usecase().get_views_history(input=input_data) + return paginate(result) # type: ignore[no-any-return] diff --git a/src/controller/http/workspace_invites.py b/src/controller/http/workspace_invites.py index 7c48aee..687be3d 100644 --- a/src/controller/http/workspace_invites.py +++ b/src/controller/http/workspace_invites.py @@ -3,6 +3,7 @@ from typing import Annotated from fastapi import Depends from fastapi.routing import APIRouter +from fastapi_pagination import Page, paginate from src import deps, dto from src.adapter.jwt import JWTPayload @@ -14,11 +15,12 @@ workspace_invites_router = APIRouter(prefix='/workspaces/{workspace_id}/invites' async def list_workspace_invites( workspace_id: uuid.UUID, current_user: Annotated[JWTPayload, Depends(deps.get_current_user)], -) -> dto.GetWorkspaceInvitesOutput: - return await deps.get_usecase().get_workspace_invites( +) -> Page[dto.WorkspaceInviteOutput]: + result = await deps.get_usecase().get_workspace_invites( workspace_id=workspace_id, user_id=current_user.user_id, ) + return paginate(result) # type: ignore[no-any-return] @workspace_invites_router.post('') diff --git a/src/controller/http/workspace_members.py b/src/controller/http/workspace_members.py index 24dc293..9901ac1 100644 --- a/src/controller/http/workspace_members.py +++ b/src/controller/http/workspace_members.py @@ -3,6 +3,7 @@ from typing import Annotated from fastapi import Depends from fastapi.routing import APIRouter +from fastapi_pagination import Page, paginate from src import deps, dto from src.adapter.jwt import JWTPayload @@ -14,11 +15,12 @@ workspace_members_router = APIRouter(prefix='/workspaces/{workspace_id}/members' async def list_workspace_members( workspace_id: uuid.UUID, current_user: Annotated[JWTPayload, Depends(deps.get_current_user)], -) -> dto.GetWorkspaceMembersOutput: - return await deps.get_usecase().get_workspace_members( +) -> Page[dto.WorkspaceMemberOutput]: + result = await deps.get_usecase().get_workspace_members( workspace_id=workspace_id, user_id=current_user.user_id, ) + return paginate(result) # type: ignore[no-any-return] @workspace_members_router.put('/{workspace_user_id}/permissions') diff --git a/src/controller/http/workspaces.py b/src/controller/http/workspaces.py index c351cd9..babcb01 100644 --- a/src/controller/http/workspaces.py +++ b/src/controller/http/workspaces.py @@ -3,6 +3,7 @@ from typing import Annotated from fastapi import Depends from fastapi.routing import APIRouter +from fastapi_pagination import Page, paginate from src import deps, dto from src.adapter.jwt import JWTPayload @@ -13,8 +14,9 @@ workspaces_router = APIRouter(prefix='/workspaces', tags=['workspaces']) @workspaces_router.get('') async def list_workspaces( current_user: Annotated[JWTPayload, Depends(deps.get_current_user)], -) -> dto.GetWorkspacesOutput: - return await deps.get_usecase().get_workspaces(current_user.user_id) +) -> Page[dto.WorkspaceMembershipOutput]: + result = await deps.get_usecase().get_workspaces(current_user.user_id) + return paginate(result) # type: ignore[no-any-return] @workspaces_router.post('') diff --git a/src/main.py b/src/main.py index 1e6ce8f..df2f472 100644 --- a/src/main.py +++ b/src/main.py @@ -3,6 +3,7 @@ from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware +from fastapi_pagination import add_pagination from shared import logger from shared.worker_base import WorkerConfig @@ -68,4 +69,6 @@ app.add_middleware( allow_headers=['*'], ) +add_pagination(app) + app.include_router(api_router) diff --git a/src/usecase/analytics/get_channel_analytics.py b/src/usecase/analytics/get_channel_analytics.py index 2e40e1e..03707ea 100644 --- a/src/usecase/analytics/get_channel_analytics.py +++ b/src/usecase/analytics/get_channel_analytics.py @@ -11,7 +11,9 @@ if TYPE_CHECKING: log = logging.getLogger(__name__) -async def get_channel_analytics(self: 'Usecase', input: dto.GetChannelAnalyticsInput) -> dto.GetChannelAnalyticsOutput: +async def get_channel_analytics( + self: 'Usecase', input: dto.GetChannelAnalyticsInput +) -> list[dto.ChannelAnalyticsOutput]: context = await self.ensure_workspace_permission( input.workspace_id, input.user_id, domain.PermissionKey.ANALYTICS_READ ) @@ -105,4 +107,4 @@ async def get_channel_analytics(self: 'Usecase', input: dto.GetChannelAnalyticsI ) ) - return dto.GetChannelAnalyticsOutput(channels=result) + return result diff --git a/src/usecase/analytics/get_creatives_analytics.py b/src/usecase/analytics/get_creatives_analytics.py index 00d6bf9..d6506f9 100644 --- a/src/usecase/analytics/get_creatives_analytics.py +++ b/src/usecase/analytics/get_creatives_analytics.py @@ -13,7 +13,7 @@ log = logging.getLogger(__name__) async def get_creatives_analytics( self: 'Usecase', input: dto.GetCreativesAnalyticsInput -) -> dto.GetCreativesAnalyticsOutput: +) -> list[dto.CreativeAnalyticsOutput]: context = await self.ensure_workspace_permission( input.workspace_id, input.user_id, domain.PermissionKey.ANALYTICS_READ ) @@ -94,4 +94,4 @@ async def get_creatives_analytics( ) ) - return dto.GetCreativesAnalyticsOutput(creatives=result) + return result diff --git a/src/usecase/analytics/get_placements_analytics.py b/src/usecase/analytics/get_placements_analytics.py index 28e04a6..f462d1f 100644 --- a/src/usecase/analytics/get_placements_analytics.py +++ b/src/usecase/analytics/get_placements_analytics.py @@ -23,7 +23,7 @@ def _calculate_cpm(cost: float | None, views: int | None) -> float | None: async def get_placements_analytics( self: 'Usecase', input: dto.GetPlacementsAnalyticsInput -) -> dto.GetPlacementsAnalyticsOutput: +) -> list[dto.PlacementAnalyticsOutput]: context = await self.ensure_workspace_permission( input.workspace_id, input.user_id, domain.PermissionKey.ANALYTICS_READ ) @@ -48,7 +48,7 @@ async def get_placements_analytics( post_ids = [p.post.id for p in placements if p.post] views_map = await self.database.get_latest_views_data_batch(post_ids) if post_ids else {} - result = [ + return [ dto.PlacementAnalyticsOutput( id=p.id, project_id=p.project_id, @@ -66,5 +66,3 @@ async def get_placements_analytics( ) for p in placements ] - - return dto.GetPlacementsAnalyticsOutput(placements=result) diff --git a/src/usecase/channel/get_channels.py b/src/usecase/channel/get_channels.py index 649082a..ba97f67 100644 --- a/src/usecase/channel/get_channels.py +++ b/src/usecase/channel/get_channels.py @@ -9,19 +9,17 @@ if TYPE_CHECKING: log = logging.getLogger(__name__) -async def get_channels(self: 'Usecase', input: dto.GetChannelsInput) -> dto.GetChannelsOutput: +async def get_channels(self: 'Usecase', input: dto.GetChannelsInput) -> list[dto.ChannelOutput]: channels = await self.database.search_channels(username_query=input.username) log.debug('Found %s channels for username query: %s', len(channels), input.username) - return dto.GetChannelsOutput( - channels=[ - dto.ChannelOutput( - id=channel.id, - telegram_id=channel.telegram_id, - title=channel.title, - username=channel.username, - ) - for channel in channels - ] - ) + return [ + dto.ChannelOutput( + id=channel.id, + telegram_id=channel.telegram_id, + title=channel.title, + username=channel.username, + ) + for channel in channels + ] diff --git a/src/usecase/creative/get_creatives.py b/src/usecase/creative/get_creatives.py index db9cbf9..97e5ed4 100644 --- a/src/usecase/creative/get_creatives.py +++ b/src/usecase/creative/get_creatives.py @@ -6,7 +6,7 @@ if TYPE_CHECKING: from .. import Usecase -async def get_creatives(self: 'Usecase', input: dto.GetCreativesInput) -> dto.GetCreativesOutput: +async def get_creatives(self: 'Usecase', input: dto.GetCreativesInput) -> list[dto.CreativeOutput]: context = await self.ensure_workspace_permission( input.workspace_id, input.user_id, domain.PermissionKey.PROJECTS_READ ) @@ -24,18 +24,16 @@ async def get_creatives(self: 'Usecase', input: dto.GetCreativesInput) -> dto.Ge allowed_project_ids=allowed_project_ids, ) - return dto.GetCreativesOutput( - creatives=[ - dto.CreativeOutput( - id=creative.id, - name=creative.name, - text=creative.text, - project_id=creative.project_id, - project_channel_title=creative.project.channel.title, - created_at=creative.created_at, - status=creative.status, - placements_count=creative.placements_count, - ) - for creative in creatives - ] - ) + return [ + dto.CreativeOutput( + id=creative.id, + name=creative.name, + text=creative.text, + project_id=creative.project_id, + project_channel_title=creative.project.channel.title, + created_at=creative.created_at, + status=creative.status, + placements_count=creative.placements_count, + ) + for creative in creatives + ] diff --git a/src/usecase/placement/get_placements.py b/src/usecase/placement/get_placements.py index 793d1df..334cc89 100644 --- a/src/usecase/placement/get_placements.py +++ b/src/usecase/placement/get_placements.py @@ -8,7 +8,7 @@ if TYPE_CHECKING: from .. import Usecase -async def get_placements(self: 'Usecase', input: dto.GetPlacementsInput) -> dto.GetPlacementsOutput: +async def get_placements(self: 'Usecase', input: dto.GetPlacementsInput) -> list[dto.PlacementOutput]: context = await self.ensure_workspace_permission( input.workspace_id, input.user_id, domain.PermissionKey.PLACEMENTS_READ ) @@ -56,4 +56,4 @@ async def get_placements(self: 'Usecase', input: dto.GetPlacementsInput) -> dto. ) ) - return dto.GetPlacementsOutput(placements=placement_outputs) + return placement_outputs diff --git a/src/usecase/project/get_workspace_projects.py b/src/usecase/project/get_workspace_projects.py index 2800ab2..ad9083e 100644 --- a/src/usecase/project/get_workspace_projects.py +++ b/src/usecase/project/get_workspace_projects.py @@ -8,7 +8,7 @@ if TYPE_CHECKING: async def get_workspace_projects( self: 'Usecase', input: dto.GetWorkspaceProjectsInput -) -> dto.GetWorkspaceProjectsOutput: +) -> list[dto.ProjectOutput]: context = await self.ensure_workspace_permission( input.workspace_id, input.user_id, domain.PermissionKey.PROJECTS_READ ) @@ -17,15 +17,13 @@ async def get_workspace_projects( projects = await self.database.get_workspace_projects(input.workspace_id, allowed_project_ids=allowed_project_ids) - return dto.GetWorkspaceProjectsOutput( - projects=[ - dto.ProjectOutput( - id=project.id, - telegram_id=project.channel.telegram_id, - title=project.channel.title, - username=project.channel.username, - status=project.status, - ) - for project in projects - ] - ) + return [ + dto.ProjectOutput( + id=project.id, + telegram_id=project.channel.telegram_id, + title=project.channel.title, + username=project.channel.username, + status=project.status, + ) + for project in projects + ] diff --git a/src/usecase/purchase_plan/get_purchase_plan_channels.py b/src/usecase/purchase_plan/get_purchase_plan_channels.py index d44368c..4b7587b 100644 --- a/src/usecase/purchase_plan/get_purchase_plan_channels.py +++ b/src/usecase/purchase_plan/get_purchase_plan_channels.py @@ -11,7 +11,7 @@ log = logging.getLogger(__name__) async def get_purchase_plan_channels( self: 'Usecase', input: dto.GetPurchasePlanChannelsInput -) -> dto.GetPurchasePlanChannelsOutput: +) -> list[dto.PurchasePlanChannelOutput]: context = await self.ensure_workspace_permission( input.workspace_id, input.user_id, domain.PermissionKey.PLACEMENTS_READ ) @@ -27,20 +27,18 @@ async def get_purchase_plan_channels( plan_channels = await self.database.get_purchase_plan_channels(plan.id) log.debug('Fetched %s channels for purchase plan %s (project %s)', len(plan_channels), plan.id, project.id) - return dto.GetPurchasePlanChannelsOutput( - channels=[ - dto.PurchasePlanChannelOutput( - id=plan_channel.id, - status=plan_channel.status, - planned_cost=plan_channel.planned_cost, - comment=plan_channel.comment, - channel=dto.ChannelOutput( - id=plan_channel.channel.id, - telegram_id=plan_channel.channel.telegram_id, - title=plan_channel.channel.title, - username=plan_channel.channel.username, - ), - ) - for plan_channel in plan_channels - ] - ) + return [ + dto.PurchasePlanChannelOutput( + id=plan_channel.id, + status=plan_channel.status, + planned_cost=plan_channel.planned_cost, + comment=plan_channel.comment, + channel=dto.ChannelOutput( + id=plan_channel.channel.id, + telegram_id=plan_channel.channel.telegram_id, + title=plan_channel.channel.title, + username=plan_channel.channel.username, + ), + ) + for plan_channel in plan_channels + ] diff --git a/src/usecase/views/get_views_history.py b/src/usecase/views/get_views_history.py index a22c026..bb68172 100644 --- a/src/usecase/views/get_views_history.py +++ b/src/usecase/views/get_views_history.py @@ -9,7 +9,7 @@ if TYPE_CHECKING: log = logging.getLogger(__name__) -async def get_views_history(self: 'Usecase', input: dto.GetViewsHistoryInput) -> dto.GetViewsHistoryOutput: +async def get_views_history(self: 'Usecase', input: dto.GetViewsHistoryInput) -> list[dto.PostViewsHistoryOutput]: context = await self.ensure_workspace_permission( input.workspace_id, input.user_id, domain.PermissionKey.PLACEMENTS_READ ) @@ -22,7 +22,7 @@ async def get_views_history(self: 'Usecase', input: dto.GetViewsHistoryInput) -> context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_READ, placement.project_id) if not placement.post: - return dto.GetViewsHistoryOutput(histories=[]) + return [] histories = await self.database.get_views_history( placement.post.id, @@ -30,15 +30,13 @@ async def get_views_history(self: 'Usecase', input: dto.GetViewsHistoryInput) -> to_date=input.to_date, ) - return dto.GetViewsHistoryOutput( - histories=[ - dto.PostViewsHistoryOutput( - id=history.id, - post_id=history.post_id, - views_count=history.views_count, - fetched_at=history.fetched_at, - created_at=history.created_at, - ) - for history in histories - ] - ) + return [ + dto.PostViewsHistoryOutput( + id=history.id, + post_id=history.post_id, + views_count=history.views_count, + fetched_at=history.fetched_at, + created_at=history.created_at, + ) + for history in histories + ] diff --git a/src/usecase/workspace/get_workspace_invites.py b/src/usecase/workspace/get_workspace_invites.py index 6182ec9..4cc7120 100644 --- a/src/usecase/workspace/get_workspace_invites.py +++ b/src/usecase/workspace/get_workspace_invites.py @@ -12,9 +12,9 @@ if TYPE_CHECKING: async def get_workspace_invites( self: Usecase, workspace_id: uuid.UUID, user_id: uuid.UUID -) -> dto.GetWorkspaceInvitesOutput: +) -> list[dto.WorkspaceInviteOutput]: await self.ensure_workspace_permission(workspace_id, user_id, domain.PermissionKey.ADMIN_FULL) invites = await self.database.get_workspace_invites(workspace_id) - return dto.GetWorkspaceInvitesOutput(invites=[workspace_invite_to_dto(invite) for invite in invites]) + return [workspace_invite_to_dto(invite) for invite in invites] diff --git a/src/usecase/workspace/get_workspace_members.py b/src/usecase/workspace/get_workspace_members.py index 1c54639..5e817ee 100644 --- a/src/usecase/workspace/get_workspace_members.py +++ b/src/usecase/workspace/get_workspace_members.py @@ -12,9 +12,9 @@ if TYPE_CHECKING: async def get_workspace_members( self: Usecase, workspace_id: uuid.UUID, user_id: uuid.UUID -) -> dto.GetWorkspaceMembersOutput: +) -> list[dto.WorkspaceMemberOutput]: await self.ensure_workspace_permission(workspace_id, user_id, domain.PermissionKey.ADMIN_FULL) members = await self.database.get_workspace_members(workspace_id) - return dto.GetWorkspaceMembersOutput(members=[workspace_member_to_dto(member) for member in members]) + return [workspace_member_to_dto(member) for member in members] diff --git a/src/usecase/workspace/get_workspaces.py b/src/usecase/workspace/get_workspaces.py index 9e9625f..b7c7264 100644 --- a/src/usecase/workspace/get_workspaces.py +++ b/src/usecase/workspace/get_workspaces.py @@ -7,15 +7,13 @@ if TYPE_CHECKING: from .. import Usecase -async def get_workspaces(self: 'Usecase', user_id: uuid.UUID) -> dto.GetWorkspacesOutput: +async def get_workspaces(self: 'Usecase', user_id: uuid.UUID) -> list[dto.WorkspaceMembershipOutput]: memberships = await self.database.get_user_workspaces(user_id) - return dto.GetWorkspacesOutput( - workspaces=[ - dto.WorkspaceMembershipOutput( - id=membership.workspace_id, - name=membership.workspace.name, - ) - for membership in memberships - ] - ) + return [ + dto.WorkspaceMembershipOutput( + id=membership.workspace_id, + name=membership.workspace.name, + ) + for membership in memberships + ] diff --git a/uv.lock b/uv.lock index 0f3919d..bbdf030 100644 --- a/uv.lock +++ b/uv.lock @@ -268,6 +268,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dd/2c/42277afc1ba1a18f8358561eee40785d27becab8f80a1f945c0a3051c6eb/fastapi-0.121.0-py3-none-any.whl", hash = "sha256:8bdf1b15a55f4e4b0d6201033da9109ea15632cb76cf156e7b8b4019f2172106", size = 109183, upload-time = "2025-11-03T10:25:53.27Z" }, ] +[[package]] +name = "fastapi-pagination" +version = "0.15.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastapi" }, + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/be/e5edfb47d0253b5dc019ad0430cc26b34f9b29a21456abfb8ea0cff40782/fastapi_pagination-0.15.3.tar.gz", hash = "sha256:0667c3e31eb0c47f15e2d4d0a971490beed9b65a1079158b5ad0115488a370e2", size = 571922, upload-time = "2025-12-11T21:53:43.297Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/99/11a13d3b2ff6fb716fbe8a1da8f15da332472f13bea84377e1d070cf3a6c/fastapi_pagination-0.15.3-py3-none-any.whl", hash = "sha256:6c0e8b3265270bfa46a580f7a3a24559b0825917625b5c4ed3a1cd60a173552f", size = 56231, upload-time = "2025-12-11T21:53:44.257Z" }, +] + [[package]] name = "frozenlist" version = "1.8.0" @@ -1012,6 +1026,7 @@ dependencies = [ { name = "asyncpg" }, { name = "beautifulsoup4" }, { name = "fastapi" }, + { name = "fastapi-pagination" }, { name = "httpx" }, { name = "lxml" }, { name = "pydantic-settings" }, @@ -1042,6 +1057,7 @@ requires-dist = [ { name = "asyncpg", specifier = ">=0.30.0" }, { name = "beautifulsoup4", specifier = ">=4.14.2" }, { name = "fastapi", specifier = ">=0.121.0" }, + { name = "fastapi-pagination", specifier = ">=0.15.3" }, { name = "httpx", specifier = ">=0.28.1" }, { name = "lxml", specifier = ">=6.0.2" }, { name = "pydantic-settings", specifier = ">=2.11.0" },