fix: type admin API contracts
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

This commit is contained in:
ik
2026-09-16 20:09:41 +07:00
parent efd5f7b172
commit c216229c61
5 changed files with 316 additions and 21 deletions
+17 -17
View File
@@ -18,7 +18,7 @@ from ..importer import ImportAlreadyRunning, ImportSourceError, import_records
from ..media_catalog import review_assets, review_file
from ..models import CatchReport, CommunityImportRun, DataSource, ExternalObservation, Fish, ModerationEvent, ModerationStatus, OfficialRecordImport, SourceType, Waterbody
from ..public_cache import public_cache
from ..schemas import AdminCatchReportOut, AdminModerationHistoryOut, CatchReportCreated, ExternalAliasSuggestionOut, ExternalObservationAction, ExternalObservationDecision, ExternalObservationMapping, ExternalObservationOut, ExternalObservationPublished, ImportRunOut, ModerationUpdate
from ..schemas import AdminCatchReportOut, AdminMediaReviewOut, AdminModerationHistoryOut, AdminSourceStatusOut, CatchReportCreated, ExternalAliasSuggestionOut, ExternalObservationAction, ExternalObservationDecision, ExternalObservationMapping, ExternalObservationOut, ExternalObservationPublished, ImportRunOut, ModerationUpdate
from ..storage import delete_screenshot, signed_screenshot_url
from ..time_utils import aware
@@ -30,14 +30,14 @@ def _admin(request: Request, db: Db, authorization: Annotated[str | None, Header
return verify_admin(request, db, authorization, settings)
@router.get("/api/v1/admin/media/catalog")
@router.get("/api/v1/admin/media/catalog", response_model=list[AdminMediaReviewOut])
def admin_media_catalog(
_: Annotated[str, Depends(_admin)],
entity_type: str | None = Query(None, pattern="^(fish|waterbody|tackle|reference)$"),
status: str | None = Query(None, pattern="^(approved|upgrade_queued|upgrade_stored)$"),
limit: int = Query(50, ge=1, le=100),
offset: int = Query(0, ge=0),
) -> list[dict]:
) -> list[AdminMediaReviewOut]:
return review_assets(entity_type, status)[offset:offset + limit]
@@ -152,11 +152,11 @@ def admin_start_official_import(db: Db, _: Annotated[str, Depends(_admin)]) -> O
raise HTTPException(status_code=502, detail=f"official records import failed: {exc}") from exc
@router.get("/api/v1/admin/source-status")
def admin_source_status(db: Db, _: Annotated[str, Depends(_admin)]) -> list[dict[str, object]]:
@router.get("/api/v1/admin/source-status", response_model=list[AdminSourceStatusOut])
def admin_source_status(db: Db, _: Annotated[str, Depends(_admin)]) -> list[AdminSourceStatusOut]:
"""Return safe operational details needed by the owner dashboard."""
now = datetime.now(timezone.utc)
result: list[dict[str, object]] = []
result: list[AdminSourceStatusOut] = []
for source in db.scalars(select(DataSource).order_by(DataSource.name)):
runs = list(db.scalars(
select(CommunityImportRun)
@@ -184,17 +184,17 @@ def admin_source_status(db: Db, _: Annotated[str, Depends(_admin)]) -> list[dict
state = "stale"
else:
state = "healthy"
result.append({
"source_system": source.key,
"name": source.name,
"status": state,
"last_started_at": latest.started_at if latest else None,
"last_success_at": success.started_at if success else None,
"next_allowed_at": next_allowed,
"cooldown_seconds": cooldown_seconds,
"recent_failures_24h": recent_failures,
"backoff_recommended": recent_failures >= 5,
})
result.append(AdminSourceStatusOut(
source_system=source.key,
name=source.name,
status=state,
last_started_at=latest.started_at if latest else None,
last_success_at=success.started_at if success else None,
next_allowed_at=next_allowed,
cooldown_seconds=cooldown_seconds,
recent_failures_24h=recent_failures,
backoff_recommended=recent_failures >= 5,
))
return result
+35
View File
@@ -287,3 +287,38 @@ class SourceStatusOut(BaseModel):
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
source_system: str
source_url: str
duplicate_of: str | None
derivatives: list[AdminMediaDerivativeOut]
+260 -4
View File
@@ -217,6 +217,187 @@
"title": "AdminCatchReportOut",
"type": "object"
},
"AdminMediaDerivativeOut": {
"properties": {
"format": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Format"
},
"height": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"title": "Height"
},
"role": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Role"
},
"width": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"title": "Width"
}
},
"required": [
"role",
"format",
"width",
"height"
],
"title": "AdminMediaDerivativeOut",
"type": "object"
},
"AdminMediaReviewOut": {
"properties": {
"content_type": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Content Type"
},
"derivatives": {
"items": {
"$ref": "#/components/schemas/AdminMediaDerivativeOut"
},
"title": "Derivatives",
"type": "array"
},
"duplicate_of": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Duplicate Of"
},
"entity_key": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Entity Key"
},
"entity_type": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Entity Type"
},
"height": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"title": "Height"
},
"id": {
"title": "Id",
"type": "string"
},
"image_url": {
"title": "Image Url",
"type": "string"
},
"label": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Label"
},
"source_system": {
"title": "Source System",
"type": "string"
},
"source_url": {
"title": "Source Url",
"type": "string"
},
"status": {
"title": "Status",
"type": "string"
},
"width": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"title": "Width"
}
},
"required": [
"id",
"status",
"entity_type",
"entity_key",
"label",
"width",
"height",
"content_type",
"image_url",
"source_system",
"source_url",
"duplicate_of",
"derivatives"
],
"title": "AdminMediaReviewOut",
"type": "object"
},
"AdminModerationHistoryOut": {
"properties": {
"action": {
@@ -276,6 +457,83 @@
"title": "AdminModerationHistoryOut",
"type": "object"
},
"AdminSourceStatusOut": {
"properties": {
"backoff_recommended": {
"title": "Backoff Recommended",
"type": "boolean"
},
"cooldown_seconds": {
"title": "Cooldown Seconds",
"type": "integer"
},
"last_started_at": {
"anyOf": [
{
"format": "date-time",
"type": "string"
},
{
"type": "null"
}
],
"title": "Last Started At"
},
"last_success_at": {
"anyOf": [
{
"format": "date-time",
"type": "string"
},
{
"type": "null"
}
],
"title": "Last Success At"
},
"name": {
"title": "Name",
"type": "string"
},
"next_allowed_at": {
"anyOf": [
{
"format": "date-time",
"type": "string"
},
{
"type": "null"
}
],
"title": "Next Allowed At"
},
"recent_failures_24h": {
"title": "Recent Failures 24H",
"type": "integer"
},
"source_system": {
"title": "Source System",
"type": "string"
},
"status": {
"title": "Status",
"type": "string"
}
},
"required": [
"source_system",
"name",
"status",
"last_started_at",
"last_success_at",
"next_allowed_at",
"cooldown_seconds",
"recent_failures_24h",
"backoff_recommended"
],
"title": "AdminSourceStatusOut",
"type": "object"
},
"BaitOut": {
"properties": {
"id": {
@@ -2942,8 +3200,7 @@
"application/json": {
"schema": {
"items": {
"additionalProperties": true,
"type": "object"
"$ref": "#/components/schemas/AdminMediaReviewOut"
},
"title": "Response Admin Media Catalog Api V1 Admin Media Catalog Get",
"type": "array"
@@ -3124,8 +3381,7 @@
"application/json": {
"schema": {
"items": {
"additionalProperties": true,
"type": "object"
"$ref": "#/components/schemas/AdminSourceStatusOut"
},
"title": "Response Admin Source Status Api V1 Admin Source Status Get",
"type": "array"
+2
View File
@@ -168,6 +168,7 @@ def test_admin_source_status_requires_auth_and_exposes_safe_cooldown_fields() ->
assert response.status_code == 200
assert response.json()
assert all({"status", "cooldown_seconds", "recent_failures_24h", "backoff_recommended"} <= set(item) for item in response.json())
assert all({"source_system", "name", "last_started_at", "last_success_at", "next_allowed_at"} <= set(item) for item in response.json())
assert all("error_summary" not in item and "base_url" not in item for item in response.json())
@@ -178,6 +179,7 @@ def test_admin_media_review_requires_auth() -> None:
assert len(response.json()) <= 2
if response.json():
assert {"status", "width", "height", "source_system", "source_url", "derivatives"} <= set(response.json()[0])
assert all({"role", "format", "width", "height"} <= set(derivative) for derivative in response.json()[0]["derivatives"])
def test_liveness_does_not_probe_dependencies() -> None:
+2
View File
@@ -107,6 +107,8 @@ for (const viewport of [{ name: "desktop", width: 1280, height: 900 }, { name: "
await page.getByRole("button", { name: "Открыть медиатеку" }).click();
await expect(page.locator(".media-library__card")).toContainText("Щука");
await expect(page.locator(".media-library__card")).toContainText("upgrade_stored");
await expect(page.getByRole("navigation", { name: "Разделы админ-панели" })).toBeVisible();
await expect(page.getByRole("link", { name: "Медиатека" })).toHaveAttribute("aria-current", "page");
expect(mutations).toBe(0);
const dimensions = await page.evaluate(() => ({ width: document.documentElement.clientWidth, scroll: document.documentElement.scrollWidth }));
expect(dimensions.scroll).toBeLessThanOrEqual(dimensions.width);