import uuid
from datetime import datetime
from pydantic import BaseModel, ConfigDict, EmailStr, Field
# ---------- auth ----------
class RegisterIn(BaseModel):
nickname: str = Field(min_length=1, max_length=80)
email: EmailStr
password: str = Field(min_length=8, max_length=128)
class LoginIn(BaseModel):
email: EmailStr
password: str = Field(min_length=1, max_length=128)
class TokenOut(BaseModel):
token: str
expires_at: datetime
class UserOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: uuid.UUID
email: str
nickname: str
created_at: datetime
# ---------- projects ----------
class ProjectCreateIn(BaseModel):
name: str = Field(min_length=1, max_length=120)
description: str | None = Field(default=None, max_length=2000)
class ProjectUpdateIn(BaseModel):
name: str | None = Field(default=None, min_length=1, max_length=120)
description: str | None = Field(default=None, max_length=2000)
class ProjectOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: uuid.UUID
name: str
description: str | None
share_token: str
created_at: datetime
class ProjectReportCounts(BaseModel):
total: int
open: int
class ProjectDetailOut(ProjectOut):
owner_nickname: str
report_counts: ProjectReportCounts
# ---------- reports ----------
class ElementContextIn(BaseModel):
selector: str | None = None
unique_selector: str | None = None
tag: str | None = None
id: str | None = None
classes: list[str] | None = None
text_snippet: str | None = None
aria_label: str | None = None
test_attributes: dict[str, str] | None = None
rect: dict | None = None # {x, y, w, h} viewport px
screenshot_rel: dict | None = None # {x, y, w, h} fractions of the screenshot
extra: dict | None = None
class EnvironmentIn(BaseModel):
user_agent: str | None = None
browser: str | None = None
browser_version: str | None = None
os: str | None = None
viewport: dict | None = None # {w, h}
dpr: float | None = None
language: str | None = None
captured_at: datetime | None = None
extra: dict | None = None
class StepIn(BaseModel):
type: str = Field(pattern="^(click|input|url_change|navigation|note|screenshot)$")
offset_ms: int = Field(ge=0)
data: dict = {}
attachment_id: uuid.UUID | None = None # uploaded screenshot to link to this step
class ReportCreateIn(BaseModel):
project_id: uuid.UUID
type: str = Field(pattern="^(element_note|recording)$")
title: str = Field(min_length=1, max_length=200)
description: str | None = Field(default=None, max_length=10000)
page_url: str | None = Field(default=None, max_length=2048)
page_title: str | None = Field(default=None, max_length=512)
environment: EnvironmentIn | None = None
element: ElementContextIn | None = None
attachment_ids: list[uuid.UUID] | None = None # uploaded files to attach
annotation_shapes: dict[uuid.UUID, list] | None = None # attachment_id -> vector shapes
steps: list[StepIn] | None = None
class ReportUpdateIn(BaseModel):
title: str | None = Field(default=None, min_length=1, max_length=200)
description: str | None = Field(default=None, max_length=10000)
status: str | None = Field(default=None, pattern="^(open|fixed|wont_fix)$")
class AuthorOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: uuid.UUID
nickname: str
class AttachmentOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: uuid.UUID
file_id: uuid.UUID
kind: str
filename: str
mime: str
size: int
annotation_shapes: list | None
class StepOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: uuid.UUID
step_index: int
type: str
offset_ms: int
data: dict
screenshot_attachment_id: uuid.UUID | None
class ReportOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: uuid.UUID
type: str
title: str
description: str | None
status: str
page_url: str | None
page_title: str | None
environment: dict
element: dict | None
share_token: str
created_at: datetime
updated_at: datetime
class ReportListItemOut(ReportOut):
author: AuthorOut
class ReportDetailOut(ReportOut):
author: AuthorOut
attachments: list[AttachmentOut]
steps: list[StepOut]
class ReportListOut(BaseModel):
items: list[ReportListItemOut]
total: int
limit: int
offset: int
# ---------- uploads ----------
class UploadOut(BaseModel):
file_id: uuid.UUID
mime: str
size: int
filename: str