Initial commit: CI Agent competitive-intelligence monitoring app
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).
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
"""Celery Beat-triggered task. Runs every minute (see celery_app.py's
|
||||
beat_schedule) and dynamically discovers which companies are due for a
|
||||
check by querying MonitorConfiguration/Source directly - adding a new
|
||||
company never requires touching this task or any static schedule config.
|
||||
|
||||
A company is due either because its own default MonitorConfiguration.next_run
|
||||
has arrived, or because at least one of its sources has its own faster
|
||||
frequency override that's independently due (see
|
||||
SourceRepository.list_due_for_company) - e.g. a "check news daily" source
|
||||
inside a company whose overall default cadence is weekly. Either way,
|
||||
run_monitoring only actually collects the sources that are themselves due
|
||||
for a SCHEDULED trigger - see tasks/collection.py.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from structlog.contextvars import bound_contextvars
|
||||
|
||||
from app.core.logging import get_logger
|
||||
from app.db.session import get_sessionmaker
|
||||
from app.models.enums import MonitoringRunTrigger
|
||||
from app.repositories.company_repository import MonitorConfigurationRepository
|
||||
from app.repositories.monitoring_run_repository import MonitoringRunRepository
|
||||
from app.repositories.source_repository import SourceRepository
|
||||
from app.tasks.base import run_async_task
|
||||
from app.tasks.celery_app import celery_app
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@celery_app.task(bind=True, name="app.tasks.scheduler.sync_schedules")
|
||||
def sync_schedules(self) -> None:
|
||||
with bound_contextvars(task_id=self.request.id):
|
||||
run_async_task(_sync_schedules_async())
|
||||
|
||||
|
||||
async def _sync_schedules_async() -> None:
|
||||
from app.tasks.collection import (
|
||||
run_monitoring,
|
||||
) # local import: avoids a circular import at module load
|
||||
|
||||
session_factory = get_sessionmaker()
|
||||
async with session_factory() as db:
|
||||
monitor_repo = MonitorConfigurationRepository(db)
|
||||
source_repo = SourceRepository(db)
|
||||
run_repo = MonitoringRunRepository(db)
|
||||
|
||||
now = datetime.now(UTC)
|
||||
enabled_configs = await monitor_repo.list_enabled()
|
||||
enqueued = 0
|
||||
|
||||
for config in enabled_configs:
|
||||
is_due = await source_repo.company_has_due_work(config.company_id, now, config.next_run)
|
||||
if not is_due:
|
||||
continue
|
||||
|
||||
# Idempotency: don't double-enqueue a company that's still
|
||||
# mid-run from a previous tick or a manual "run now".
|
||||
active_run = await run_repo.get_active_for_company(config.company_id)
|
||||
if active_run is not None:
|
||||
continue
|
||||
|
||||
run = await run_repo.create(
|
||||
company_id=config.company_id, trigger_type=MonitoringRunTrigger.SCHEDULED
|
||||
)
|
||||
await db.commit()
|
||||
run_monitoring.delay(str(run.id))
|
||||
enqueued += 1
|
||||
|
||||
if enqueued:
|
||||
logger.info("schedule_sync_enqueued_runs", count=enqueued)
|
||||
Reference in New Issue
Block a user