diff --git a/backend/app/api/activity.py b/backend/app/api/activity.py index 6848d66d..966cba38 100644 --- a/backend/app/api/activity.py +++ b/backend/app/api/activity.py @@ -1,13 +1,14 @@ from __future__ import annotations from fastapi import APIRouter, Depends, Query -from sqlmodel import Session, select +from sqlalchemy import desc +from sqlmodel import Session, col, select -from app.core.auth import get_auth_context +from app.api.deps import require_admin_auth +from app.core.auth import AuthContext from app.db.session import get_session from app.models.activity_events import ActivityEvent from app.schemas.activity_events import ActivityEventRead -from app.services.admin_access import require_admin router = APIRouter(prefix="/activity", tags=["activity"]) @@ -17,10 +18,12 @@ def list_activity( limit: int = Query(50, ge=1, le=200), offset: int = Query(0, ge=0), session: Session = Depends(get_session), - auth=Depends(get_auth_context), + auth: AuthContext = Depends(require_admin_auth), ) -> list[ActivityEvent]: - require_admin(auth) statement = ( - select(ActivityEvent).order_by(ActivityEvent.created_at.desc()).offset(offset).limit(limit) + select(ActivityEvent) + .order_by(desc(col(ActivityEvent.created_at))) + .offset(offset) + .limit(limit) ) return list(session.exec(statement)) diff --git a/backend/app/api/agents.py b/backend/app/api/agents.py index a799d8e6..8992288d 100644 --- a/backend/app/api/agents.py +++ b/backend/app/api/agents.py @@ -7,10 +7,10 @@ from uuid import uuid4 from fastapi import APIRouter, Depends, HTTPException, status from sqlmodel import Session, select -from app.core.auth import get_auth_context +from app.api.deps import require_admin_auth +from app.core.auth import AuthContext from app.db.session import get_session from app.integrations.openclaw_gateway import OpenClawGatewayError, openclaw_call -from app.models.activity_events import ActivityEvent from app.models.agents import Agent from app.schemas.agents import ( AgentCreate, @@ -19,7 +19,7 @@ from app.schemas.agents import ( AgentRead, AgentUpdate, ) -from app.services.admin_access import require_admin +from app.services.activity_log import record_activity from app.services.agent_provisioning import send_provisioning_message router = APIRouter(prefix="/agents", tags=["agents"]) @@ -54,20 +54,28 @@ def _with_computed_status(agent: Agent) -> Agent: def _record_heartbeat(session: Session, agent: Agent) -> None: - event = ActivityEvent( + record_activity( + session, event_type="agent.heartbeat", message=f"Heartbeat received from {agent.name}.", agent_id=agent.id, ) - session.add(event) + + +def _record_provisioning_failure(session: Session, agent: Agent, error: str) -> None: + record_activity( + session, + event_type="agent.provision.failed", + message=f"Provisioning message failed: {error}", + agent_id=agent.id, + ) @router.get("", response_model=list[AgentRead]) def list_agents( session: Session = Depends(get_session), - auth=Depends(get_auth_context), + auth: AuthContext = Depends(require_admin_auth), ) -> list[Agent]: - require_admin(auth) agents = list(session.exec(select(Agent))) return [_with_computed_status(agent) for agent in agents] @@ -76,9 +84,8 @@ def list_agents( async def create_agent( payload: AgentCreate, session: Session = Depends(get_session), - auth=Depends(get_auth_context), + auth: AuthContext = Depends(require_admin_auth), ) -> Agent: - require_admin(auth) agent = Agent.model_validate(payload) session_key, session_error = await _ensure_gateway_session(agent.name) agent.openclaw_session_id = session_key @@ -86,41 +93,27 @@ async def create_agent( session.commit() session.refresh(agent) if session_error: - session.add( - ActivityEvent( - event_type="agent.session.failed", - message=f"Session sync failed for {agent.name}: {session_error}", - agent_id=agent.id, - ) + record_activity( + session, + event_type="agent.session.failed", + message=f"Session sync failed for {agent.name}: {session_error}", + agent_id=agent.id, ) else: - session.add( - ActivityEvent( - event_type="agent.session.created", - message=f"Session created for {agent.name}.", - agent_id=agent.id, - ) + record_activity( + session, + event_type="agent.session.created", + message=f"Session created for {agent.name}.", + agent_id=agent.id, ) session.commit() try: await send_provisioning_message(agent) except OpenClawGatewayError as exc: - session.add( - ActivityEvent( - event_type="agent.provision.failed", - message=f"Provisioning message failed: {exc}", - agent_id=agent.id, - ) - ) + _record_provisioning_failure(session, agent, str(exc)) session.commit() except Exception as exc: # pragma: no cover - unexpected provisioning errors - session.add( - ActivityEvent( - event_type="agent.provision.failed", - message=f"Provisioning message failed: {exc}", - agent_id=agent.id, - ) - ) + _record_provisioning_failure(session, agent, str(exc)) session.commit() return agent @@ -129,9 +122,8 @@ async def create_agent( def get_agent( agent_id: str, session: Session = Depends(get_session), - auth=Depends(get_auth_context), + auth: AuthContext = Depends(require_admin_auth), ) -> Agent: - require_admin(auth) agent = session.get(Agent, agent_id) if agent is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) @@ -143,9 +135,8 @@ def update_agent( agent_id: str, payload: AgentUpdate, session: Session = Depends(get_session), - auth=Depends(get_auth_context), + auth: AuthContext = Depends(require_admin_auth), ) -> Agent: - require_admin(auth) agent = session.get(Agent, agent_id) if agent is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) @@ -164,9 +155,8 @@ def heartbeat_agent( agent_id: str, payload: AgentHeartbeat, session: Session = Depends(get_session), - auth=Depends(get_auth_context), + auth: AuthContext = Depends(require_admin_auth), ) -> Agent: - require_admin(auth) agent = session.get(Agent, agent_id) if agent is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) @@ -185,9 +175,8 @@ def heartbeat_agent( async def heartbeat_or_create_agent( payload: AgentHeartbeatCreate, session: Session = Depends(get_session), - auth=Depends(get_auth_context), + auth: AuthContext = Depends(require_admin_auth), ) -> Agent: - require_admin(auth) agent = session.exec(select(Agent).where(Agent.name == payload.name)).first() if agent is None: agent = Agent(name=payload.name, status=payload.status or "online") @@ -197,81 +186,53 @@ async def heartbeat_or_create_agent( session.commit() session.refresh(agent) if session_error: - session.add( - ActivityEvent( - event_type="agent.session.failed", - message=f"Session sync failed for {agent.name}: {session_error}", - agent_id=agent.id, - ) + record_activity( + session, + event_type="agent.session.failed", + message=f"Session sync failed for {agent.name}: {session_error}", + agent_id=agent.id, ) else: - session.add( - ActivityEvent( - event_type="agent.session.created", - message=f"Session created for {agent.name}.", - agent_id=agent.id, - ) + record_activity( + session, + event_type="agent.session.created", + message=f"Session created for {agent.name}.", + agent_id=agent.id, ) session.commit() try: await send_provisioning_message(agent) except OpenClawGatewayError as exc: - session.add( - ActivityEvent( - event_type="agent.provision.failed", - message=f"Provisioning message failed: {exc}", - agent_id=agent.id, - ) - ) + _record_provisioning_failure(session, agent, str(exc)) session.commit() except Exception as exc: # pragma: no cover - unexpected provisioning errors - session.add( - ActivityEvent( - event_type="agent.provision.failed", - message=f"Provisioning message failed: {exc}", - agent_id=agent.id, - ) - ) + _record_provisioning_failure(session, agent, str(exc)) session.commit() elif not agent.openclaw_session_id: session_key, session_error = await _ensure_gateway_session(agent.name) agent.openclaw_session_id = session_key if session_error: - session.add( - ActivityEvent( - event_type="agent.session.failed", - message=f"Session sync failed for {agent.name}: {session_error}", - agent_id=agent.id, - ) + record_activity( + session, + event_type="agent.session.failed", + message=f"Session sync failed for {agent.name}: {session_error}", + agent_id=agent.id, ) else: - session.add( - ActivityEvent( - event_type="agent.session.created", - message=f"Session created for {agent.name}.", - agent_id=agent.id, - ) + record_activity( + session, + event_type="agent.session.created", + message=f"Session created for {agent.name}.", + agent_id=agent.id, ) session.commit() try: await send_provisioning_message(agent) except OpenClawGatewayError as exc: - session.add( - ActivityEvent( - event_type="agent.provision.failed", - message=f"Provisioning message failed: {exc}", - agent_id=agent.id, - ) - ) + _record_provisioning_failure(session, agent, str(exc)) session.commit() except Exception as exc: # pragma: no cover - unexpected provisioning errors - session.add( - ActivityEvent( - event_type="agent.provision.failed", - message=f"Provisioning message failed: {exc}", - agent_id=agent.id, - ) - ) + _record_provisioning_failure(session, agent, str(exc)) session.commit() if payload.status: agent.status = payload.status @@ -288,9 +249,8 @@ async def heartbeat_or_create_agent( def delete_agent( agent_id: str, session: Session = Depends(get_session), - auth=Depends(get_auth_context), + auth: AuthContext = Depends(require_admin_auth), ) -> dict[str, bool]: - require_admin(auth) agent = session.get(Agent, agent_id) if agent: session.delete(agent) diff --git a/backend/app/api/auth.py b/backend/app/api/auth.py index c66562b6..cfcc7283 100644 --- a/backend/app/api/auth.py +++ b/backend/app/api/auth.py @@ -2,14 +2,14 @@ from __future__ import annotations from fastapi import APIRouter, Depends, HTTPException, status -from app.core.auth import get_auth_context +from app.core.auth import AuthContext, get_auth_context from app.schemas.users import UserRead router = APIRouter(prefix="/auth", tags=["auth"]) @router.post("/bootstrap", response_model=UserRead) -async def bootstrap_user(auth=Depends(get_auth_context)) -> UserRead: +async def bootstrap_user(auth: AuthContext = Depends(get_auth_context)) -> UserRead: if auth.actor_type != "user" or auth.user is None: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) - return auth.user + return UserRead.model_validate(auth.user) diff --git a/backend/app/api/boards.py b/backend/app/api/boards.py index 70885a7f..f1bec3d4 100644 --- a/backend/app/api/boards.py +++ b/backend/app/api/boards.py @@ -1,13 +1,13 @@ from __future__ import annotations -from fastapi import APIRouter, Depends, HTTPException, status +from fastapi import APIRouter, Depends from sqlmodel import Session, select -from app.core.auth import get_auth_context +from app.api.deps import get_board_or_404, require_admin_auth +from app.core.auth import AuthContext from app.db.session import get_session from app.models.boards import Board from app.schemas.boards import BoardCreate, BoardRead, BoardUpdate -from app.services.admin_access import require_admin router = APIRouter(prefix="/boards", tags=["boards"]) @@ -15,9 +15,8 @@ router = APIRouter(prefix="/boards", tags=["boards"]) @router.get("", response_model=list[BoardRead]) def list_boards( session: Session = Depends(get_session), - auth=Depends(get_auth_context), + auth: AuthContext = Depends(require_admin_auth), ) -> list[Board]: - require_admin(auth) return list(session.exec(select(Board))) @@ -25,9 +24,8 @@ def list_boards( def create_board( payload: BoardCreate, session: Session = Depends(get_session), - auth=Depends(get_auth_context), + auth: AuthContext = Depends(require_admin_auth), ) -> Board: - require_admin(auth) board = Board.model_validate(payload) session.add(board) session.commit() @@ -37,28 +35,19 @@ def create_board( @router.get("/{board_id}", response_model=BoardRead) def get_board( - board_id: str, - session: Session = Depends(get_session), - auth=Depends(get_auth_context), + board: Board = Depends(get_board_or_404), + auth: AuthContext = Depends(require_admin_auth), ) -> Board: - require_admin(auth) - board = session.get(Board, board_id) - if board is None: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) return board @router.patch("/{board_id}", response_model=BoardRead) def update_board( - board_id: str, payload: BoardUpdate, session: Session = Depends(get_session), - auth=Depends(get_auth_context), + board: Board = Depends(get_board_or_404), + auth: AuthContext = Depends(require_admin_auth), ) -> Board: - require_admin(auth) - board = session.get(Board, board_id) - if board is None: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) updates = payload.model_dump(exclude_unset=True) for key, value in updates.items(): setattr(board, key, value) @@ -70,13 +59,10 @@ def update_board( @router.delete("/{board_id}") def delete_board( - board_id: str, session: Session = Depends(get_session), - auth=Depends(get_auth_context), + board: Board = Depends(get_board_or_404), + auth: AuthContext = Depends(require_admin_auth), ) -> dict[str, bool]: - require_admin(auth) - board = session.get(Board, board_id) - if board: - session.delete(board) - session.commit() + session.delete(board) + session.commit() return {"ok": True} diff --git a/backend/app/api/deps.py b/backend/app/api/deps.py new file mode 100644 index 00000000..2aa4b41c --- /dev/null +++ b/backend/app/api/deps.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +from fastapi import Depends, HTTPException, status +from sqlmodel import Session + +from app.core.auth import AuthContext, get_auth_context +from app.db.session import get_session +from app.models.boards import Board +from app.models.tasks import Task +from app.services.admin_access import require_admin + + +def require_admin_auth(auth: AuthContext = Depends(get_auth_context)) -> AuthContext: + require_admin(auth) + return auth + + +def get_board_or_404( + board_id: str, + session: Session = Depends(get_session), +) -> Board: + board = session.get(Board, board_id) + if board is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + return board + + +def get_task_or_404( + task_id: str, + board: Board = Depends(get_board_or_404), + session: Session = Depends(get_session), +) -> Task: + task = session.get(Task, task_id) + if task is None or task.board_id != board.id: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + return task diff --git a/backend/app/api/gateway.py b/backend/app/api/gateway.py index 0800bbcb..c572019f 100644 --- a/backend/app/api/gateway.py +++ b/backend/app/api/gateway.py @@ -2,7 +2,8 @@ from __future__ import annotations from fastapi import APIRouter, Body, Depends, HTTPException, status -from app.core.auth import get_auth_context +from app.api.deps import require_admin_auth +from app.core.auth import AuthContext from app.core.config import settings from app.integrations.openclaw_gateway import ( OpenClawGatewayError, @@ -10,14 +11,12 @@ from app.integrations.openclaw_gateway import ( openclaw_call, send_message, ) -from app.services.admin_access import require_admin router = APIRouter(prefix="/gateway", tags=["gateway"]) @router.get("/status") -async def gateway_status(auth=Depends(get_auth_context)) -> dict[str, object]: - require_admin(auth) +async def gateway_status(auth: AuthContext = Depends(require_admin_auth)) -> dict[str, object]: gateway_url = settings.openclaw_gateway_url or "ws://127.0.0.1:18789" try: sessions = await openclaw_call("sessions.list") @@ -40,8 +39,7 @@ async def gateway_status(auth=Depends(get_auth_context)) -> dict[str, object]: @router.get("/sessions") -async def list_sessions(auth=Depends(get_auth_context)) -> dict[str, object]: - require_admin(auth) +async def list_sessions(auth: AuthContext = Depends(require_admin_auth)) -> dict[str, object]: try: sessions = await openclaw_call("sessions.list") except OpenClawGatewayError as exc: @@ -52,8 +50,9 @@ async def list_sessions(auth=Depends(get_auth_context)) -> dict[str, object]: @router.get("/sessions/{session_id}") -async def get_session(session_id: str, auth=Depends(get_auth_context)) -> dict[str, object]: - require_admin(auth) +async def get_session( + session_id: str, auth: AuthContext = Depends(require_admin_auth) +) -> dict[str, object]: try: sessions = await openclaw_call("sessions.list") except OpenClawGatewayError as exc: @@ -69,8 +68,9 @@ async def get_session(session_id: str, auth=Depends(get_auth_context)) -> dict[s @router.get("/sessions/{session_id}/history") -async def get_session_history(session_id: str, auth=Depends(get_auth_context)) -> dict[str, object]: - require_admin(auth) +async def get_session_history( + session_id: str, auth: AuthContext = Depends(require_admin_auth) +) -> dict[str, object]: try: history = await get_chat_history(session_id) except OpenClawGatewayError as exc: @@ -84,9 +84,8 @@ async def get_session_history(session_id: str, auth=Depends(get_auth_context)) - async def send_session_message( session_id: str, payload: dict = Body(...), - auth=Depends(get_auth_context), + auth: AuthContext = Depends(require_admin_auth), ) -> dict[str, bool]: - require_admin(auth) content = payload.get("content") if not content: raise HTTPException( diff --git a/backend/app/api/tasks.py b/backend/app/api/tasks.py index b68817a1..7212e21e 100644 --- a/backend/app/api/tasks.py +++ b/backend/app/api/tasks.py @@ -2,45 +2,36 @@ from __future__ import annotations from datetime import datetime -from fastapi import APIRouter, Depends, HTTPException, status +from fastapi import APIRouter, Depends from sqlmodel import Session, select -from app.core.auth import get_auth_context +from app.api.deps import get_board_or_404, get_task_or_404, require_admin_auth +from app.core.auth import AuthContext from app.db.session import get_session -from app.models.activity_events import ActivityEvent from app.models.boards import Board from app.models.tasks import Task from app.schemas.tasks import TaskCreate, TaskRead, TaskUpdate -from app.services.admin_access import require_admin +from app.services.activity_log import record_activity router = APIRouter(prefix="/boards/{board_id}/tasks", tags=["tasks"]) @router.get("", response_model=list[TaskRead]) def list_tasks( - board_id: str, + board: Board = Depends(get_board_or_404), session: Session = Depends(get_session), - auth=Depends(get_auth_context), + auth: AuthContext = Depends(require_admin_auth), ) -> list[Task]: - require_admin(auth) - board = session.get(Board, board_id) - if board is None: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) return list(session.exec(select(Task).where(Task.board_id == board.id))) @router.post("", response_model=TaskRead) def create_task( - board_id: str, payload: TaskCreate, + board: Board = Depends(get_board_or_404), session: Session = Depends(get_session), - auth=Depends(get_auth_context), + auth: AuthContext = Depends(require_admin_auth), ) -> Task: - require_admin(auth) - board = session.get(Board, board_id) - if board is None: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) - task = Task.model_validate(payload) task.board_id = board.id if task.created_by_user_id is None and auth.user is not None: @@ -49,32 +40,23 @@ def create_task( session.commit() session.refresh(task) - event = ActivityEvent( + record_activity( + session, event_type="task.created", task_id=task.id, message=f"Task created: {task.title}.", ) - session.add(event) session.commit() return task @router.patch("/{task_id}", response_model=TaskRead) def update_task( - board_id: str, - task_id: str, payload: TaskUpdate, + task: Task = Depends(get_task_or_404), session: Session = Depends(get_session), - auth=Depends(get_auth_context), + auth: AuthContext = Depends(require_admin_auth), ) -> Task: - require_admin(auth) - board = session.get(Board, board_id) - if board is None: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) - task = session.get(Task, task_id) - if task is None or task.board_id != board.id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) - previous_status = task.status updates = payload.model_dump(exclude_unset=True) for key, value in updates.items(): @@ -91,25 +73,17 @@ def update_task( else: event_type = "task.updated" message = f"Task updated: {task.title}." - event = ActivityEvent(event_type=event_type, task_id=task.id, message=message) - session.add(event) + record_activity(session, event_type=event_type, task_id=task.id, message=message) session.commit() return task @router.delete("/{task_id}") def delete_task( - board_id: str, - task_id: str, session: Session = Depends(get_session), - auth=Depends(get_auth_context), + task: Task = Depends(get_task_or_404), + auth: AuthContext = Depends(require_admin_auth), ) -> dict[str, bool]: - require_admin(auth) - board = session.get(Board, board_id) - if board is None: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) - task = session.get(Task, task_id) - if task and task.board_id == board.id: - session.delete(task) - session.commit() + session.delete(task) + session.commit() return {"ok": True} diff --git a/backend/app/integrations/openclaw_gateway.py b/backend/app/integrations/openclaw_gateway.py index 78da547f..06c0eae6 100644 --- a/backend/app/integrations/openclaw_gateway.py +++ b/backend/app/integrations/openclaw_gateway.py @@ -58,10 +58,12 @@ async def _send_request( async def _handle_challenge( - ws: websockets.WebSocketClientProtocol, first_message: str | None + ws: websockets.WebSocketClientProtocol, first_message: str | bytes | None ) -> None: if not first_message: return + if isinstance(first_message, bytes): + first_message = first_message.decode("utf-8") data = json.loads(first_message) if data.get("type") != "event" or data.get("event") != "connect.challenge": return diff --git a/backend/app/services/activity_log.py b/backend/app/services/activity_log.py new file mode 100644 index 00000000..49cf7843 --- /dev/null +++ b/backend/app/services/activity_log.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from uuid import UUID + +from sqlmodel import Session + +from app.models.activity_events import ActivityEvent + + +def record_activity( + session: Session, + *, + event_type: str, + message: str, + agent_id: UUID | None = None, + task_id: UUID | None = None, +) -> ActivityEvent: + event = ActivityEvent( + event_type=event_type, + message=message, + agent_id=agent_id, + task_id=task_id, + ) + session.add(event) + return event diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 01ba5857..f0127996 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -33,7 +33,17 @@ dev = [ "black==24.10.0", "flake8==7.1.1", "isort==5.13.2", + "mypy==1.11.2", "pytest==8.3.3", "pytest-asyncio==0.24.0", "ruff==0.6.9", ] + +[tool.mypy] +python_version = "3.12" +ignore_missing_imports = true +warn_unused_ignores = true +warn_redundant_casts = true +warn_unused_configs = true +check_untyped_defs = true +plugins = ["pydantic.mypy"] diff --git a/backend/requirements-dev.txt b/backend/requirements-dev.txt index c0bc50de..c4afd042 100644 --- a/backend/requirements-dev.txt +++ b/backend/requirements-dev.txt @@ -1,3 +1,4 @@ pytest==8.3.3 pytest-asyncio==0.24.0 ruff==0.6.9 +mypy==1.11.2 diff --git a/backend/uv.lock b/backend/uv.lock index 77cc3403..a9c97b55 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -437,6 +437,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, ] +[[package]] +name = "mypy" +version = "1.11.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/86/5d7cbc4974fd564550b80fbb8103c05501ea11aa7835edf3351d90095896/mypy-1.11.2.tar.gz", hash = "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79", size = 3078806, upload-time = "2024-08-24T22:50:11.357Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/3a/ed7b12ecc3f6db2f664ccf85cb2e004d3e90bec928e9d7be6aa2f16b7cdf/mypy-1.11.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318", size = 10990335, upload-time = "2024-08-24T22:49:54.245Z" }, + { url = "https://files.pythonhosted.org/packages/04/e4/1a9051e2ef10296d206519f1df13d2cc896aea39e8683302f89bf5792a59/mypy-1.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36", size = 10007119, upload-time = "2024-08-24T22:49:03.451Z" }, + { url = "https://files.pythonhosted.org/packages/f3/3c/350a9da895f8a7e87ade0028b962be0252d152e0c2fbaafa6f0658b4d0d4/mypy-1.11.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987", size = 12506856, upload-time = "2024-08-24T22:50:08.804Z" }, + { url = "https://files.pythonhosted.org/packages/b6/49/ee5adf6a49ff13f4202d949544d3d08abb0ea1f3e7f2a6d5b4c10ba0360a/mypy-1.11.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca", size = 12952066, upload-time = "2024-08-24T22:50:03.89Z" }, + { url = "https://files.pythonhosted.org/packages/27/c0/b19d709a42b24004d720db37446a42abadf844d5c46a2c442e2a074d70d9/mypy-1.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70", size = 9664000, upload-time = "2024-08-24T22:49:59.703Z" }, + { url = "https://files.pythonhosted.org/packages/42/3a/bdf730640ac523229dd6578e8a581795720a9321399de494374afc437ec5/mypy-1.11.2-py3-none-any.whl", hash = "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12", size = 2619625, upload-time = "2024-08-24T22:50:01.842Z" }, +] + [[package]] name = "mypy-extensions" version = "1.1.0" @@ -472,6 +490,7 @@ dev = [ { name = "black" }, { name = "flake8" }, { name = "isort" }, + { name = "mypy" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "ruff" }, @@ -486,6 +505,7 @@ requires-dist = [ { name = "flake8", marker = "extra == 'dev'", specifier = "==7.1.1" }, { name = "httpx", specifier = "==0.27.2" }, { name = "isort", marker = "extra == 'dev'", specifier = "==5.13.2" }, + { name = "mypy", marker = "extra == 'dev'", specifier = "==1.11.2" }, { name = "psycopg", extras = ["binary"], specifier = "==3.2.1" }, { name = "pydantic-settings", specifier = "==2.5.2" }, { name = "pytest", marker = "extra == 'dev'", specifier = "==8.3.3" },