feat(provenance): ProvenanceService.for_image (peer provenance rows)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
"""Read-only provenance queries.
|
||||
|
||||
Provenance is its own system, intentionally separate from the tag/ML
|
||||
system (see project_provenance_separation). This service joins
|
||||
ImageProvenance -> Post/Source/Artist and returns plain dicts. It never
|
||||
mutates and never imports tag/ML modules.
|
||||
"""
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from ..models import Artist, ImageProvenance, ImageRecord, Post, Source
|
||||
from ..utils.html_sanitize import sanitize_post_html
|
||||
|
||||
|
||||
def _post_dict(p: Post) -> dict:
|
||||
return {
|
||||
"id": p.id,
|
||||
"external_post_id": p.external_post_id,
|
||||
"url": p.post_url,
|
||||
"title": p.post_title,
|
||||
"date": p.post_date.isoformat() if p.post_date else None,
|
||||
"description_html": sanitize_post_html(p.description),
|
||||
"attachment_count": p.attachment_count,
|
||||
}
|
||||
|
||||
|
||||
def _source_dict(s: Source) -> dict:
|
||||
return {"id": s.id, "platform": s.platform, "url": s.url}
|
||||
|
||||
|
||||
def _artist_dict(a: Artist) -> dict:
|
||||
return {"id": a.id, "name": a.name, "slug": a.slug}
|
||||
|
||||
|
||||
class ProvenanceService:
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
|
||||
async def for_image(self, image_id: int) -> dict | None:
|
||||
rec = await self.session.get(ImageRecord, image_id)
|
||||
if rec is None:
|
||||
return None
|
||||
stmt = (
|
||||
select(ImageProvenance, Post, Source, Artist)
|
||||
.join(Post, Post.id == ImageProvenance.post_id)
|
||||
.join(Source, Source.id == ImageProvenance.source_id)
|
||||
.join(Artist, Artist.id == Source.artist_id)
|
||||
.where(ImageProvenance.image_record_id == image_id)
|
||||
.order_by(ImageProvenance.captured_at.asc(),
|
||||
ImageProvenance.id.asc())
|
||||
)
|
||||
rows = (await self.session.execute(stmt)).all()
|
||||
return {
|
||||
"image_id": image_id,
|
||||
"provenance": [
|
||||
{
|
||||
"provenance_id": ip.id,
|
||||
"captured_at": ip.captured_at.isoformat()
|
||||
if ip.captured_at else None,
|
||||
"post": _post_dict(post),
|
||||
"source": _source_dict(src),
|
||||
"artist": _artist_dict(art),
|
||||
}
|
||||
for ip, post, src, art in rows
|
||||
],
|
||||
}
|
||||
Reference in New Issue
Block a user