"""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