Initial Aurora Core 0.1 implementation
CI / lint (push) Canceled after 0s
CI / typecheck (push) Canceled after 0s
CI / unit-tests (push) Canceled after 0s
CI / integration-tests (push) Canceled after 0s
CI / container-build (push) Canceled after 0s
CI / dependency-audit (push) Canceled after 0s
CI / lint (push) Canceled after 0s
CI / typecheck (push) Canceled after 0s
CI / unit-tests (push) Canceled after 0s
CI / integration-tests (push) Canceled after 0s
CI / container-build (push) Canceled after 0s
CI / dependency-audit (push) Canceled after 0s
Production-grade media metadata backend per docs/aurora-core-spec.md: Aurora-owned UUIDv7 identity system, normalized movie/TV/people/credits schema, provider abstraction with a TMDb adapter, an idempotent async ingestion pipeline (normalize -> match -> upsert -> provenance), a Postgres FTS/trigram search layer, a stateless FastAPI v1 API with Redis caching/rate-limiting, arq workers, dev/prod Docker Compose topologies, unit + integration + load tests, CI, and architecture/ database/API/ERD documentation. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
"""Orchestrates one ingestion job end-to-end: create/track the `IngestionJob`
|
||||
row, run the provider fetch + normalize + upsert pipeline, and record
|
||||
success/failure. Used by both the admin trigger endpoint (section 17) and
|
||||
the arq worker task (section 15) so the two entry points share identical
|
||||
job-lifecycle and idempotency behavior.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.database import session_scope
|
||||
from app.core.logging import get_logger
|
||||
from app.models.provider import IngestionJob
|
||||
from app.providers.tmdb import TMDbProvider
|
||||
from app.repositories.provider_repository import (
|
||||
create_ingestion_job,
|
||||
get_ingestion_job,
|
||||
get_or_create_provider,
|
||||
mark_job_failed,
|
||||
mark_job_running,
|
||||
mark_job_succeeded,
|
||||
)
|
||||
from app.services.ingestion.movie_ingestion import ingest_movie
|
||||
from app.services.ingestion.person_ingestion import ingest_person
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
async def _require_job(session: AsyncSession, job_id: uuid.UUID) -> IngestionJob:
|
||||
job = await get_ingestion_job(session, job_id)
|
||||
if job is None:
|
||||
# Only possible if the row was deleted out-of-band; the job was
|
||||
# created moments earlier in this same call chain.
|
||||
raise RuntimeError(f"ingestion job {job_id} vanished mid-run")
|
||||
return job
|
||||
|
||||
|
||||
async def _create_job(job_type: str, entity_type: str, external_id: str) -> uuid.UUID:
|
||||
idempotency_key = f"tmdb:{entity_type}:{external_id}"
|
||||
async with session_scope() as session:
|
||||
provider_row = await get_or_create_provider(session, "tmdb", "The Movie Database (TMDb)")
|
||||
job = await create_ingestion_job(
|
||||
session,
|
||||
provider_id=provider_row.id,
|
||||
job_type=job_type,
|
||||
idempotency_key=idempotency_key,
|
||||
entity_type=entity_type,
|
||||
external_id=external_id,
|
||||
)
|
||||
return job.id
|
||||
|
||||
|
||||
async def run_movie_ingestion(external_id: str) -> uuid.UUID:
|
||||
job_id = await _create_job("movie_sync", "movie", external_id)
|
||||
|
||||
async with session_scope() as session:
|
||||
job = await _require_job(session, job_id)
|
||||
await mark_job_running(session, job)
|
||||
|
||||
provider = TMDbProvider()
|
||||
try:
|
||||
async with session_scope() as session:
|
||||
movie_id = await ingest_movie(session, provider, external_id, ingestion_job_id=job_id)
|
||||
async with session_scope() as session:
|
||||
job = await _require_job(session, job_id)
|
||||
await mark_job_succeeded(session, job, movie_id)
|
||||
return movie_id
|
||||
except Exception as exc:
|
||||
logger.error("movie_ingestion_failed", external_id=external_id, error=str(exc))
|
||||
async with session_scope() as session:
|
||||
job = await _require_job(session, job_id)
|
||||
await mark_job_failed(session, job, str(exc))
|
||||
raise
|
||||
finally:
|
||||
await provider.aclose()
|
||||
|
||||
|
||||
async def run_person_ingestion(external_id: str) -> uuid.UUID:
|
||||
job_id = await _create_job("person_sync", "person", external_id)
|
||||
|
||||
async with session_scope() as session:
|
||||
job = await _require_job(session, job_id)
|
||||
await mark_job_running(session, job)
|
||||
|
||||
provider = TMDbProvider()
|
||||
try:
|
||||
async with session_scope() as session:
|
||||
person_id = await ingest_person(session, provider, external_id, ingestion_job_id=job_id)
|
||||
async with session_scope() as session:
|
||||
job = await _require_job(session, job_id)
|
||||
await mark_job_succeeded(session, job, person_id)
|
||||
return person_id
|
||||
except Exception as exc:
|
||||
logger.error("person_ingestion_failed", external_id=external_id, error=str(exc))
|
||||
async with session_scope() as session:
|
||||
job = await _require_job(session, job_id)
|
||||
await mark_job_failed(session, job, str(exc))
|
||||
raise
|
||||
finally:
|
||||
await provider.aclose()
|
||||
Reference in New Issue
Block a user