US04-06: Automate Phase D End-to-End Acceptance (#70)
This commit was merged in pull request #70.
This commit is contained in:
@@ -14,7 +14,9 @@ import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
@@ -159,3 +161,88 @@ def wait_until(predicate, *, timeout: float = 20, interval: float = 0.1):
|
||||
return value
|
||||
time.sleep(interval)
|
||||
raise AssertionError("condition not met before timeout")
|
||||
|
||||
|
||||
# ── Phase D: an analysed album, ready to be named and renamed ────────────────
|
||||
|
||||
NOW = datetime(2026, 1, 1, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
class session_factory:
|
||||
"""Session factory against a seeded database, for the few things a test has to
|
||||
set up or inspect below the API — journal states, mainly."""
|
||||
|
||||
def __init__(self, seeded: Seeded) -> None:
|
||||
self._seeded = seeded
|
||||
|
||||
def __enter__(self):
|
||||
from photo_pipeline.config import Config
|
||||
from photo_pipeline.db import create_db_engine, create_session_factory, run_migrations
|
||||
|
||||
config = Config.from_env(
|
||||
{
|
||||
"PHOTO_PIPELINE_DATA_DIR": str(self._seeded.data),
|
||||
"PHOTO_PIPELINE_LIBRARY_ROOTS": str(self._seeded.lib),
|
||||
}
|
||||
)
|
||||
run_migrations(config.database_url)
|
||||
self._engine = create_db_engine(config.database_url)
|
||||
return create_session_factory(self._engine)
|
||||
|
||||
def __exit__(self, *_):
|
||||
self._engine.dispose()
|
||||
return False
|
||||
|
||||
|
||||
def seed_album(tmp_path: Path, album: str = "rome", names: tuple[str, ...] = ("a.jpg", "b.jpg")):
|
||||
"""A library holding one album folder whose photos are confirmed SFW and analysed
|
||||
— the state a naming proposal, and therefore a rename plan, is built from."""
|
||||
from sqlalchemy import select
|
||||
|
||||
from photo_pipeline.models import AnalysisResult, Asset, SafetyReview
|
||||
from photo_pipeline.services.inventory import InventoryService
|
||||
|
||||
seeded = seed_library(tmp_path, {}, {})
|
||||
folder = seeded.lib / album
|
||||
folder.mkdir(parents=True)
|
||||
for index, name in enumerate(names):
|
||||
image(folder / name, index + 1)
|
||||
|
||||
with session_factory(seeded) as sf:
|
||||
InventoryService(sf).scan(seeded.lib)
|
||||
with sf() as session:
|
||||
rows = list(session.execute(select(Asset.id, Asset.current_path)).all())
|
||||
for asset_id, path in rows:
|
||||
session.add(
|
||||
SafetyReview(
|
||||
id=str(uuid.uuid4()), asset_id=asset_id, decision="sfw", created_at=NOW
|
||||
)
|
||||
)
|
||||
session.add(
|
||||
AnalysisResult(
|
||||
asset_id=asset_id,
|
||||
status="analyzed",
|
||||
description=f"a view of {path}",
|
||||
tags='["ruins", "city"]',
|
||||
approx_year=2019,
|
||||
location_hint="Rome",
|
||||
)
|
||||
)
|
||||
session.commit()
|
||||
seeded.asset_ids.update({Path(path).stem: aid for aid, path in rows})
|
||||
return seeded
|
||||
|
||||
|
||||
def approve_album(base: str, *, album: str = "rome", name: str) -> None:
|
||||
"""Generate a proposal, set its final name, and approve it over HTTP."""
|
||||
httpx.post(f"{base}/api/v1/albums/proposals", json={}, timeout=10).raise_for_status()
|
||||
for payload, route in (
|
||||
({"name": name}, "edit"),
|
||||
({}, "approve"),
|
||||
):
|
||||
current = httpx.get(f"{base}/api/v1/albums/proposals/{album}", timeout=10).json()
|
||||
httpx.post(
|
||||
f"{base}/api/v1/albums/proposals/{album}/{route}",
|
||||
json={**payload, "expected_version": current["version"]},
|
||||
timeout=10,
|
||||
).raise_for_status()
|
||||
|
||||
Reference in New Issue
Block a user