413 lines
9.5 KiB
Python
413 lines
9.5 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 TackleItemOut(BaseModel):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
id: UUID
|
|
name: str
|
|
category: str
|
|
subcategory: str | None
|
|
brand: str | None
|
|
family: str | None
|
|
unlock_level: int | None
|
|
source_system: str | None
|
|
source_external_id: str | None
|
|
source_url: str | None
|
|
source_checked_at: datetime | None
|
|
missing_fields: list[str] = Field(default_factory=list)
|
|
|
|
|
|
class PaginatedTackleItemOut(BaseModel):
|
|
items: list[TackleItemOut]
|
|
total: int
|
|
limit: int
|
|
offset: int
|
|
|
|
|
|
class RigComponentOut(BaseModel):
|
|
id: UUID
|
|
role: str
|
|
position: int
|
|
raw_value: str | None
|
|
tackle_item_id: UUID | None
|
|
|
|
|
|
class RigOut(BaseModel):
|
|
id: UUID
|
|
name: str
|
|
source_system: str | None
|
|
source_external_id: str | None
|
|
source_url: str | None
|
|
source_checked_at: datetime | None
|
|
missing_fields: list[str] = Field(default_factory=list)
|
|
components: list[RigComponentOut]
|
|
|
|
|
|
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 TackleCombinationOut(BaseModel):
|
|
role: str
|
|
value: str
|
|
catches: int
|
|
unique_players: int
|
|
last_seen_at: datetime
|
|
status: str
|
|
explanation: str
|
|
|
|
|
|
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
|
|
tackle_components: list["CatchTackleComponentOut"]
|
|
|
|
|
|
class CatchTackleComponentOut(BaseModel):
|
|
id: UUID
|
|
role: str
|
|
position: int
|
|
raw_value: str
|
|
tackle_item_id: UUID | None
|
|
rig_id: UUID | None
|
|
source_system: str | None
|
|
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)
|