feat: пагинация

This commit is contained in:
Artem Tsyrulnikov
2025-12-15 23:18:59 +03:00
parent 83a81f46f7
commit 2339471956
25 changed files with 151 additions and 121 deletions

View File

@@ -11,6 +11,7 @@ dependencies = [
"asyncpg>=0.30.0", "asyncpg>=0.30.0",
"beautifulsoup4>=4.14.2", "beautifulsoup4>=4.14.2",
"fastapi>=0.121.0", "fastapi>=0.121.0",
"fastapi-pagination>=0.15.3",
"httpx>=0.28.1", "httpx>=0.28.1",
"lxml>=6.0.2", "lxml>=6.0.2",
"pydantic-settings>=2.11.0", "pydantic-settings>=2.11.0",

View File

@@ -4,6 +4,7 @@ from typing import Annotated
from fastapi import Depends from fastapi import Depends
from fastapi.routing import APIRouter from fastapi.routing import APIRouter
from fastapi_pagination import Page, paginate
from src import deps, dto from src import deps, dto
from src.adapter.jwt import JWTPayload from src.adapter.jwt import JWTPayload
@@ -16,13 +17,14 @@ async def get_placements_analytics(
workspace_id: uuid.UUID, workspace_id: uuid.UUID,
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)], current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
project_id: uuid.UUID | None = None, project_id: uuid.UUID | None = None,
) -> dto.GetPlacementsAnalyticsOutput: ) -> Page[dto.PlacementAnalyticsOutput]:
input = dto.GetPlacementsAnalyticsInput( input = dto.GetPlacementsAnalyticsInput(
user_id=current_user.user_id, user_id=current_user.user_id,
workspace_id=workspace_id, workspace_id=workspace_id,
project_id=project_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') @analytics_router.get('/creatives')
@@ -30,13 +32,14 @@ async def get_creatives_analytics(
workspace_id: uuid.UUID, workspace_id: uuid.UUID,
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)], current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
project_id: uuid.UUID | None = None, project_id: uuid.UUID | None = None,
) -> dto.GetCreativesAnalyticsOutput: ) -> Page[dto.CreativeAnalyticsOutput]:
input = dto.GetCreativesAnalyticsInput( input = dto.GetCreativesAnalyticsInput(
user_id=current_user.user_id, user_id=current_user.user_id,
workspace_id=workspace_id, workspace_id=workspace_id,
project_id=project_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') @analytics_router.get('/channels')
@@ -44,13 +47,14 @@ async def get_channel_analytics(
workspace_id: uuid.UUID, workspace_id: uuid.UUID,
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)], current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
project_id: uuid.UUID | None = None, project_id: uuid.UUID | None = None,
) -> dto.GetChannelAnalyticsOutput: ) -> Page[dto.ChannelAnalyticsOutput]:
input = dto.GetChannelAnalyticsInput( input = dto.GetChannelAnalyticsInput(
user_id=current_user.user_id, user_id=current_user.user_id,
workspace_id=workspace_id, workspace_id=workspace_id,
project_id=project_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') @analytics_router.get('/spending')

View File

@@ -2,6 +2,7 @@ from typing import Annotated
from fastapi import Depends from fastapi import Depends
from fastapi.routing import APIRouter from fastapi.routing import APIRouter
from fastapi_pagination import Page, paginate
from src import deps, dto from src import deps, dto
from src.adapter.jwt import JWTPayload from src.adapter.jwt import JWTPayload
@@ -13,6 +14,7 @@ channels_router = APIRouter(prefix='/channels', tags=['channels'])
async def get_channels( async def get_channels(
_: Annotated[JWTPayload, Depends(deps.get_current_user)], _: Annotated[JWTPayload, Depends(deps.get_current_user)],
username: str | None = None, username: str | None = None,
) -> dto.GetChannelsOutput: ) -> Page[dto.ChannelOutput]:
input = dto.GetChannelsInput(username=username) 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]

View File

@@ -3,6 +3,7 @@ from typing import Annotated
from fastapi import Depends, Query from fastapi import Depends, Query
from fastapi.routing import APIRouter from fastapi.routing import APIRouter
from fastapi_pagination import Page, paginate
from src import deps, dto from src import deps, dto
from src.adapter.jwt import JWTPayload from src.adapter.jwt import JWTPayload
@@ -16,7 +17,7 @@ async def list_creatives(
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)], current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
project_id: uuid.UUID | None = None, project_id: uuid.UUID | None = None,
include_archived: bool = False, include_archived: bool = False,
) -> dto.GetCreativesOutput: ) -> Page[dto.CreativeOutput]:
input = dto.GetCreativesInput( input = dto.GetCreativesInput(
user_id=current_user.user_id, user_id=current_user.user_id,
workspace_id=workspace_id, workspace_id=workspace_id,
@@ -24,7 +25,8 @@ async def list_creatives(
include_archived=include_archived, 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}') @creatives_router.get('/{creative_id}')

View File

@@ -3,6 +3,7 @@ from typing import Annotated
from fastapi import Depends from fastapi import Depends
from fastapi.routing import APIRouter from fastapi.routing import APIRouter
from fastapi_pagination import Page, paginate
from src import deps, dto from src import deps, dto
from src.adapter.jwt import JWTPayload from src.adapter.jwt import JWTPayload
@@ -18,7 +19,7 @@ async def list_placements(
placement_channel_id: uuid.UUID | None = None, placement_channel_id: uuid.UUID | None = None,
creative_id: uuid.UUID | None = None, creative_id: uuid.UUID | None = None,
include_archived: bool = False, include_archived: bool = False,
) -> dto.GetPlacementsOutput: ) -> Page[dto.PlacementOutput]:
input = dto.GetPlacementsInput( input = dto.GetPlacementsInput(
user_id=current_user.user_id, user_id=current_user.user_id,
workspace_id=workspace_id, workspace_id=workspace_id,
@@ -28,7 +29,8 @@ async def list_placements(
include_archived=include_archived, 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}') @placements_router.get('/{placement_id}')

View File

@@ -3,6 +3,7 @@ from typing import Annotated
from fastapi import Depends from fastapi import Depends
from fastapi.routing import APIRouter from fastapi.routing import APIRouter
from fastapi_pagination import Page, paginate
from src import deps, dto from src import deps, dto
from src.adapter.jwt import JWTPayload from src.adapter.jwt import JWTPayload
@@ -14,10 +15,11 @@ projects_router = APIRouter(prefix='/workspaces/{workspace_id}/projects', tags=[
async def get_workspace_projects( async def get_workspace_projects(
workspace_id: uuid.UUID, workspace_id: uuid.UUID,
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)], current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
) -> dto.GetWorkspaceProjectsOutput: ) -> Page[dto.ProjectOutput]:
input = dto.GetWorkspaceProjectsInput( input = dto.GetWorkspaceProjectsInput(
user_id=current_user.user_id, user_id=current_user.user_id,
workspace_id=workspace_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]

View File

@@ -3,6 +3,7 @@ from typing import Annotated
from fastapi import Depends from fastapi import Depends
from fastapi.routing import APIRouter from fastapi.routing import APIRouter
from fastapi_pagination import Page, paginate
from src import deps, dto from src import deps, dto
from src.adapter.jwt import JWTPayload from src.adapter.jwt import JWTPayload
@@ -18,13 +19,14 @@ async def get_purchase_plan_channels(
workspace_id: uuid.UUID, workspace_id: uuid.UUID,
project_id: uuid.UUID, project_id: uuid.UUID,
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)], current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
) -> dto.GetPurchasePlanChannelsOutput: ) -> Page[dto.PurchasePlanChannelOutput]:
input = dto.GetPurchasePlanChannelsInput( input = dto.GetPurchasePlanChannelsInput(
user_id=current_user.user_id, user_id=current_user.user_id,
workspace_id=workspace_id, workspace_id=workspace_id,
project_id=project_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('') @purchase_plan_channels_router.post('')

View File

@@ -4,6 +4,7 @@ from typing import Annotated
from fastapi import Depends from fastapi import Depends
from fastapi.routing import APIRouter from fastapi.routing import APIRouter
from fastapi_pagination import Page, paginate
from src import deps, dto from src import deps, dto
from src.adapter.jwt import JWTPayload from src.adapter.jwt import JWTPayload
@@ -18,7 +19,7 @@ async def get_views_history(
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)], current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
from_date: datetime.datetime | None = None, from_date: datetime.datetime | None = None,
to_date: datetime.datetime | None = None, to_date: datetime.datetime | None = None,
) -> dto.GetViewsHistoryOutput: ) -> Page[dto.PostViewsHistoryOutput]:
input_data = dto.GetViewsHistoryInput( input_data = dto.GetViewsHistoryInput(
placement_id=placement_id, placement_id=placement_id,
user_id=current_user.user_id, user_id=current_user.user_id,
@@ -27,4 +28,5 @@ async def get_views_history(
to_date=to_date, 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]

View File

@@ -3,6 +3,7 @@ from typing import Annotated
from fastapi import Depends from fastapi import Depends
from fastapi.routing import APIRouter from fastapi.routing import APIRouter
from fastapi_pagination import Page, paginate
from src import deps, dto from src import deps, dto
from src.adapter.jwt import JWTPayload from src.adapter.jwt import JWTPayload
@@ -14,11 +15,12 @@ workspace_invites_router = APIRouter(prefix='/workspaces/{workspace_id}/invites'
async def list_workspace_invites( async def list_workspace_invites(
workspace_id: uuid.UUID, workspace_id: uuid.UUID,
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)], current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
) -> dto.GetWorkspaceInvitesOutput: ) -> Page[dto.WorkspaceInviteOutput]:
return await deps.get_usecase().get_workspace_invites( result = await deps.get_usecase().get_workspace_invites(
workspace_id=workspace_id, workspace_id=workspace_id,
user_id=current_user.user_id, user_id=current_user.user_id,
) )
return paginate(result) # type: ignore[no-any-return]
@workspace_invites_router.post('') @workspace_invites_router.post('')

View File

@@ -3,6 +3,7 @@ from typing import Annotated
from fastapi import Depends from fastapi import Depends
from fastapi.routing import APIRouter from fastapi.routing import APIRouter
from fastapi_pagination import Page, paginate
from src import deps, dto from src import deps, dto
from src.adapter.jwt import JWTPayload from src.adapter.jwt import JWTPayload
@@ -14,11 +15,12 @@ workspace_members_router = APIRouter(prefix='/workspaces/{workspace_id}/members'
async def list_workspace_members( async def list_workspace_members(
workspace_id: uuid.UUID, workspace_id: uuid.UUID,
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)], current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
) -> dto.GetWorkspaceMembersOutput: ) -> Page[dto.WorkspaceMemberOutput]:
return await deps.get_usecase().get_workspace_members( result = await deps.get_usecase().get_workspace_members(
workspace_id=workspace_id, workspace_id=workspace_id,
user_id=current_user.user_id, user_id=current_user.user_id,
) )
return paginate(result) # type: ignore[no-any-return]
@workspace_members_router.put('/{workspace_user_id}/permissions') @workspace_members_router.put('/{workspace_user_id}/permissions')

