Files
ik 722c88d436
CI / backend-and-migrations (push) Canceled after 0s
CI / astro-build (push) Canceled after 0s
CI / dependency-audit (push) Canceled after 0s
CI / compose-e2e (push) Canceled after 0s
sync
2026-09-17 07:35:04 +07:00

349 lines
8.2 KiB
Python

from __future__ import annotations
from datetime import datetime
from uuid import UUID
from pydantic import BaseModel, ConfigDict, Field, field_validator
class FishOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: UUID
slug: str
name_ru: str
trophy_weight_g: int | None
class WaterbodyOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: UUID
slug: str
name_ru: str
unlock_level: int | None
fish_species_count: int | None
source_system: str | None
source_external_id: str | None
source_url: str | None
description: str | None
source_aliases: list[str] | None
source_fish_species: list[str] | None
source_image_urls: list[str] | None
source_point_urls: list[str] | None
source_checked_at: datetime | None
class BaitOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: UUID
name: str
normalized_name: str
kind: str
class ActivityOut(BaseModel):
spot_id: UUID
waterbody_slug: str
waterbody: str
fish_slug: str
fish: str
x: int
y: int
best_bait: str | None
catches: int
unique_players: int
average_weight_g: int
max_weight_g: int
last_confirmed_at: datetime
activity_score: int
confidence_score: int
explanation: str
sources: list[str]
coordinate_precision: str
coordinate_sources: list[str]
class PaginatedActivityOut(BaseModel):
items: list[ActivityOut]
total: int
limit: int
offset: int
class CatchOut(BaseModel):
id: UUID
fish: str
weight_g: int
bait: str | None
player_name: str | None
caught_at: datetime | None
reported_at: datetime
retrieve_method: str | None
retrieve_speed: int | None
source_system: str
source_url: str | None
class SpotOut(BaseModel):
id: UUID
waterbody_slug: str
waterbody: str
x: int
y: int
description: str | None
catches_24h: int
catches_3d: int
catches_7d: int
top_baits: list[str]
coordinate_precision: str
coordinate_sources: list[str]
class OfficialRecordOut(BaseModel):
id: UUID
fish: str
weight_g: int
waterbody: str
bait: str | None
player_name: str | None
record_date: datetime | None
category: str | None
region: str | None
source_url: str | None
source_system: str = "rf4-official"
class PaginatedOfficialRecordOut(BaseModel):
items: list[OfficialRecordOut]
total: int
limit: int
offset: int
class PublicObservationOut(BaseModel):
id: UUID
source_system: str
source_name: str
source_url: str
fish_name: str
waterbody_name: str
x: int | None
y: int | None
weight_g: int | None
last_seen_at: datetime
missing_fields: list[str]
quality: str
class ImportRunOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: UUID
started_at: datetime
finished_at: datetime | None
status: str
source_url: str
rows_seen: int
rows_created: int
rows_updated: int
error_summary: str | None
response_status: int | None
response_etag: str | None
response_last_modified: str | None
response_content_type: str | None
response_bytes: int | None
not_modified: bool
class ImportRunPublicOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: UUID
started_at: datetime
finished_at: datetime | None
status: str
rows_seen: int
rows_created: int
rows_updated: int
not_modified: bool
class CatchReportCreate(BaseModel):
fish_slug: str
waterbody_slug: str
x: int = Field(ge=-10_000, le=10_000)
y: int = Field(ge=-10_000, le=10_000)
weight_g: int = Field(gt=0, le=3_000_000)
bait_name: str | None = Field(default=None, max_length=200)
fishing_method: str | None = Field(default=None, max_length=50)
rig_type: str | None = Field(default=None, max_length=100)
retrieve_method: str | None = Field(default=None, max_length=100)
retrieve_speed: int | None = Field(default=None, ge=0, le=100)
caught_at: datetime | None = None
player_name: str | None = Field(default=None, max_length=100)
source_url: str | None = Field(default=None, max_length=1000)
comment: str | None = Field(default=None, max_length=1000)
website: str = Field(default="", max_length=200)
@field_validator("fish_slug", "waterbody_slug")
@classmethod
def nonempty_slug(cls, value: str) -> str:
value = value.strip()
if not value:
raise ValueError("must not be empty")
return value
class CatchReportCreated(BaseModel):
id: UUID
moderation_status: str
class CatchReportAccepted(CatchReportCreated):
screenshot_upload_token: str
idempotent: bool = False
class AdminCatchReportOut(BaseModel):
id: UUID
fish: str
waterbody: str
coordinates: str
weight_g: int
bait: str | None
player_name: str | None
reported_at: datetime
moderation_status: str
comment: str | None
screenshot_url: str | None
moderation_version: int
class ModerationUpdate(BaseModel):
status: str
reason: str | None = Field(default=None, max_length=1000)
expected_version: int = Field(ge=0)
@field_validator("status")
@classmethod
def known_status(cls, value: str) -> str:
if value not in {"approved", "rejected", "pending"}:
raise ValueError("unknown moderation status")
return value
class ExternalObservationOut(BaseModel):
id: UUID
source_system: str
source_external_id: str
source_url: str
fish_name: str
fish_external_id: str | None
waterbody_name: str
waterbody_external_id: str | None
x: int | None
y: int | None
weight_g: int | None
published_at: datetime | None
first_seen_at: datetime
last_seen_at: datetime
reviewed_at: datetime | None
status: str
fish_slug: str | None
waterbody_slug: str | None
catch_report_id: UUID | None
review_note: str | None
missing_fields: list[str]
source_payload: dict[str, str | int | float | bool | None]
moderation_version: int
source_check_status: str | None
source_checked_at: datetime | None
class ExternalObservationMapping(BaseModel):
fish_slug: str
waterbody_slug: str
note: str | None = Field(default=None, max_length=1000)
expected_version: int = Field(ge=0)
class ExternalAliasSuggestionOut(BaseModel):
fish_slug: str | None
waterbody_slug: str | None
class AdminModerationHistoryOut(BaseModel):
entity_type: str
entity_id: UUID
decided_at: datetime
action: str
moderator: str | None
reason: str | None
requires_confirmation: bool = True
class ExternalObservationDecision(BaseModel):
reason: str = Field(min_length=1, max_length=1000)
expected_version: int = Field(ge=0)
class ExternalObservationAction(BaseModel):
expected_version: int = Field(ge=0)
class ExternalObservationPublished(BaseModel):
observation_id: UUID
catch_report_id: UUID
status: str
class SourceStatusOut(BaseModel):
source_system: str
name: str
status: str
last_started_at: datetime | None
last_success_at: datetime | None
observations: int
class AdminSourceStatusOut(BaseModel):
source_system: str
name: str
status: str
last_started_at: datetime | None
last_success_at: datetime | None
next_allowed_at: datetime | None
cooldown_seconds: int
recent_failures_24h: int
backoff_recommended: bool
class AdminMediaDerivativeOut(BaseModel):
role: str | None
format: str | None
width: int | None
height: int | None
class AdminMediaReviewOut(BaseModel):
id: str
status: str
entity_type: str | None
entity_key: str | None
label: str | None
width: int | None
height: int | None
content_type: str | None
image_url: str
asset_url: str
source_system: str
source_url: str
duplicate_of: str | None
supersedes: str | None
derivatives: list[AdminMediaDerivativeOut]
class AdminMediaDecision(BaseModel):
note: str = Field(min_length=1, max_length=1000)
class AdminMediaRollback(AdminMediaDecision):
asset_url: str = Field(min_length=1, max_length=2000)