Public Access
feat: admin orchestrate endpoints — run-week, per-step, status
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,9 @@ from app.security import require_admin
|
|||||||
from app.services.scraper_service import ScraperService, enqueue_scrape
|
from app.services.scraper_service import ScraperService, enqueue_scrape
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
from datetime import date as DateType
|
||||||
|
from app.services.orchestrator import run_step, run_week
|
||||||
|
from app.models import WeeklyRun
|
||||||
|
|
||||||
router = APIRouter(dependencies=[Depends(require_admin)])
|
router = APIRouter(dependencies=[Depends(require_admin)])
|
||||||
|
|
||||||
@@ -162,3 +165,63 @@ def get_stats(db: Session = Depends(get_db)):
|
|||||||
"ingredients": ingredient_count,
|
"ingredients": ingredient_count,
|
||||||
"meal_plans": plan_count
|
"meal_plans": plan_count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_VALID_STEPS = {"scrape", "generate", "email", "deadline", "finalize"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/orchestrate/status")
|
||||||
|
def orchestrate_status(limit: int = 10, db: Session = Depends(get_db)):
|
||||||
|
runs = (
|
||||||
|
db.query(WeeklyRun)
|
||||||
|
.order_by(WeeklyRun.week_start_date.desc())
|
||||||
|
.limit(limit)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"runs": [
|
||||||
|
{
|
||||||
|
"id": str(r.id),
|
||||||
|
"family_id": str(r.family_id),
|
||||||
|
"week_start_date": r.week_start_date.isoformat(),
|
||||||
|
"status": r.status,
|
||||||
|
"scraped_at": r.scraped_at.isoformat() if r.scraped_at else None,
|
||||||
|
"generated_at": r.generated_at.isoformat() if r.generated_at else None,
|
||||||
|
"emailed_at": r.emailed_at.isoformat() if r.emailed_at else None,
|
||||||
|
"deadline_passed_at": r.deadline_passed_at.isoformat()
|
||||||
|
if r.deadline_passed_at
|
||||||
|
else None,
|
||||||
|
"finalized_at": r.finalized_at.isoformat() if r.finalized_at else None,
|
||||||
|
"used_stale_data": r.used_stale_data,
|
||||||
|
"error_step": r.error_step,
|
||||||
|
"error_message": r.error_message,
|
||||||
|
}
|
||||||
|
for r in runs
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/orchestrate/run-week", status_code=202)
|
||||||
|
def orchestrate_run_week(
|
||||||
|
background_tasks: BackgroundTasks,
|
||||||
|
week_start: Optional[str] = None,
|
||||||
|
):
|
||||||
|
week = DateType.fromisoformat(week_start) if week_start else None
|
||||||
|
background_tasks.add_task(run_week, week)
|
||||||
|
return {"status": "queued", "week_start": str(week or "current")}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/orchestrate/{step}", status_code=202)
|
||||||
|
def orchestrate_step(
|
||||||
|
step: str,
|
||||||
|
background_tasks: BackgroundTasks,
|
||||||
|
week_start: Optional[str] = None,
|
||||||
|
):
|
||||||
|
if step not in _VALID_STEPS:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Unknown step {step!r}. Valid: {sorted(_VALID_STEPS)}",
|
||||||
|
)
|
||||||
|
week = DateType.fromisoformat(week_start) if week_start else None
|
||||||
|
background_tasks.add_task(run_step, step, week)
|
||||||
|
return {"status": "queued", "step": step, "week_start": str(week or "current")}
|
||||||
@@ -474,3 +474,52 @@ def test_run_week_calls_all_steps(monkeypatch):
|
|||||||
from app.services.orchestrator.runner import run_week
|
from app.services.orchestrator.runner import run_week
|
||||||
run_week(WEEK)
|
run_week(WEEK)
|
||||||
assert called == ["scrape", "generate", "email", "deadline", "finalize"]
|
assert called == ["scrape", "generate", "email", "deadline", "finalize"]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Admin endpoint tests
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def admin_client(db):
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from app.main import app
|
||||||
|
from app.database import get_db
|
||||||
|
|
||||||
|
app.dependency_overrides[get_db] = lambda: db
|
||||||
|
client = TestClient(app)
|
||||||
|
yield client
|
||||||
|
app.dependency_overrides.clear()
|
||||||
|
|
||||||
|
|
||||||
|
ADMIN_HEADERS = {"Authorization": "Bearer test-admin-token"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_orchestrate_status_returns_list(admin_client, family, monkeypatch):
|
||||||
|
from app.config import settings
|
||||||
|
monkeypatch.setattr(settings, "ADMIN_TOKEN", "test-admin-token")
|
||||||
|
resp = admin_client.get("/api/admin/orchestrate/status", headers=ADMIN_HEADERS)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert "runs" in resp.json()
|
||||||
|
|
||||||
|
|
||||||
|
def test_orchestrate_step_invalid_returns_400(admin_client, monkeypatch):
|
||||||
|
from app.config import settings
|
||||||
|
monkeypatch.setattr(settings, "ADMIN_TOKEN", "test-admin-token")
|
||||||
|
resp = admin_client.post(
|
||||||
|
"/api/admin/orchestrate/bogus", headers=ADMIN_HEADERS
|
||||||
|
)
|
||||||
|
assert resp.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
def test_orchestrate_run_week_returns_202(admin_client, monkeypatch):
|
||||||
|
from app.config import settings
|
||||||
|
monkeypatch.setattr(settings, "ADMIN_TOKEN", "test-admin-token")
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"app.api.admin.run_week",
|
||||||
|
lambda week=None: None,
|
||||||
|
)
|
||||||
|
resp = admin_client.post(
|
||||||
|
"/api/admin/orchestrate/run-week", headers=ADMIN_HEADERS
|
||||||
|
)
|
||||||
|
assert resp.status_code == 202
|
||||||
|
|||||||
Reference in New Issue
Block a user