Files
MapMil/centers/analytics/api/app/routers/admin.py
T
gitrusprusandCursor 1492576fd9 Add Telegram listener, scheduler, and extended parsers admin UI.
Introduce background scheduling and migrations for analytics ingest, expand parser management and map toolbar UX, and ignore local data/session files from version control.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 20:47:56 +03:00

226 lines
6.7 KiB
Python

from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from ..database import get_db
from ..models import Consumer, Event, ParseJob
from ..schemas import (
AnalyticsSummary,
ConsumerCreate,
ConsumerRead,
ConsumerUpdate,
EventListResponse,
EventRead,
ParseJobCreate,
ParseJobRead,
ParseJobUpdate,
TimelinePoint,
TopItem,
)
from ..services.analytics import (
get_analytics_summary,
get_timeline,
get_top_localities,
get_top_regions,
)
from ..services.events_query import build_events_query
from ..services.filtering import (
consumer_to_read,
create_consumer,
list_consumers,
rotate_consumer_key,
update_consumer,
)
from ..services.jobs import enqueue_job
router = APIRouter(prefix="/admin", tags=["admin"])
@router.post("/jobs", response_model=ParseJobRead, status_code=201)
def create_parse_job(payload: ParseJobCreate, db: Session = Depends(get_db)):
job = ParseJob(
source_type=payload.source_type,
source_config=payload.source_config,
schedule=payload.schedule,
interval_seconds=payload.interval_seconds,
is_active=payload.is_active,
status="queued",
)
db.add(job)
db.commit()
db.refresh(job)
enqueue_job(job.id, job.source_type, job.source_config)
return job
@router.get("/jobs", response_model=list[ParseJobRead])
def list_parse_jobs(db: Session = Depends(get_db)):
return db.query(ParseJob).order_by(ParseJob.id.desc()).all()
@router.get("/jobs/{job_id}", response_model=ParseJobRead)
def get_parse_job(job_id: int, db: Session = Depends(get_db)):
job = db.query(ParseJob).filter(ParseJob.id == job_id).first()
if not job:
raise HTTPException(status_code=404, detail="Job not found")
return job
@router.post("/jobs/{job_id}/retry", response_model=ParseJobRead)
def retry_parse_job(job_id: int, db: Session = Depends(get_db)):
job = db.query(ParseJob).filter(ParseJob.id == job_id).first()
if not job:
raise HTTPException(status_code=404, detail="Job not found")
if job.status in ("queued", "running"):
raise HTTPException(status_code=409, detail="Job is already running or queued")
job.status = "queued"
job.last_error = None
db.commit()
db.refresh(job)
enqueue_job(job.id, job.source_type, job.source_config)
return job
@router.patch("/jobs/{job_id}", response_model=ParseJobRead)
def update_parse_job(
job_id: int,
payload: ParseJobUpdate,
db: Session = Depends(get_db),
):
job = db.query(ParseJob).filter(ParseJob.id == job_id).first()
if not job:
raise HTTPException(status_code=404, detail="Job not found")
if payload.source_config is not None:
job.source_config = payload.source_config
if payload.interval_seconds is not None:
job.interval_seconds = payload.interval_seconds
if payload.is_active is not None:
job.is_active = payload.is_active
db.commit()
db.refresh(job)
return job
@router.delete("/jobs/{job_id}", status_code=204)
def delete_parse_job(job_id: int, db: Session = Depends(get_db)):
job = db.query(ParseJob).filter(ParseJob.id == job_id).first()
if not job:
raise HTTPException(status_code=404, detail="Job not found")
if job.status == "running":
raise HTTPException(status_code=409, detail="Cannot delete a running job")
db.delete(job)
db.commit()
@router.get("/events", response_model=EventListResponse)
def list_events(
limit: int = Query(default=50, ge=1, le=1000),
offset: int = Query(default=0, ge=0),
source_type: str | None = None,
region: str | None = None,
topic: str | None = None,
locality: str | None = None,
date_from: datetime | None = None,
date_to: datetime | None = None,
search: str | None = None,
db: Session = Depends(get_db),
):
query = build_events_query(
db,
source_type=source_type,
region=region,
topic=topic,
locality=locality,
date_from=date_from,
date_to=date_to,
search=search,
)
total = query.count()
items = query.offset(offset).limit(limit).all()
return EventListResponse(items=items, total=total)
@router.get("/analytics/summary", response_model=AnalyticsSummary)
def analytics_summary(db: Session = Depends(get_db)):
return get_analytics_summary(db)
@router.get("/analytics/timeline", response_model=list[TimelinePoint])
def analytics_timeline(
days: int = Query(default=30, ge=1, le=365),
db: Session = Depends(get_db),
):
return get_timeline(db, days=days)
@router.get("/analytics/top-localities", response_model=list[TopItem])
def analytics_top_localities(
limit: int = Query(default=10, ge=1, le=100),
db: Session = Depends(get_db),
):
return get_top_localities(db, limit=limit)
@router.get("/analytics/top-regions", response_model=list[TopItem])
def analytics_top_regions(
limit: int = Query(default=10, ge=1, le=100),
db: Session = Depends(get_db),
):
return get_top_regions(db, limit=limit)
@router.get("/consumers", response_model=list[ConsumerRead])
def list_pi_consumers(db: Session = Depends(get_db)):
return [consumer_to_read(c) for c in list_consumers(db)]
@router.post("/consumers", response_model=ConsumerRead, status_code=201)
def create_pi_consumer(payload: ConsumerCreate, db: Session = Depends(get_db)):
consumer, api_key = create_consumer(
db,
name=payload.name,
regions=payload.regions,
topics=payload.topics,
date_from=payload.date_from,
)
return consumer_to_read(consumer, api_key=api_key)
@router.patch("/consumers/{consumer_id}", response_model=ConsumerRead)
def patch_pi_consumer(
consumer_id: int,
payload: ConsumerUpdate,
db: Session = Depends(get_db),
):
consumer = db.query(Consumer).filter(Consumer.id == consumer_id).first()
if not consumer:
raise HTTPException(status_code=404, detail="Consumer not found")
consumer = update_consumer(
db,
consumer,
name=payload.name,
is_active=payload.is_active,
regions=payload.regions,
topics=payload.topics,
date_from=payload.date_from,
)
return consumer_to_read(consumer)
@router.post("/consumers/{consumer_id}/rotate-key", response_model=ConsumerRead)
def rotate_pi_consumer_key(consumer_id: int, db: Session = Depends(get_db)):
consumer = db.query(Consumer).filter(Consumer.id == consumer_id).first()
if not consumer:
raise HTTPException(status_code=404, detail="Consumer not found")
consumer, api_key = rotate_consumer_key(db, consumer)
return consumer_to_read(consumer, api_key=api_key)