feat(provenance): /api/provenance image + post endpoints
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,7 @@ def all_blueprints() -> list[Blueprint]:
|
|||||||
from .gallery import gallery_bp
|
from .gallery import gallery_bp
|
||||||
from .import_admin import import_admin_bp
|
from .import_admin import import_admin_bp
|
||||||
from .ml_admin import ml_admin_bp
|
from .ml_admin import ml_admin_bp
|
||||||
|
from .provenance import provenance_bp
|
||||||
from .settings import settings_bp
|
from .settings import settings_bp
|
||||||
from .showcase import showcase_bp
|
from .showcase import showcase_bp
|
||||||
from .suggestions import suggestions_bp
|
from .suggestions import suggestions_bp
|
||||||
@@ -27,6 +28,7 @@ def all_blueprints() -> list[Blueprint]:
|
|||||||
return [
|
return [
|
||||||
api_bp,
|
api_bp,
|
||||||
gallery_bp,
|
gallery_bp,
|
||||||
|
provenance_bp,
|
||||||
tags_bp,
|
tags_bp,
|
||||||
artist_bp,
|
artist_bp,
|
||||||
showcase_bp,
|
showcase_bp,
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
"""Provenance API: image -> posts, post -> header payload.
|
||||||
|
|
||||||
|
Separate from the gallery/tag APIs by design (provenance is its own
|
||||||
|
system). Read-only.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from quart import Blueprint, jsonify
|
||||||
|
|
||||||
|
from ..extensions import get_session
|
||||||
|
from ..services.provenance_service import ProvenanceService
|
||||||
|
|
||||||
|
provenance_bp = Blueprint("provenance", __name__,
|
||||||
|
url_prefix="/api/provenance")
|
||||||
|
|
||||||
|
|
||||||
|
@provenance_bp.route("/image/<int:image_id>", methods=["GET"])
|
||||||
|
async def image_provenance(image_id: int):
|
||||||
|
async with get_session() as session:
|
||||||
|
svc = ProvenanceService(session)
|
||||||
|
payload = await svc.for_image(image_id)
|
||||||
|
if payload is None:
|
||||||
|
return jsonify({"error": "not found"}), 404
|
||||||
|
return jsonify(payload)
|
||||||
|
|
||||||
|
|
||||||
|
@provenance_bp.route("/post/<int:post_id>", methods=["GET"])
|
||||||
|
async def post_provenance(post_id: int):
|
||||||
|
async with get_session() as session:
|
||||||
|
svc = ProvenanceService(session)
|
||||||
|
payload = await svc.for_post(post_id)
|
||||||
|
if payload is None:
|
||||||
|
return jsonify({"error": "not found"}), 404
|
||||||
|
return jsonify(payload)
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from backend.app import create_app
|
||||||
|
from backend.app.models import (
|
||||||
|
Artist,
|
||||||
|
ImageProvenance,
|
||||||
|
ImageRecord,
|
||||||
|
Post,
|
||||||
|
Source,
|
||||||
|
)
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.integration
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def app():
|
||||||
|
return create_app()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def client(app):
|
||||||
|
async with app.test_client() as c:
|
||||||
|
yield c
|
||||||
|
|
||||||
|
|
||||||
|
async def _seed_full(db):
|
||||||
|
rec = ImageRecord(
|
||||||
|
path="/images/p/1.jpg", sha256="p" + "0" * 63,
|
||||||
|
size_bytes=1, mime="image/jpeg", width=1, height=1,
|
||||||
|
origin="imported_filesystem", integrity_status="unknown",
|
||||||
|
)
|
||||||
|
artist = Artist(name="Alice", slug="alice")
|
||||||
|
db.add_all([rec, artist])
|
||||||
|
await db.flush()
|
||||||
|
source = Source(artist_id=artist.id, platform="patreon",
|
||||||
|
url="https://patreon.test/alice")
|
||||||
|
db.add(source)
|
||||||
|
await db.flush()
|
||||||
|
post = Post(source_id=source.id, external_post_id="555",
|
||||||
|
post_url="https://patreon.test/p/555", post_title="Set 1",
|
||||||
|
post_date=datetime(2023, 8, 1, tzinfo=UTC),
|
||||||
|
description="<p>hi</p>", attachment_count=2)
|
||||||
|
db.add(post)
|
||||||
|
await db.flush()
|
||||||
|
db.add(ImageProvenance(image_record_id=rec.id, post_id=post.id,
|
||||||
|
source_id=source.id))
|
||||||
|
await db.flush()
|
||||||
|
await db.commit()
|
||||||
|
return rec.id, post.id
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_image_provenance_endpoint(client, db):
|
||||||
|
image_id, post_id = await _seed_full(db)
|
||||||
|
resp = await client.get(f"/api/provenance/image/{image_id}")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
body = await resp.get_json()
|
||||||
|
assert body["image_id"] == image_id
|
||||||
|
assert len(body["provenance"]) == 1
|
||||||
|
assert body["provenance"][0]["post"]["id"] == post_id
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_image_provenance_404(client):
|
||||||
|
resp = await client.get("/api/provenance/image/999999")
|
||||||
|
assert resp.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_post_provenance_endpoint(client, db):
|
||||||
|
_, post_id = await _seed_full(db)
|
||||||
|
resp = await client.get(f"/api/provenance/post/{post_id}")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
body = await resp.get_json()
|
||||||
|
assert body["post"]["id"] == post_id
|
||||||
|
assert body["artist"]["slug"] == "alice"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_post_provenance_404(client):
|
||||||
|
resp = await client.get("/api/provenance/post/999999")
|
||||||
|
assert resp.status_code == 404
|
||||||
Reference in New Issue
Block a user