View File

@@ -3,6 +3,7 @@ from typing import Annotated
from fastapi import Depends from fastapi import Depends
from fastapi.routing import APIRouter from fastapi.routing import APIRouter
from fastapi_pagination import Page, paginate
from src import deps, dto from src import deps, dto
from src.adapter.jwt import JWTPayload from src.adapter.jwt import JWTPayload
@@ -13,8 +14,9 @@ workspaces_router = APIRouter(prefix='/workspaces', tags=['workspaces'])
@workspaces_router.get('') @workspaces_router.get('')
async def list_workspaces( async def list_workspaces(
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)], current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
) -> dto.GetWorkspacesOutput: ) -> Page[dto.WorkspaceMembershipOutput]:
return await deps.get_usecase().get_workspaces(current_user.user_id) result = await deps.get_usecase().get_workspaces(current_user.user_id)
return paginate(result) # type: ignore[no-any-return]
@workspaces_router.post('') @workspaces_router.post('')

View File

@@ -3,6 +3,7 @@ from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi_pagination import add_pagination
from shared import logger from shared import logger
from shared.worker_base import WorkerConfig from shared.worker_base import WorkerConfig
@@ -68,4 +69,6 @@ app.add_middleware(
allow_headers=['*'], allow_headers=['*'],
) )
add_pagination(app)
app.include_router(api_router) app.include_router(api_router)

