Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,8 +257,7 @@ async def get_user(self, user_id: str) -> UserDocument:
return user

# Route: no None check, no HTTPException; the service already raises if missing
@router.get("/{user_id}", response_model=UserResponse,
responses={404: {"model": ErrorResponse}})
@router.get("/{user_id}", responses={404: {"model": ErrorResponse}})
async def get_user(user_id: str, service: FromDishka[AdminUserService]) -> UserResponse:
user = await service.get_user(user_id=user_id)
return UserResponse.model_validate(user)
Expand All @@ -283,7 +282,6 @@ router = APIRouter(

@router.post(
"/execute",
response_model=ExecutionResponse,
responses={
409: {"model": ErrorResponse, "description": "Duplicate execution"},
429: {"model": ErrorResponse, "description": "Rate limit exceeded"},
Expand All @@ -303,7 +301,7 @@ async def create_execution(
Rules:
- All routes live in `app/api/routes/`, prefixed with `/api/v1` via `settings.API_V1_STR`
- Path ordering: literal paths (`/defaults`) before parameterised ones (`/{user_id}`) in the same router
- Return Pydantic schema models, **never** raw dicts
- Return Pydantic schema models, **never** raw dicts. The return annotation is the response model, so routes set no `response_model=`
- Use `responses={}` to document non-200 outcomes
- Auth: JWT in an httpOnly cookie plus CSRF double-submit (`X-CSRF-Token` header required for POST, PUT, PATCH and DELETE)
- OpenAPI and docs pages are served only when `DEVELOPMENT_MODE` is true
Expand Down
2 changes: 1 addition & 1 deletion backend/app/api/routes/admin/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ async def stream_replay_status(
) -> EventSourceResponse:
"""Stream the status and progress of a replay session via SSE."""
status = await service.get_replay_sse_status(session_id)
statuses = await sse_service.create_replay_stream(status)
statuses = sse_service.create_replay_stream(status)
return EventSourceResponse(
({"data": EventReplayStatusResponse.model_validate(s).model_dump_json()} async for s in statuses), ping=15
)
Expand Down
13 changes: 5 additions & 8 deletions backend/app/api/routes/admin/executions.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,16 @@

@router.get(
"/",
response_model=AdminExecutionListResponse,
responses={403: {"model": ErrorResponse}},
)
async def list_executions(
_: Annotated[UserDocument, Depends(admin_user)],
service: FromDishka[AdminExecutionService],
status: ExecutionStatus | None = Query(None),
priority: QueuePriority | None = Query(None),
user_id: str | None = Query(None),
limit: int = Query(50, ge=1, le=200),
skip: int = Query(0, ge=0),
status: Annotated[ExecutionStatus | None, Query()] = None,
priority: Annotated[QueuePriority | None, Query()] = None,
user_id: Annotated[str | None, Query()] = None,
limit: Annotated[int, Query(ge=1, le=200)] = 50,
skip: Annotated[int, Query(ge=0)] = 0,
) -> AdminExecutionListResponse:
executions, total = await service.list_executions(
status=status,
Expand All @@ -56,7 +55,6 @@ async def list_executions(

@router.put(
"/{execution_id}/priority",
response_model=AdminExecutionResponse,
responses={403: {"model": ErrorResponse}, 404: {"model": ErrorResponse}},
)
async def update_priority(
Expand All @@ -71,7 +69,6 @@ async def update_priority(

@router.get(
"/queue",
response_model=QueueStatusResponse,
responses={403: {"model": ErrorResponse}},
)
async def get_queue_status(
Expand Down
6 changes: 3 additions & 3 deletions backend/app/api/routes/admin/rate_limits.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@
_rate_limit_update = TypeAdapter(UserRateLimitUpdate)


@router.get("/defaults", response_model=list[RateLimitRuleResponse])
@router.get("/defaults")
async def get_default_rate_limit_rules() -> list[RateLimitRuleResponse]:
"""Get the system-level default rate limit rules applied to all users."""
rules = RateLimitConfig.get_default_config().default_rules
return [RateLimitRuleResponse.model_validate(r) for r in rules]


@router.get("/{user_id}", response_model=UserRateLimitsResponse)
@router.get("/{user_id}")
async def get_user_rate_limits(
admin: Annotated[UserDocument, Depends(admin_user)],
admin_user_service: FromDishka[AdminUserService],
Expand All @@ -45,7 +45,7 @@ async def get_user_rate_limits(
return UserRateLimitsResponse.model_validate(result)


@router.put("/{user_id}", response_model=RateLimitUpdateResponse)
@router.put("/{user_id}")
async def update_user_rate_limits(
admin: Annotated[UserDocument, Depends(admin_user)],
admin_user_service: FromDishka[AdminUserService],
Expand Down
3 changes: 0 additions & 3 deletions backend/app/api/routes/admin/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@

@router.get(
"/",
response_model=SystemSettingsSchema,
responses={500: {"model": ErrorResponse, "description": "Failed to load system settings"}},
)
async def get_system_settings(
Expand All @@ -35,7 +34,6 @@ async def get_system_settings(

@router.put(
"/",
response_model=SystemSettingsSchema,
responses={
400: {"model": ErrorResponse, "description": "Invalid settings values"},
422: {"model": ErrorResponse, "description": "Settings validation failed"},
Expand All @@ -55,7 +53,6 @@ async def update_system_settings(

@router.post(
"/reset",
response_model=SystemSettingsSchema,
responses={500: {"model": ErrorResponse, "description": "Failed to reset settings"}},
)
async def reset_system_settings(
Expand Down
9 changes: 1 addition & 8 deletions backend/app/api/routes/admin/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
_user_changes = TypeAdapter(UserChanges)


@router.get("/", response_model=UserListResponse)
@router.get("/")
async def list_users(
admin: Annotated[UserDocument, Depends(admin_user)],
admin_user_service: FromDishka[AdminUserService],
Expand Down Expand Up @@ -64,7 +64,6 @@ async def list_users(

@router.post(
"/",
response_model=UserResponse,
responses={409: {"model": ErrorResponse, "description": "Username already exists"}},
)
async def create_user(
Expand All @@ -86,7 +85,6 @@ async def create_user(

@router.get(
"/{user_id}",
response_model=UserResponse,
responses={404: {"model": ErrorResponse, "description": "User not found"}},
)
async def get_user(
Expand All @@ -101,7 +99,6 @@ async def get_user(

@router.get(
"/{user_id}/overview",
response_model=AdminUserOverview,
responses={404: {"model": ErrorResponse, "description": "User not found"}},
)
async def get_user_overview(
Expand All @@ -116,7 +113,6 @@ async def get_user_overview(

@router.put(
"/{user_id}",
response_model=UserResponse,
responses={
404: {"model": ErrorResponse, "description": "User not found"},
409: {"model": ErrorResponse, "description": "Username or email already exists"},
Expand All @@ -141,7 +137,6 @@ async def update_user(

@router.delete(
"/{user_id}",
response_model=DeleteUserResponse,
responses={
400: {"model": ErrorResponse, "description": "Cannot delete your own account"},
404: {"model": ErrorResponse, "description": "User not found"},
Expand All @@ -160,7 +155,6 @@ async def delete_user(

@router.post(
"/{user_id}/reset-password",
response_model=MessageResponse,
responses={404: {"model": ErrorResponse, "description": "User not found"}},
)
async def reset_user_password(
Expand All @@ -178,7 +172,6 @@ async def reset_user_password(

@router.post(
"/{user_id}/unlock",
response_model=UnlockResponse,
responses={404: {"model": ErrorResponse, "description": "User not found"}},
)
async def unlock_user(
Expand Down
8 changes: 3 additions & 5 deletions backend/app/api/routes/auth.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Annotated

import structlog
from dishka import FromDishka
from dishka.integrations.fastapi import DishkaRoute
Expand All @@ -20,7 +22,6 @@

@router.post(
"/login",
response_model=LoginResponse,
responses={
401: {"model": ErrorResponse, "description": "Invalid username or password"},
403: {"model": ErrorResponse, "description": "Account deactivated"},
Expand All @@ -32,7 +33,7 @@ async def login(
response: Response,
auth_service: FromDishka[AuthService],
logger: FromDishka[structlog.stdlib.BoundLogger],
form_data: OAuth2PasswordRequestForm = Depends(),
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
) -> LoginResponse:
"""Authenticate and receive session cookies."""
logger.info(
Expand Down Expand Up @@ -85,7 +86,6 @@ async def login(

@router.post(
"/register",
response_model=UserResponse,
responses={
409: {"model": ErrorResponse, "description": "Username or email already registered"},
422: {
Expand Down Expand Up @@ -122,7 +122,6 @@ async def register(

@router.get(
"/me",
response_model=UserResponse,
responses={401: {"model": ErrorResponse, "description": "Not authenticated or session invalid"}},
)
async def get_current_user_profile(
Expand Down Expand Up @@ -150,7 +149,6 @@ async def get_current_user_profile(

@router.post(
"/logout",
response_model=MessageResponse,
responses={403: {"model": ErrorResponse, "description": "CSRF token missing or invalid"}},
)
async def logout(
Expand Down
18 changes: 5 additions & 13 deletions backend/app/api/routes/execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
from app.domain.events import DomainEvent
from app.schemas_pydantic.common import ErrorResponse
from app.schemas_pydantic.execution import (
CancelExecutionRequest,
CancelResponse,
DeleteResponse,
ExampleScripts,
Expand Down Expand Up @@ -42,7 +41,6 @@ async def get_execution_with_access(

@router.post(
"/execute",
response_model=ExecutionResponse,
responses={
409: {
"model": ErrorResponse,
Expand Down Expand Up @@ -84,7 +82,6 @@ async def create_execution(

@router.get(
"/executions/{execution_id}/result",
response_model=ExecutionResult,
responses={
403: {"model": ErrorResponse, "description": "Not the owner of this execution"},
404: {"model": ErrorResponse, "description": "Execution not found"},
Expand All @@ -99,7 +96,6 @@ async def get_result(

@router.post(
"/executions/{execution_id}/cancel",
response_model=CancelResponse,
responses={
400: {"model": ErrorResponse, "description": "Execution is in a terminal state"},
403: {"model": ErrorResponse, "description": "Not the owner of this execution"},
Expand All @@ -109,22 +105,19 @@ async def get_result(
async def cancel_execution(
execution: Annotated[ExecutionInDB, Depends(get_execution_with_access)],
current_user: Annotated[UserDocument, Depends(current_user)],
cancel_request: CancelExecutionRequest,
execution_service: FromDishka[ExecutionService],
) -> CancelResponse:
"""Cancel a running or queued execution."""
result = await execution_service.cancel_execution(
execution_id=execution.execution_id,
current_status=execution.status,
user_id=current_user.user_id,
reason=cancel_request.reason,
)
return CancelResponse.model_validate(result)


@router.post(
"/executions/{execution_id}/retry",
response_model=ExecutionResponse,
responses={
400: {"model": ErrorResponse, "description": "Execution is still running or queued"},
403: {"model": ErrorResponse, "description": "Not the owner of this execution"},
Expand All @@ -145,7 +138,6 @@ async def retry_execution(

@router.get(
"/executions/{execution_id}/events",
response_model=list[DomainEvent],
responses={
403: {"model": ErrorResponse, "description": "Not the owner of this execution"},
404: {"model": ErrorResponse, "description": "Execution not found"},
Expand All @@ -164,7 +156,7 @@ async def get_execution_events(
return events


@router.get("/user/executions", response_model=ExecutionListResponse)
@router.get("/user/executions")
async def get_user_executions(
current_user: Annotated[UserDocument, Depends(current_user)],
execution_service: FromDishka[ExecutionService],
Expand Down Expand Up @@ -198,16 +190,16 @@ async def get_user_executions(
)


@router.get("/example-scripts", response_model=ExampleScripts)
@router.get("/example-scripts")
async def get_example_scripts(
execution_service: FromDishka[ExecutionService],
) -> ExampleScripts:
"""Get example scripts for the code editor."""
scripts = await execution_service.get_example_scripts()
scripts = execution_service.get_example_scripts()
return ExampleScripts(scripts=scripts)


@router.get("/k8s-limits", response_model=ResourceLimits)
@router.get("/k8s-limits")
async def get_k8s_resource_limits(
execution_service: FromDishka[ExecutionService],
) -> ResourceLimits:
Expand All @@ -216,7 +208,7 @@ async def get_k8s_resource_limits(
return ResourceLimits.model_validate(limits)


@router.delete("/executions/{execution_id}", response_model=DeleteResponse)
@router.delete("/executions/{execution_id}")
async def delete_execution(
execution_id: str,
admin: Annotated[UserDocument, Depends(admin_user)],
Expand Down
2 changes: 1 addition & 1 deletion backend/app/api/routes/health.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
_START_TIME = time.time()


@router.get("/live", response_model=LivenessResponse)
@router.get("/live")
async def liveness() -> LivenessResponse:
"""Basic liveness probe. Does not touch external deps."""
return LivenessResponse(
Expand Down
6 changes: 3 additions & 3 deletions backend/app/api/routes/notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
router = APIRouter(prefix="/notifications", tags=["notifications"], route_class=DishkaRoute)


@router.get("", response_model=NotificationListResponse)
@router.get("")
async def get_notifications(
user: Annotated[UserDocument, Depends(current_user)],
notification_service: FromDishka[NotificationService],
Expand Down Expand Up @@ -64,7 +64,7 @@ async def mark_all_read(
return Response(status_code=204)


@router.get("/unread-count", response_model=UnreadCountResponse)
@router.get("/unread-count")
async def get_unread_count(
user: Annotated[UserDocument, Depends(current_user)],
notification_service: FromDishka[NotificationService],
Expand All @@ -74,7 +74,7 @@ async def get_unread_count(
return UnreadCountResponse(unread_count=count)


@router.delete("/{notification_id}", response_model=DeleteNotificationResponse)
@router.delete("/{notification_id}")
async def delete_notification(
notification_id: str,
user: Annotated[UserDocument, Depends(current_user)],
Expand Down
Loading
Loading