feat(api): Pydantic schemas + Data Repositories
This commit is contained in:
50
app/schemas/__init__.py
Normal file
50
app/schemas/__init__.py
Normal 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
42
app/schemas/auth.py
Normal 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
20
app/schemas/common.py
Normal 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
57
app/schemas/incident.py
Normal 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
69
app/schemas/org.py
Normal 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
|
||||
Reference in New Issue
Block a user