FastAPI + Celery + Next.js + Postgres/Redis app with company monitoring, source collection, LLM-based change analysis, enrichment, and account security (Turnstile, escalating lockout, email verification).
67 lines
2.5 KiB
Python
67 lines
2.5 KiB
Python
"""Run-now / run-history service. Enqueuing goes through Celery
|
|
(`run_monitoring.delay`); this module only ever touches the `MonitoringRun`
|
|
row and ownership checks - the actual collection work happens in the task
|
|
(app/tasks/collection.py) and collection_service.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
from datetime import UTC, datetime, timedelta
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.core.config import Settings
|
|
from app.core.errors import NotFoundError, RateLimitedError
|
|
from app.models.enums import MonitoringRunTrigger
|
|
from app.models.monitoring_run import MonitoringRun
|
|
from app.repositories.monitoring_run_repository import MonitoringRunRepository
|
|
from app.services import company_service
|
|
|
|
|
|
async def enqueue_run_now(
|
|
db: AsyncSession, settings: Settings, user_id: uuid.UUID, company_id: uuid.UUID
|
|
) -> MonitoringRun:
|
|
company = await company_service.get_company(db, user_id, company_id)
|
|
run_repo = MonitoringRunRepository(db)
|
|
|
|
# Idempotent: a company already mid-run returns that run rather than
|
|
# queuing a duplicate (spec: "unique job keys to prevent duplicate
|
|
# concurrent runs").
|
|
active = await run_repo.get_active_for_company(company.id)
|
|
if active is not None:
|
|
return active
|
|
|
|
since = datetime.now(UTC) - timedelta(days=1)
|
|
manual_count = await run_repo.count_manual_since(company.id, since)
|
|
if manual_count >= settings.max_manual_runs_per_day:
|
|
raise RateLimitedError(
|
|
f"This company has reached the maximum of {settings.max_manual_runs_per_day} "
|
|
"manual runs per day. Scheduled runs are unaffected."
|
|
)
|
|
|
|
run = await run_repo.create(company_id=company.id, trigger_type=MonitoringRunTrigger.MANUAL)
|
|
await db.commit()
|
|
|
|
from app.tasks.collection import (
|
|
run_monitoring,
|
|
) # local import: keeps Celery out of API startup path
|
|
|
|
run_monitoring.delay(str(run.id))
|
|
return run
|
|
|
|
|
|
async def list_runs(
|
|
db: AsyncSession, user_id: uuid.UUID, company_id: uuid.UUID
|
|
) -> list[MonitoringRun]:
|
|
await company_service.get_company(db, user_id, company_id)
|
|
return await MonitoringRunRepository(db).list_for_company(company_id)
|
|
|
|
|
|
async def get_run(db: AsyncSession, user_id: uuid.UUID, run_id: uuid.UUID) -> MonitoringRun:
|
|
run = await MonitoringRunRepository(db).get(run_id)
|
|
if run is None:
|
|
raise NotFoundError("Monitoring run not found")
|
|
await company_service.get_company(db, user_id, run.company_id) # ownership check
|
|
return run
|