feat(api): Pydantic schemas + Data Repositories

This commit is contained in:
2025-12-07 12:00:00 +00:00
parent 359291eec7
commit 3170f10e86
23 changed files with 3549 additions and 3 deletions

50
app/schemas/__init__.py Normal file
View File

@@ -0,0 +1,50 @@
"""Pydantic schemas for request/response models."""
from app.schemas.auth import (
LoginRequest,
RefreshRequest,
RegisterRequest,
SwitchOrgRequest,
TokenResponse,
)
from app.schemas.common import CursorParams, PaginatedResponse
from app.schemas.incident import (
CommentRequest,
IncidentCreate,
IncidentEventResponse,
IncidentResponse,
TransitionRequest,
)
from app.schemas.org import (
MemberResponse,
NotificationTargetCreate,
NotificationTargetResponse,
OrgResponse,
ServiceCreate,
ServiceResponse,
)
__all__ = [
# Auth
"LoginRequest",
"RefreshRequest",
"RegisterRequest",
"SwitchOrgRequest",
"TokenResponse",
# Common
"CursorParams",
"PaginatedResponse",
# Incident
"CommentRequest",
"IncidentCreate",
"IncidentEventResponse",
"IncidentResponse",
"TransitionRequest",
# Org
"MemberResponse",
"NotificationTargetCreate",
"NotificationTargetResponse",
"OrgResponse",
"ServiceCreate",
"ServiceResponse",
]

42
app/schemas/auth.py Normal file
View File

@@ -0,0 +1,42 @@
"""Authentication schemas."""
from uuid import UUID
from pydantic import BaseModel, EmailStr, Field
class RegisterRequest(BaseModel):
"""Request body for user registration."""
email: EmailStr
password: str = Field(min_length=8, max_length=128)
org_name: str = Field(min_length=1, max_length=100, description="Name for the default org")
class LoginRequest(BaseModel):
"""Request body for user login."""
email: EmailStr
password: str
class RefreshRequest(BaseModel):
"""Request body for token refresh."""
refresh_token: str
class SwitchOrgRequest(BaseModel):
"""Request body for switching active organization."""
org_id: UUID
refresh_token: str
class TokenResponse(BaseModel):
"""Response containing access and refresh tokens."""
access_token: str
refresh_token: str
token_type: str = "bearer"
expires_in: int = Field(description="Access token expiry in seconds")

20
app/schemas/common.py Normal file
View File

@@ -0,0 +1,20 @@
"""Common schemas used across the API."""
from pydantic import BaseModel, Field
class CursorParams(BaseModel):
"""Pagination parameters using cursor-based pagination."""
cursor: str | None = Field(default=None, description="Cursor for pagination")
limit: int = Field(default=20, ge=1, le=100, description="Number of items per page")
class PaginatedResponse[T](BaseModel):
"""Generic paginated response wrapper."""
items: list[T]
next_cursor: str | None = Field(
default=None, description="Cursor for next page, null if no more items"
)
has_more: bool = Field(description="Whether there are more items")

57
app/schemas/incident.py Normal file
View File

@@ -0,0 +1,57 @@
"""Incident-related schemas."""
from datetime import datetime
from typing import Any, Literal
from uuid import UUID
from pydantic import BaseModel, Field
IncidentStatus = Literal["triggered", "acknowledged", "mitigated", "resolved"]
IncidentSeverity = Literal["critical", "high", "medium", "low"]
class IncidentCreate(BaseModel):
"""Request body for creating an incident."""
title: str = Field(min_length=1, max_length=200)
description: str | None = Field(default=None, max_length=5000)
severity: IncidentSeverity = "medium"
class IncidentResponse(BaseModel):
"""Incident response."""
id: UUID
service_id: UUID
title: str
description: str | None
status: IncidentStatus
severity: IncidentSeverity
version: int
created_at: datetime
updated_at: datetime
class IncidentEventResponse(BaseModel):
"""Incident event response."""
id: UUID
incident_id: UUID
event_type: str
actor_user_id: UUID | None
payload: dict[str, Any] | None
created_at: datetime
class TransitionRequest(BaseModel):
"""Request body for transitioning incident status."""
to_status: IncidentStatus
version: int = Field(description="Current version for optimistic locking")
note: str | None = Field(default=None, max_length=1000)
class CommentRequest(BaseModel):
"""Request body for adding a comment to an incident."""
content: str = Field(min_length=1, max_length=5000)

69
app/schemas/org.py Normal file
View File

@@ -0,0 +1,69 @@
"""Organization-related schemas."""
from datetime import datetime
from typing import Literal
from uuid import UUID
from pydantic import BaseModel, Field, HttpUrl
class OrgResponse(BaseModel):
"""Organization summary response."""
id: UUID
name: str
slug: str
created_at: datetime
class MemberResponse(BaseModel):
"""Organization member response."""
id: UUID
user_id: UUID
email: str
role: Literal["admin", "member", "viewer"]
created_at: datetime
class ServiceCreate(BaseModel):
"""Request body for creating a service."""
name: str = Field(min_length=1, max_length=100)
slug: str = Field(
min_length=1,
max_length=50,
pattern=r"^[a-z0-9]+(?:-[a-z0-9]+)*$",
description="URL-friendly identifier (lowercase, hyphens allowed)",
)
class ServiceResponse(BaseModel):
"""Service response."""
id: UUID
name: str
slug: str
created_at: datetime
class NotificationTargetCreate(BaseModel):
"""Request body for creating a notification target."""
name: str = Field(min_length=1, max_length=100)
target_type: Literal["webhook", "email", "slack"]
webhook_url: HttpUrl | None = Field(
default=None, description="Required for webhook type"
)
enabled: bool = True
class NotificationTargetResponse(BaseModel):
"""Notification target response."""
id: UUID
name: str
target_type: Literal["webhook", "email", "slack"]
webhook_url: str | None
enabled: bool
created_at: datetime