94 lines
4.1 KiB
Python
94 lines
4.1 KiB
Python
"""Domain job handlers: safety scoring, content analysis, uploads, archive
|
|
transfers, restores (US02-06, US05-02, US06-02, US06-04).
|
|
|
|
Importing this module registers the ``safety_score``, ``analysis``,
|
|
``upload_batch``, ``archive_plan``, and ``restore_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.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from photo_pipeline.jobs.handlers import Cancelled, JobContext, register
|
|
|
|
SAFETY_SCORE = "safety_score"
|
|
ANALYSIS = "analysis"
|
|
UPLOAD_BATCH = "upload_batch"
|
|
ARCHIVE_PLAN = "archive_plan"
|
|
RESTORE_PLAN = "restore_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).
|
|
ARCHIVE_LOCK = "archive"
|
|
|
|
|
|
def _safety_score_item(asset_id: str, ctx: JobContext) -> None:
|
|
from photo_pipeline.services.safety import SafetyService
|
|
|
|
SafetyService(ctx.session_factory).score_assets([asset_id])
|
|
|
|
|
|
def _analysis_item(asset_id: str, ctx: JobContext) -> None:
|
|
from photo_pipeline.services.analysis import AnalysisService
|
|
|
|
roots = tuple(getattr(ctx.config, "library_roots", ()) or ())
|
|
AnalysisService(ctx.session_factory, library_roots=roots).run([asset_id])
|
|
|
|
|
|
def _upload_batch_item(batch_id: str, ctx: JobContext) -> None:
|
|
"""One item = one album batch. The upload itself is long and external, so the
|
|
handler hands the job's cancellation check to the service, which stops the
|
|
uploader and leaves a resumable batch."""
|
|
from photo_pipeline.config import Config
|
|
from photo_pipeline.services.upload_batches import BatchState, UploadBatchService
|
|
|
|
config = ctx.config if ctx.config is not None else Config.from_env()
|
|
service = UploadBatchService(ctx.session_factory, config=config)
|
|
batch = service.run(batch_id, worker_id=ctx.worker_id, cancelled=ctx.cancelled)
|
|
if batch["state"] == BatchState.CANCELLED:
|
|
raise Cancelled(f"upload batch {batch_id} was cancelled")
|
|
if batch["state"] in (BatchState.FAILED, BatchState.UNKNOWN):
|
|
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")
|
|
|
|
|
|
def _restore_plan_item(plan_id: str, ctx: JobContext) -> None:
|
|
"""One item = one restore plan. A restore removes nothing, so an item failure
|
|
simply leaves that asset archived (US06-04)."""
|
|
from photo_pipeline.config import Config
|
|
from photo_pipeline.services.restores import RestoreService
|
|
|
|
config = ctx.config if ctx.config is not None else Config.from_env()
|
|
result = RestoreService(ctx.session_factory, config=config).apply(
|
|
plan_id, worker_id=ctx.worker_id
|
|
)
|
|
if result["failed"]:
|
|
raise RuntimeError(f"restore 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)
|
|
register(RESTORE_PLAN, _restore_plan_item)
|