refactor: replace DefaultLimitOffsetPage with LimitOffsetPage in multiple files and update timezone handling to use UTC
This commit is contained in:
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import json
|
||||
from collections import deque
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from uuid import UUID
|
||||
|
||||
@@ -36,6 +36,7 @@ from app.services.organizations import (
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import AsyncIterator, Sequence
|
||||
|
||||
from fastapi_pagination.limit_offset import LimitOffsetPage
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
router = APIRouter(prefix="/activity", tags=["activity"])
|
||||
@@ -63,7 +64,7 @@ def _parse_since(value: str | None) -> datetime | None:
|
||||
except ValueError:
|
||||
return None
|
||||
if parsed.tzinfo is not None:
|
||||
return parsed.astimezone(timezone.utc).replace(tzinfo=None)
|
||||
return parsed.astimezone(UTC).replace(tzinfo=None)
|
||||
return parsed
|
||||
|
||||
|
||||
@@ -145,7 +146,7 @@ async def _fetch_task_comment_events(
|
||||
async def list_activity(
|
||||
session: AsyncSession = SESSION_DEP,
|
||||
actor: ActorContext = ACTOR_DEP,
|
||||
) -> DefaultLimitOffsetPage[ActivityEventRead]:
|
||||
) -> LimitOffsetPage[ActivityEventRead]:
|
||||
"""List activity events visible to the calling actor."""
|
||||
statement = select(ActivityEvent)
|
||||
if actor.actor_type == "agent" and actor.agent:
|
||||
@@ -174,7 +175,7 @@ async def list_task_comment_feed(
|
||||
board_id: UUID | None = BOARD_ID_QUERY,
|
||||
session: AsyncSession = SESSION_DEP,
|
||||
ctx: OrganizationContext = ORG_MEMBER_DEP,
|
||||
) -> DefaultLimitOffsetPage[ActivityTaskCommentFeedItemRead]:
|
||||
) -> LimitOffsetPage[ActivityTaskCommentFeedItemRead]:
|
||||
"""List task-comment feed items for accessible boards."""
|
||||
statement = (
|
||||
select(ActivityEvent, Task, Board, Agent)
|
||||
|
||||
@@ -76,6 +76,7 @@ if TYPE_CHECKING:
|
||||
from collections.abc import Sequence
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi_pagination.limit_offset import LimitOffsetPage
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from app.models.activity_events import ActivityEvent
|
||||
@@ -222,7 +223,7 @@ async def _require_gateway_board(
|
||||
async def list_boards(
|
||||
session: AsyncSession = SESSION_DEP,
|
||||
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
||||
) -> DefaultLimitOffsetPage[BoardRead]:
|
||||
) -> LimitOffsetPage[BoardRead]:
|
||||
"""List boards visible to the authenticated agent."""
|
||||
statement = select(Board)
|
||||
if agent_ctx.agent.board_id:
|
||||
@@ -246,7 +247,7 @@ async def list_agents(
|
||||
board_id: UUID | None = BOARD_ID_QUERY,
|
||||
session: AsyncSession = SESSION_DEP,
|
||||
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
||||
) -> DefaultLimitOffsetPage[AgentRead]:
|
||||
) -> LimitOffsetPage[AgentRead]:
|
||||
"""List agents, optionally filtered to a board."""
|
||||
statement = select(Agent)
|
||||
if agent_ctx.agent.board_id:
|
||||
@@ -277,7 +278,7 @@ async def list_tasks(
|
||||
board: Board = BOARD_DEP,
|
||||
session: AsyncSession = SESSION_DEP,
|
||||
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
||||
) -> DefaultLimitOffsetPage[TaskRead]:
|
||||
) -> LimitOffsetPage[TaskRead]:
|
||||
"""List tasks on a board with optional status and assignment filters."""
|
||||
_guard_board_access(agent_ctx, board)
|
||||
return await tasks_api.list_tasks(
|
||||
@@ -414,7 +415,7 @@ async def list_task_comments(
|
||||
task: Task = TASK_DEP,
|
||||
session: AsyncSession = SESSION_DEP,
|
||||
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
||||
) -> DefaultLimitOffsetPage[TaskCommentRead]:
|
||||
) -> LimitOffsetPage[TaskCommentRead]:
|
||||
"""List comments for a task visible to the authenticated agent."""
|
||||
if (
|
||||
agent_ctx.agent.board_id
|
||||
@@ -460,7 +461,7 @@ async def list_board_memory(
|
||||
board: Board = BOARD_DEP,
|
||||
session: AsyncSession = SESSION_DEP,
|
||||
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
||||
) -> DefaultLimitOffsetPage[BoardMemoryRead]:
|
||||
) -> LimitOffsetPage[BoardMemoryRead]:
|
||||
"""List board memory entries with optional chat filtering."""
|
||||
_guard_board_access(agent_ctx, board)
|
||||
return await board_memory_api.list_board_memory(
|
||||
@@ -497,7 +498,7 @@ async def list_approvals(
|
||||
board: Board = BOARD_DEP,
|
||||
session: AsyncSession = SESSION_DEP,
|
||||
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
||||
) -> DefaultLimitOffsetPage[ApprovalRead]:
|
||||
) -> LimitOffsetPage[ApprovalRead]:
|
||||
"""List approvals for a board."""
|
||||
_guard_board_access(agent_ctx, board)
|
||||
return await approvals_api.list_approvals(
|
||||
@@ -960,12 +961,12 @@ async def broadcast_gateway_lead_message(
|
||||
sent = 0
|
||||
failed = 0
|
||||
|
||||
async def _send_to_board(board: Board) -> GatewayLeadBroadcastBoardResult:
|
||||
async def _send_to_board(target_board: Board) -> GatewayLeadBroadcastBoardResult:
|
||||
try:
|
||||
lead, _lead_created = await ensure_board_lead_agent(
|
||||
session,
|
||||
request=LeadAgentRequest(
|
||||
board=board,
|
||||
board=target_board,
|
||||
gateway=gateway,
|
||||
config=config,
|
||||
user=None,
|
||||
@@ -975,14 +976,14 @@ async def broadcast_gateway_lead_message(
|
||||
lead_session_key = _require_lead_session_key(lead)
|
||||
message = (
|
||||
f"{header}\n"
|
||||
f"Board: {board.name}\n"
|
||||
f"Board ID: {board.id}\n"
|
||||
f"Board: {target_board.name}\n"
|
||||
f"Board ID: {target_board.id}\n"
|
||||
f"From agent: {agent_ctx.agent.name}\n"
|
||||
f"{correlation_line}\n"
|
||||
f"{payload.content.strip()}\n\n"
|
||||
"Reply to the gateway main by writing a NON-chat memory item "
|
||||
"on this board:\n"
|
||||
f"POST {base_url}/api/v1/agent/boards/{board.id}/memory\n"
|
||||
f"POST {base_url}/api/v1/agent/boards/{target_board.id}/memory\n"
|
||||
f'Body: {{"content":"...","tags":{tags_json},'
|
||||
f'"source":"{reply_source}"}}\n'
|
||||
"Do NOT reply in OpenClaw chat."
|
||||
@@ -990,14 +991,14 @@ async def broadcast_gateway_lead_message(
|
||||
await ensure_session(lead_session_key, config=config, label=lead.name)
|
||||
await send_message(message, session_key=lead_session_key, config=config)
|
||||
return GatewayLeadBroadcastBoardResult(
|
||||
board_id=board.id,
|
||||
board_id=target_board.id,
|
||||
lead_agent_id=lead.id,
|
||||
lead_agent_name=lead.name,
|
||||
ok=True,
|
||||
)
|
||||
except (HTTPException, OpenClawGatewayError, ValueError) as exc:
|
||||
return GatewayLeadBroadcastBoardResult(
|
||||
board_id=board.id,
|
||||
board_id=target_board.id,
|
||||
ok=False,
|
||||
error=str(exc),
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ import asyncio
|
||||
import json
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
@@ -65,6 +65,7 @@ from app.services.organizations import (
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import AsyncIterator, Sequence
|
||||
|
||||
from fastapi_pagination.limit_offset import LimitOffsetPage
|
||||
from sqlalchemy.sql.elements import ColumnElement
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
from sqlmodel.sql.expression import SelectOfScalar
|
||||
@@ -115,7 +116,7 @@ def _parse_since(value: str | None) -> datetime | None:
|
||||
except ValueError:
|
||||
return None
|
||||
if parsed.tzinfo is not None:
|
||||
return parsed.astimezone(timezone.utc).replace(tzinfo=None)
|
||||
return parsed.astimezone(UTC).replace(tzinfo=None)
|
||||
return parsed
|
||||
|
||||
|
||||
@@ -564,7 +565,7 @@ async def _validate_agent_update_inputs(
|
||||
updates: dict[str, Any],
|
||||
make_main: bool | None,
|
||||
) -> None:
|
||||
if make_main is True and not is_org_admin(ctx.member):
|
||||
if make_main and not is_org_admin(ctx.member):
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||
if "status" in updates:
|
||||
raise HTTPException(
|
||||
@@ -597,7 +598,7 @@ async def _apply_agent_update_mutations(
|
||||
)
|
||||
gateway_for_main: Gateway | None = None
|
||||
|
||||
if make_main is True:
|
||||
if make_main:
|
||||
board_source = updates.get("board_id") or agent.board_id
|
||||
board_for_main = await _require_board(session, board_source)
|
||||
gateway_for_main, _ = await _require_gateway(session, board_for_main)
|
||||
@@ -605,10 +606,10 @@ async def _apply_agent_update_mutations(
|
||||
agent.is_board_lead = False
|
||||
agent.openclaw_session_id = gateway_for_main.main_session_key
|
||||
main_gateway = gateway_for_main
|
||||
elif make_main is False:
|
||||
elif make_main is not None:
|
||||
agent.openclaw_session_id = None
|
||||
|
||||
if make_main is not True and "board_id" in updates:
|
||||
if not make_main and "board_id" in updates:
|
||||
await _require_board(session, updates["board_id"])
|
||||
for key, value in updates.items():
|
||||
setattr(agent, key, value)
|
||||
@@ -633,7 +634,7 @@ async def _resolve_agent_update_target(
|
||||
main_gateway: Gateway | None,
|
||||
gateway_for_main: Gateway | None,
|
||||
) -> _AgentUpdateProvisionTarget:
|
||||
if make_main is True:
|
||||
if make_main:
|
||||
if gateway_for_main is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
@@ -955,7 +956,7 @@ async def list_agents(
|
||||
gateway_id: UUID | None = GATEWAY_ID_QUERY,
|
||||
session: AsyncSession = SESSION_DEP,
|
||||
ctx: OrganizationContext = ORG_ADMIN_DEP,
|
||||
) -> DefaultLimitOffsetPage[AgentRead]:
|
||||
) -> LimitOffsetPage[AgentRead]:
|
||||
"""List agents visible to the active organization admin."""
|
||||
main_session_keys = await _get_gateway_main_session_keys(session)
|
||||
board_ids = await list_accessible_board_ids(session, member=ctx.member, write=False)
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
from typing import TYPE_CHECKING
|
||||
from uuid import UUID
|
||||
|
||||
@@ -35,6 +35,7 @@ from app.schemas.pagination import DefaultLimitOffsetPage
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import AsyncIterator
|
||||
|
||||
from fastapi_pagination.limit_offset import LimitOffsetPage
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from app.models.boards import Board
|
||||
@@ -79,7 +80,7 @@ def _parse_since(value: str | None) -> datetime | None:
|
||||
except ValueError:
|
||||
return None
|
||||
if parsed.tzinfo is not None:
|
||||
return parsed.astimezone(timezone.utc).replace(tzinfo=None)
|
||||
return parsed.astimezone(UTC).replace(tzinfo=None)
|
||||
return parsed
|
||||
|
||||
|
||||
@@ -118,7 +119,7 @@ async def list_approvals(
|
||||
board: Board = BOARD_READ_DEP,
|
||||
session: AsyncSession = SESSION_DEP,
|
||||
_actor: ActorContext = ACTOR_DEP,
|
||||
) -> DefaultLimitOffsetPage[ApprovalRead]:
|
||||
) -> LimitOffsetPage[ApprovalRead]:
|
||||
"""List approvals for a board, optionally filtering by status."""
|
||||
statement = Approval.objects.filter_by(board_id=board.id)
|
||||
if status_filter:
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
from typing import TYPE_CHECKING
|
||||
from uuid import UUID
|
||||
|
||||
@@ -53,6 +53,7 @@ from app.services.organizations import (
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import AsyncIterator
|
||||
|
||||
from fastapi_pagination.limit_offset import LimitOffsetPage
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from app.services.organizations import OrganizationContext
|
||||
@@ -90,7 +91,7 @@ def _parse_since(value: str | None) -> datetime | None:
|
||||
except ValueError:
|
||||
return None
|
||||
if parsed.tzinfo is not None:
|
||||
return parsed.astimezone(timezone.utc).replace(tzinfo=None)
|
||||
return parsed.astimezone(UTC).replace(tzinfo=None)
|
||||
return parsed
|
||||
|
||||
|
||||
@@ -343,7 +344,7 @@ async def list_board_group_memory(
|
||||
is_chat: bool | None = IS_CHAT_QUERY,
|
||||
session: AsyncSession = SESSION_DEP,
|
||||
ctx: OrganizationContext = ORG_MEMBER_DEP,
|
||||
) -> DefaultLimitOffsetPage[BoardGroupMemoryRead]:
|
||||
) -> LimitOffsetPage[BoardGroupMemoryRead]:
|
||||
"""List board-group memory entries for a specific group."""
|
||||
await _require_group_access(session, group_id=group_id, ctx=ctx, write=False)
|
||||
statement = (
|
||||
@@ -439,7 +440,7 @@ async def list_board_group_memory_for_board(
|
||||
is_chat: bool | None = IS_CHAT_QUERY,
|
||||
board: Board = BOARD_READ_DEP,
|
||||
session: AsyncSession = SESSION_DEP,
|
||||
) -> DefaultLimitOffsetPage[BoardGroupMemoryRead]:
|
||||
) -> LimitOffsetPage[BoardGroupMemoryRead]:
|
||||
"""List memory entries for the board's linked group."""
|
||||
group_id = board.board_group_id
|
||||
if group_id is None:
|
||||
|
||||
@@ -50,6 +50,7 @@ from app.services.organizations import (
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastapi_pagination.limit_offset import LimitOffsetPage
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from app.models.organization_members import OrganizationMember
|
||||
@@ -103,7 +104,7 @@ async def _require_group_access(
|
||||
async def list_board_groups(
|
||||
session: AsyncSession = SESSION_DEP,
|
||||
ctx: OrganizationContext = ORG_MEMBER_DEP,
|
||||
) -> DefaultLimitOffsetPage[BoardGroupRead]:
|
||||
) -> LimitOffsetPage[BoardGroupRead]:
|
||||
"""List board groups in the active organization."""
|
||||
if member_all_boards_read(ctx.member):
|
||||
statement = select(BoardGroup).where(
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
from typing import TYPE_CHECKING
|
||||
from uuid import UUID
|
||||
|
||||
@@ -39,6 +39,7 @@ from app.services.mentions import extract_mentions, matches_agent_mention
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import AsyncIterator
|
||||
|
||||
from fastapi_pagination.limit_offset import LimitOffsetPage
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from app.models.boards import Board
|
||||
@@ -67,7 +68,7 @@ def _parse_since(value: str | None) -> datetime | None:
|
||||
except ValueError:
|
||||
return None
|
||||
if parsed.tzinfo is not None:
|
||||
return parsed.astimezone(timezone.utc).replace(tzinfo=None)
|
||||
return parsed.astimezone(UTC).replace(tzinfo=None)
|
||||
return parsed
|
||||
|
||||
|
||||
@@ -250,7 +251,7 @@ async def list_board_memory(
|
||||
board: Board = BOARD_READ_DEP,
|
||||
session: AsyncSession = SESSION_DEP,
|
||||
_actor: ActorContext = ACTOR_DEP,
|
||||
) -> DefaultLimitOffsetPage[BoardMemoryRead]:
|
||||
) -> LimitOffsetPage[BoardMemoryRead]:
|
||||
"""List board memory entries, optionally filtering chat entries."""
|
||||
statement = (
|
||||
BoardMemory.objects.filter_by(board_id=board.id)
|
||||
|
||||
@@ -50,6 +50,7 @@ from app.services.board_snapshot import build_board_snapshot
|
||||
from app.services.organizations import OrganizationContext, board_access_filter
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastapi_pagination.limit_offset import LimitOffsetPage
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
router = APIRouter(prefix="/boards", tags=["boards"])
|
||||
@@ -246,7 +247,7 @@ async def list_boards(
|
||||
board_group_id: UUID | None = BOARD_GROUP_ID_QUERY,
|
||||
session: AsyncSession = SESSION_DEP,
|
||||
ctx: OrganizationContext = ORG_MEMBER_DEP,
|
||||
) -> DefaultLimitOffsetPage[BoardRead]:
|
||||
) -> LimitOffsetPage[BoardRead]:
|
||||
"""List boards visible to the current organization member."""
|
||||
statement = select(Board).where(board_access_filter(ctx.member, write=False))
|
||||
if gateway_id is not None:
|
||||
|
||||
@@ -46,6 +46,7 @@ from app.services.template_sync import (
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastapi_pagination.limit_offset import LimitOffsetPage
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from app.services.organizations import OrganizationContext
|
||||
@@ -224,7 +225,7 @@ async def _ensure_main_agent(
|
||||
async def list_gateways(
|
||||
session: AsyncSession = SESSION_DEP,
|
||||
ctx: OrganizationContext = ORG_ADMIN_DEP,
|
||||
) -> DefaultLimitOffsetPage[GatewayRead]:
|
||||
) -> LimitOffsetPage[GatewayRead]:
|
||||
"""List gateways for the caller's organization."""
|
||||
statement = (
|
||||
Gateway.objects.filter_by(organization_id=ctx.organization.id)
|
||||
|
||||
@@ -8,7 +8,8 @@ from typing import Literal
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy import DateTime, case, cast, func
|
||||
from sqlalchemy import DateTime, case, func
|
||||
from sqlalchemy import cast as sql_cast
|
||||
from sqlmodel import col, select
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
@@ -152,7 +153,7 @@ async def _query_cycle_time(
|
||||
board_ids: list[UUID],
|
||||
) -> DashboardRangeSeries:
|
||||
bucket_col = func.date_trunc(range_spec.bucket, Task.updated_at).label("bucket")
|
||||
in_progress = cast(Task.in_progress_at, DateTime)
|
||||
in_progress = sql_cast(Task.in_progress_at, DateTime)
|
||||
duration_hours = func.extract("epoch", Task.updated_at - in_progress) / 3600.0
|
||||
statement = (
|
||||
select(bucket_col, func.avg(duration_hours))
|
||||
@@ -249,7 +250,7 @@ async def _median_cycle_time_7d(
|
||||
) -> float | None:
|
||||
now = utcnow()
|
||||
start = now - timedelta(days=7)
|
||||
in_progress = cast(Task.in_progress_at, DateTime)
|
||||
in_progress = sql_cast(Task.in_progress_at, DateTime)
|
||||
duration_hours = func.extract("epoch", Task.updated_at - in_progress) / 3600.0
|
||||
statement = (
|
||||
select(func.percentile_cont(0.5).within_group(duration_hours))
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import secrets
|
||||
from typing import TYPE_CHECKING, Any, Sequence
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
@@ -65,6 +65,9 @@ from app.services.organizations import (
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Sequence
|
||||
|
||||
from fastapi_pagination.limit_offset import LimitOffsetPage
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from app.core.auth import AuthContext
|
||||
@@ -369,7 +372,7 @@ async def get_my_membership(
|
||||
async def list_org_members(
|
||||
session: AsyncSession = SESSION_DEP,
|
||||
ctx: OrganizationContext = ORG_MEMBER_DEP,
|
||||
) -> DefaultLimitOffsetPage[OrganizationMemberRead]:
|
||||
) -> LimitOffsetPage[OrganizationMemberRead]:
|
||||
"""List members for the active organization."""
|
||||
statement = (
|
||||
select(OrganizationMember, User)
|
||||
@@ -542,7 +545,7 @@ async def remove_org_member(
|
||||
async def list_org_invites(
|
||||
session: AsyncSession = SESSION_DEP,
|
||||
ctx: OrganizationContext = ORG_ADMIN_DEP,
|
||||
) -> DefaultLimitOffsetPage[OrganizationInviteRead]:
|
||||
) -> LimitOffsetPage[OrganizationInviteRead]:
|
||||
"""List pending invites for the active organization."""
|
||||
statement = (
|
||||
OrganizationInvite.objects.filter_by(organization_id=ctx.organization.id)
|
||||
|
||||
@@ -3,13 +3,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Generic, TypeVar
|
||||
from typing import TYPE_CHECKING, Any, Generic, TypeVar
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
from app.db.queryset import QuerySet, qs
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sqlalchemy.orm import Mapped
|
||||
from sqlalchemy.sql.elements import ColumnElement
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
from sqlmodel.sql.expression import SelectOfScalar
|
||||
|
||||
@@ -27,11 +29,17 @@ class APIQuerySet(Generic[ModelT]):
|
||||
"""Expose the underlying SQL statement for advanced composition."""
|
||||
return self.queryset.statement
|
||||
|
||||
def filter(self, *criteria: object) -> APIQuerySet[ModelT]:
|
||||
def filter(
|
||||
self,
|
||||
*criteria: ColumnElement[bool] | bool,
|
||||
) -> APIQuerySet[ModelT]:
|
||||
"""Return a new queryset with additional SQL criteria applied."""
|
||||
return APIQuerySet(self.queryset.filter(*criteria))
|
||||
|
||||
def order_by(self, *ordering: object) -> APIQuerySet[ModelT]:
|
||||
def order_by(
|
||||
self,
|
||||
*ordering: Mapped[Any] | ColumnElement[Any] | str,
|
||||
) -> APIQuerySet[ModelT]:
|
||||
"""Return a new queryset with ordering clauses applied."""
|
||||
return APIQuerySet(self.queryset.order_by(*ordering))
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ import json
|
||||
from collections import deque
|
||||
from contextlib import suppress
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from typing import TYPE_CHECKING, cast
|
||||
from datetime import UTC, datetime
|
||||
from typing import TYPE_CHECKING
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
|
||||
@@ -67,8 +67,9 @@ from app.services.task_dependencies import (
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import AsyncIterator, Sequence
|
||||
|
||||
from fastapi_pagination.limit_offset import LimitOffsetPage
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
from sqlmodel.sql.expression import Select, SelectOfScalar
|
||||
from sqlmodel.sql.expression import SelectOfScalar
|
||||
|
||||
from app.core.auth import AuthContext
|
||||
from app.models.users import User
|
||||
@@ -85,6 +86,7 @@ TASK_EVENT_TYPES = {
|
||||
SSE_SEEN_MAX = 2000
|
||||
TASK_SNIPPET_MAX_LEN = 500
|
||||
TASK_SNIPPET_TRUNCATED_LEN = 497
|
||||
TASK_EVENT_ROW_LEN = 2
|
||||
BOARD_READ_DEP = Depends(get_board_for_actor_read)
|
||||
ACTOR_DEP = Depends(require_admin_or_agent)
|
||||
SINCE_QUERY = Query(default=None)
|
||||
@@ -154,7 +156,7 @@ def _parse_since(value: str | None) -> datetime | None:
|
||||
except ValueError:
|
||||
return None
|
||||
if parsed.tzinfo is not None:
|
||||
return parsed.astimezone(timezone.utc).replace(tzinfo=None)
|
||||
return parsed.astimezone(UTC).replace(tzinfo=None)
|
||||
return parsed
|
||||
|
||||
|
||||
@@ -168,6 +170,24 @@ def _coerce_task_items(items: Sequence[object]) -> list[Task]:
|
||||
return tasks
|
||||
|
||||
|
||||
def _coerce_task_event_rows(
|
||||
items: Sequence[object],
|
||||
) -> list[tuple[ActivityEvent, Task | None]]:
|
||||
rows: list[tuple[ActivityEvent, Task | None]] = []
|
||||
for item in items:
|
||||
if (
|
||||
isinstance(item, tuple)
|
||||
and len(item) == TASK_EVENT_ROW_LEN
|
||||
and isinstance(item[0], ActivityEvent)
|
||||
and (isinstance(item[1], Task) or item[1] is None)
|
||||
):
|
||||
rows.append((item[0], item[1]))
|
||||
continue
|
||||
msg = "Expected (ActivityEvent, Task | None) rows"
|
||||
raise TypeError(msg)
|
||||
return rows
|
||||
|
||||
|
||||
async def _lead_was_mentioned(
|
||||
session: AsyncSession,
|
||||
task: Task,
|
||||
@@ -276,16 +296,16 @@ async def _fetch_task_events(
|
||||
)
|
||||
if not task_ids:
|
||||
return []
|
||||
statement = cast(
|
||||
"Select[tuple[ActivityEvent, Task | None]]",
|
||||
statement = (
|
||||
select(ActivityEvent, Task)
|
||||
.outerjoin(Task, col(ActivityEvent.task_id) == col(Task.id))
|
||||
.where(col(ActivityEvent.task_id).in_(task_ids))
|
||||
.where(col(ActivityEvent.event_type).in_(TASK_EVENT_TYPES))
|
||||
.where(col(ActivityEvent.created_at) >= since)
|
||||
.order_by(asc(col(ActivityEvent.created_at))),
|
||||
.order_by(asc(col(ActivityEvent.created_at)))
|
||||
)
|
||||
return list(await session.exec(statement))
|
||||
result = await session.execute(statement)
|
||||
return _coerce_task_event_rows(list(result.tuples().all()))
|
||||
|
||||
|
||||
def _serialize_comment(event: ActivityEvent) -> dict[str, object]:
|
||||
@@ -718,7 +738,7 @@ async def list_tasks(
|
||||
board: Board = BOARD_READ_DEP,
|
||||
session: AsyncSession = SESSION_DEP,
|
||||
_actor: ActorContext = ACTOR_DEP,
|
||||
) -> DefaultLimitOffsetPage[TaskRead]:
|
||||
) -> LimitOffsetPage[TaskRead]:
|
||||
"""List board tasks with optional status and assignment filters."""
|
||||
statement = _task_list_statement(
|
||||
board_id=board.id,
|
||||
@@ -914,7 +934,7 @@ async def delete_task(
|
||||
async def list_task_comments(
|
||||
task: Task = TASK_DEP,
|
||||
session: AsyncSession = SESSION_DEP,
|
||||
) -> DefaultLimitOffsetPage[TaskCommentRead]:
|
||||
) -> LimitOffsetPage[TaskCommentRead]:
|
||||
"""List comments for a task in chronological order."""
|
||||
statement = (
|
||||
select(ActivityEvent)
|
||||
|
||||
Reference in New Issue
Block a user