fix: type admin API contracts
This commit is contained in:
@@ -18,7 +18,7 @@ from ..importer import ImportAlreadyRunning, ImportSourceError, import_records
|
|||||||
from ..media_catalog import review_assets, review_file
|
from ..media_catalog import review_assets, review_file
|
||||||
from ..models import CatchReport, CommunityImportRun, DataSource, ExternalObservation, Fish, ModerationEvent, ModerationStatus, OfficialRecordImport, SourceType, Waterbody
|
from ..models import CatchReport, CommunityImportRun, DataSource, ExternalObservation, Fish, ModerationEvent, ModerationStatus, OfficialRecordImport, SourceType, Waterbody
|
||||||
from ..public_cache import public_cache
|
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 ..storage import delete_screenshot, signed_screenshot_url
|
||||||
from ..time_utils import aware
|
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)
|
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(
|
def admin_media_catalog(
|
||||||
_: Annotated[str, Depends(_admin)],
|
_: Annotated[str, Depends(_admin)],
|
||||||
entity_type: str | None = Query(None, pattern="^(fish|waterbody|tackle|reference)$"),
|
entity_type: str | None = Query(None, pattern="^(fish|waterbody|tackle|reference)$"),
|
||||||
status: str | None = Query(None, pattern="^(approved|upgrade_queued|upgrade_stored)$"),
|
status: str | None = Query(None, pattern="^(approved|upgrade_queued|upgrade_stored)$"),
|
||||||
limit: int = Query(50, ge=1, le=100),
|
limit: int = Query(50, ge=1, le=100),
|
||||||
offset: int = Query(0, ge=0),
|
offset: int = Query(0, ge=0),
|
||||||
) -> list[dict]:
|
) -> list[AdminMediaReviewOut]:
|
||||||
return review_assets(entity_type, status)[offset:offset + limit]
|
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
|
raise HTTPException(status_code=502, detail=f"official records import failed: {exc}") from exc
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/v1/admin/source-status")
|
@router.get("/api/v1/admin/source-status", response_model=list[AdminSourceStatusOut])
|
||||||
def admin_source_status(db: Db, _: Annotated[str, Depends(_admin)]) -> list[dict[str, object]]:
|
def admin_source_status(db: Db, _: Annotated[str, Depends(_admin)]) -> list[AdminSourceStatusOut]:
|
||||||
"""Return safe operational details needed by the owner dashboard."""
|
"""Return safe operational details needed by the owner dashboard."""
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
result: list[dict[str, object]] = []
|
result: list[AdminSourceStatusOut] = []
|
||||||
for source in db.scalars(select(DataSource).order_by(DataSource.name)):
|
for source in db.scalars(select(DataSource).order_by(DataSource.name)):
|
||||||
runs = list(db.scalars(
|
runs = list(db.scalars(
|
||||||
select(CommunityImportRun)
|
select(CommunityImportRun)
|
||||||
@@ -184,17 +184,17 @@ def admin_source_status(db: Db, _: Annotated[str, Depends(_admin)]) -> list[dict
|
|||||||
state = "stale"
|
state = "stale"
|
||||||
else:
|
else:
|
||||||
state = "healthy"
|
state = "healthy"
|
||||||
result.append({
|
result.append(AdminSourceStatusOut(
|
||||||
"source_system": source.key,
|
source_system=source.key,
|
||||||
"name": source.name,
|
name=source.name,
|
||||||
"status": state,
|
status=state,
|
||||||
"last_started_at": latest.started_at if latest else None,
|
last_started_at=latest.started_at if latest else None,
|
||||||
"last_success_at": success.started_at if success else None,
|
last_success_at=success.started_at if success else None,
|
||||||
"next_allowed_at": next_allowed,
|
next_allowed_at=next_allowed,
|
||||||
"cooldown_seconds": cooldown_seconds,
|
cooldown_seconds=cooldown_seconds,
|
||||||
"recent_failures_24h": recent_failures,
|
recent_failures_24h=recent_failures,
|
||||||
"backoff_recommended": recent_failures >= 5,
|
backoff_recommended=recent_failures >= 5,
|
||||||
})
|
))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -287,3 +287,38 @@ class SourceStatusOut(BaseModel):
|
|||||||
last_started_at: datetime | None
|
last_started_at: datetime | None
|
||||||
last_success_at: datetime | None
|
last_success_at: datetime | None
|
||||||
observations: int
|
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
@@ -217,6 +217,187 @@
|
|||||||
"title": "AdminCatchReportOut",
|
"title": "AdminCatchReportOut",
|
||||||
"type": "object"
|
"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": {
|
"AdminModerationHistoryOut": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"action": {
|
"action": {
|
||||||
@@ -276,6 +457,83 @@
|
|||||||
"title": "AdminModerationHistoryOut",
|
"title": "AdminModerationHistoryOut",
|
||||||
"type": "object"
|
"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": {
|
"BaitOut": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
@@ -2942,8 +3200,7 @@
|
|||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"items": {
|
"items": {
|
||||||
"additionalProperties": true,
|
"$ref": "#/components/schemas/AdminMediaReviewOut"
|
||||||
"type": "object"
|
|
||||||
},
|
},
|
||||||
"title": "Response Admin Media Catalog Api V1 Admin Media Catalog Get",
|
"title": "Response Admin Media Catalog Api V1 Admin Media Catalog Get",
|
||||||
"type": "array"
|
"type": "array"
|
||||||
@@ -3124,8 +3381,7 @@
|
|||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"items": {
|
"items": {
|
||||||
"additionalProperties": true,
|
"$ref": "#/components/schemas/AdminSourceStatusOut"
|
||||||
"type": "object"
|
|
||||||
},
|
},
|
||||||
"title": "Response Admin Source Status Api V1 Admin Source Status Get",
|
"title": "Response Admin Source Status Api V1 Admin Source Status Get",
|
||||||
"type": "array"
|
"type": "array"
|
||||||
|
|||||||
@@ -168,6 +168,7 @@ def test_admin_source_status_requires_auth_and_exposes_safe_cooldown_fields() ->
|
|||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.json()
|
assert response.json()
|
||||||
assert all({"status", "cooldown_seconds", "recent_failures_24h", "backoff_recommended"} <= set(item) for item in 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())
|
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
|
assert len(response.json()) <= 2
|
||||||
if response.json():
|
if response.json():
|
||||||
assert {"status", "width", "height", "source_system", "source_url", "derivatives"} <= set(response.json()[0])
|
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:
|
def test_liveness_does_not_probe_dependencies() -> None:
|
||||||
|
|||||||
@@ -107,6 +107,8 @@ for (const viewport of [{ name: "desktop", width: 1280, height: 900 }, { name: "
|
|||||||
await page.getByRole("button", { name: "Открыть медиатеку" }).click();
|
await page.getByRole("button", { name: "Открыть медиатеку" }).click();
|
||||||
await expect(page.locator(".media-library__card")).toContainText("Щука");
|
await expect(page.locator(".media-library__card")).toContainText("Щука");
|
||||||
await expect(page.locator(".media-library__card")).toContainText("upgrade_stored");
|
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);
|
expect(mutations).toBe(0);
|
||||||
const dimensions = await page.evaluate(() => ({ width: document.documentElement.clientWidth, scroll: document.documentElement.scrollWidth }));
|
const dimensions = await page.evaluate(() => ({ width: document.documentElement.clientWidth, scroll: document.documentElement.scrollWidth }));
|
||||||
expect(dimensions.scroll).toBeLessThanOrEqual(dimensions.width);
|
expect(dimensions.scroll).toBeLessThanOrEqual(dimensions.width);
|
||||||
|
|||||||
Reference in New Issue
Block a user