US06-02: Transfer, Verify, and Remove Active Sources (#78)

This commit was merged in pull request #78.
This commit is contained in:
2026-08-16 18:47:04 +02:00
parent b4b316acf5
commit b90d1718be
14 changed files with 2167 additions and 24 deletions

View File

@@ -1,10 +1,12 @@
"""Domain job handlers: safety scoring, content analysis, uploads (US02-06, US05-02).
"""Domain job handlers: safety scoring, content analysis, uploads, archive
transfers (US02-06, US05-02, US06-02).
Importing this module registers the ``safety_score``, ``analysis``, and
``upload_batch`` job types so the generic worker can run them per item. Each handler
delegates to its service, which owns the real work and the privacy gate. Handlers
are idempotent: re-scoring or re-analyzing one asset is safe after an interrupted
attempt, and an upload batch refuses to re-run an attempt whose outcome is unknown.
Importing this module registers the ``safety_score``, ``analysis``,
``upload_batch``, and ``archive_plan`` job types so the generic worker can run them
per item. Each handler delegates to its service, which owns the real work and the
privacy gate. Handlers are idempotent: re-scoring or re-analyzing one asset is safe
after an interrupted attempt, an upload batch refuses to re-run an attempt whose
outcome is unknown, and an archive plan skips items it already completed.
Providers/models are the service defaults here (real NsfwModel / vision provider);
tests exercise the services directly with injected fakes rather than the worker.
@@ -17,12 +19,12 @@ from photo_pipeline.jobs.handlers import Cancelled, JobContext, register
SAFETY_SCORE = "safety_score"
ANALYSIS = "analysis"
UPLOAD_BATCH = "upload_batch"
ARCHIVE_PLAN = "archive_plan"
# Both mutate the library's metadata/derived state; one at a time (concept §one job).
LIBRARY_WRITE_LOCK = "library_write"
# The uploader lane: one album batch at a time (concept §16).
UPLOAD_LOCK = "upload"
# The archiver lane: one archive/restore plan at a time (concept §16). No handler
# runs on it yet (US06-02); preflight already refuses to plan around a held lease.
# The archiver lane: one archive/restore plan at a time (concept §16).
ARCHIVE_LOCK = "archive"
@@ -54,6 +56,21 @@ def _upload_batch_item(batch_id: str, ctx: JobContext) -> None:
raise RuntimeError(f"upload batch {batch_id} is {batch['state']}: {batch['error_code']}")
def _archive_plan_item(plan_id: str, ctx: JobContext) -> None:
"""One item = one archive plan. Item-level failures stay in the journal (the
source is then still there); only an unusable plan fails the job."""
from photo_pipeline.config import Config
from photo_pipeline.services.archive_transfer import ArchiveTransferService
config = ctx.config if ctx.config is not None else Config.from_env()
result = ArchiveTransferService(ctx.session_factory, config=config).apply(
plan_id, worker_id=ctx.worker_id
)
if result["failed"]:
raise RuntimeError(f"archive plan {plan_id}: {result['failed']} item(s) failed")
register(SAFETY_SCORE, _safety_score_item)
register(ANALYSIS, _analysis_item)
register(UPLOAD_BATCH, _upload_batch_item)
register(ARCHIVE_PLAN, _archive_plan_item)