View File

@@ -11,7 +11,9 @@ if TYPE_CHECKING:
log = logging.getLogger(__name__) 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( context = await self.ensure_workspace_permission(
input.workspace_id, input.user_id, domain.PermissionKey.ANALYTICS_READ 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

View File

@@ -13,7 +13,7 @@ log = logging.getLogger(__name__)
async def get_creatives_analytics( async def get_creatives_analytics(
self: 'Usecase', input: dto.GetCreativesAnalyticsInput self: 'Usecase', input: dto.GetCreativesAnalyticsInput
) -> dto.GetCreativesAnalyticsOutput: ) -> list[dto.CreativeAnalyticsOutput]:
context = await self.ensure_workspace_permission( context = await self.ensure_workspace_permission(
input.workspace_id, input.user_id, domain.PermissionKey.ANALYTICS_READ 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

View File

@@ -23,7 +23,7 @@ def _calculate_cpm(cost: float | None, views: int | None) -> float | None:
async def get_placements_analytics( async def get_placements_analytics(
self: 'Usecase', input: dto.GetPlacementsAnalyticsInput self: 'Usecase', input: dto.GetPlacementsAnalyticsInput
) -> dto.GetPlacementsAnalyticsOutput: ) -> list[dto.PlacementAnalyticsOutput]:
context = await self.ensure_workspace_permission( context = await self.ensure_workspace_permission(
input.workspace_id, input.user_id, domain.PermissionKey.ANALYTICS_READ 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] 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 {} views_map = await self.database.get_latest_views_data_batch(post_ids) if post_ids else {}
result = [ return [
dto.PlacementAnalyticsOutput( dto.PlacementAnalyticsOutput(
id=p.id, id=p.id,
project_id=p.project_id, project_id=p.project_id,
@@ -66,5 +66,3 @@ async def get_placements_analytics(
) )
for p in placements for p in placements
] ]
return dto.GetPlacementsAnalyticsOutput(placements=result)

View File

@@ -9,19 +9,17 @@ if TYPE_CHECKING:
log = logging.getLogger(__name__) 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) channels = await self.database.search_channels(username_query=input.username)
log.debug('Found %s channels for username query: %s', len(channels), input.username) log.debug('Found %s channels for username query: %s', len(channels), input.username)
return dto.GetChannelsOutput( return [
channels=[ dto.ChannelOutput(
dto.ChannelOutput( id=channel.id,
id=channel.id, telegram_id=channel.telegram_id,
telegram_id=channel.telegram_id, title=channel.title,
title=channel.title, username=channel.username,
username=channel.username, )
) for channel in channels
for channel in channels ]
]
)

View File

@@ -6,7 +6,7 @@ if TYPE_CHECKING:
from .. import Usecase 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( context = await self.ensure_workspace_permission(
input.workspace_id, input.user_id, domain.PermissionKey.PROJECTS_READ 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, allowed_project_ids=allowed_project_ids,
) )
return dto.GetCreativesOutput( return [
creatives=[ dto.CreativeOutput(
dto.CreativeOutput( id=creative.id,
id=creative.id, name=creative.name,
name=creative.name, text=creative.text,
text=creative.text, project_id=creative.project_id,
project_id=creative.project_id, project_channel_title=creative.project.channel.title,
project_channel_title=creative.project.channel.title, created_at=creative.created_at,
created_at=creative.created_at, status=creative.status,
status=creative.status, placements_count=creative.placements_count,
placements_count=creative.placements_count, )
) for creative in creatives
for creative in creatives ]
]
)

View File

@@ -8,7 +8,7 @@ if TYPE_CHECKING:
from .. import Usecase 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( context = await self.ensure_workspace_permission(
input.workspace_id, input.user_id, domain.PermissionKey.PLACEMENTS_READ 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

View File

@@ -8,7 +8,7 @@ if TYPE_CHECKING:
async def get_workspace_projects( async def get_workspace_projects(
self: 'Usecase', input: dto.GetWorkspaceProjectsInput self: 'Usecase', input: dto.GetWorkspaceProjectsInput
) -> dto.GetWorkspaceProjectsOutput: ) -> list[dto.ProjectOutput]:
context = await self.ensure_workspace_permission( context = await self.ensure_workspace_permission(
input.workspace_id, input.user_id, domain.PermissionKey.PROJECTS_READ 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) projects = await self.database.get_workspace_projects(input.workspace_id, allowed_project_ids=allowed_project_ids)
return dto.GetWorkspaceProjectsOutput( return [
projects=[ dto.ProjectOutput(
dto.ProjectOutput( id=project.id,
id=project.id, telegram_id=project.channel.telegram_id,
telegram_id=project.channel.telegram_id, title=project.channel.title,
title=project.channel.title, username=project.channel.username,
username=project.channel.username, status=project.status,
status=project.status, )
) for project in projects
for project in projects ]
]
)

View File

@@ -11,7 +11,7 @@ log = logging.getLogger(__name__)
async def get_purchase_plan_channels( async def get_purchase_plan_channels(
self: 'Usecase', input: dto.GetPurchasePlanChannelsInput self: 'Usecase', input: dto.GetPurchasePlanChannelsInput
) -> dto.GetPurchasePlanChannelsOutput: ) -> list[dto.PurchasePlanChannelOutput]:
context = await self.ensure_workspace_permission( context = await self.ensure_workspace_permission(
input.workspace_id, input.user_id, domain.PermissionKey.PLACEMENTS_READ 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) 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) log.debug('Fetched %s channels for purchase plan %s (project %s)', len(plan_channels), plan.id, project.id)
return dto.GetPurchasePlanChannelsOutput( return [
channels=[ dto.PurchasePlanChannelOutput(
dto.PurchasePlanChannelOutput( id=plan_channel.id,
id=plan_channel.id, status=plan_channel.status,
status=plan_channel.status, planned_cost=plan_channel.planned_cost,
planned_cost=plan_channel.planned_cost, comment=plan_channel.comment,
comment=plan_channel.comment, channel=dto.ChannelOutput(
channel=dto.ChannelOutput( id=plan_channel.channel.id,
id=plan_channel.channel.id, telegram_id=plan_channel.channel.telegram_id,
telegram_id=plan_channel.channel.telegram_id, title=plan_channel.channel.title,
title=plan_channel.channel.title, username=plan_channel.channel.username,
username=plan_channel.channel.username, ),
), )
) for plan_channel in plan_channels
for plan_channel in plan_channels ]
]
)

View File

@@ -9,7 +9,7 @@ if TYPE_CHECKING:
log = logging.getLogger(__name__) 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( context = await self.ensure_workspace_permission(
input.workspace_id, input.user_id, domain.PermissionKey.PLACEMENTS_READ 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) context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_READ, placement.project_id)
if not placement.post: if not placement.post:
return dto.GetViewsHistoryOutput(histories=[]) return []
histories = await self.database.get_views_history( histories = await self.database.get_views_history(
placement.post.id, placement.post.id,
@@ -30,15 +30,13 @@ async def get_views_history(self: 'Usecase', input: dto.GetViewsHistoryInput) ->
to_date=input.to_date, to_date=input.to_date,
) )
return dto.GetViewsHistoryOutput( return [
histories=[ dto.PostViewsHistoryOutput(
dto.PostViewsHistoryOutput( id=history.id,
id=history.id, post_id=history.post_id,
post_id=history.post_id, views_count=history.views_count,
views_count=history.views_count, fetched_at=history.fetched_at,
fetched_at=history.fetched_at, created_at=history.created_at,
created_at=history.created_at, )
) for history in histories
for history in histories ]
]
)

View File

@@ -12,9 +12,9 @@ if TYPE_CHECKING:
async def get_workspace_invites( async def get_workspace_invites(
self: Usecase, workspace_id: uuid.UUID, user_id: uuid.UUID 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) await self.ensure_workspace_permission(workspace_id, user_id, domain.PermissionKey.ADMIN_FULL)
invites = await self.database.get_workspace_invites(workspace_id) 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]

View File

@@ -12,9 +12,9 @@ if TYPE_CHECKING:
async def get_workspace_members( async def get_workspace_members(
self: Usecase, workspace_id: uuid.UUID, user_id: uuid.UUID 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) await self.ensure_workspace_permission(workspace_id, user_id, domain.PermissionKey.ADMIN_FULL)
members = await self.database.get_workspace_members(workspace_id) 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]

View File

@@ -7,15 +7,13 @@ if TYPE_CHECKING:
from .. import Usecase 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) memberships = await self.database.get_user_workspaces(user_id)
return dto.GetWorkspacesOutput( return [
workspaces=[ dto.WorkspaceMembershipOutput(
dto.WorkspaceMembershipOutput( id=membership.workspace_id,
id=membership.workspace_id, name=membership.workspace.name,
name=membership.workspace.name, )
) for membership in memberships
for membership in memberships ]
]
)

16
uv.lock generated
View File

@@ -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" }, { 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]] [[package]]
name = "frozenlist" name = "frozenlist"
version = "1.8.0" version = "1.8.0"
@@ -1012,6 +1026,7 @@ dependencies = [
{ name = "asyncpg" }, { name = "asyncpg" },
{ name = "beautifulsoup4" }, { name = "beautifulsoup4" },
{ name = "fastapi" }, { name = "fastapi" },
{ name = "fastapi-pagination" },
{ name = "httpx" }, { name = "httpx" },
{ name = "lxml" }, { name = "lxml" },
{ name = "pydantic-settings" }, { name = "pydantic-settings" },
@@ -1042,6 +1057,7 @@ requires-dist = [
{ name = "asyncpg", specifier = ">=0.30.0" }, { name = "asyncpg", specifier = ">=0.30.0" },
{ name = "beautifulsoup4", specifier = ">=4.14.2" }, { name = "beautifulsoup4", specifier = ">=4.14.2" },
{ name = "fastapi", specifier = ">=0.121.0" }, { name = "fastapi", specifier = ">=0.121.0" },
{ name = "fastapi-pagination", specifier = ">=0.15.3" },
{ name = "httpx", specifier = ">=0.28.1" }, { name = "httpx", specifier = ">=0.28.1" },
{ name = "lxml", specifier = ">=6.0.2" }, { name = "lxml", specifier = ">=6.0.2" },
{ name = "pydantic-settings", specifier = ">=2.11.0" }, { name = "pydantic-settings", specifier = ">=2.11.0" },