gendesign/tradein-mvp/backend/app/main.py
lekss361 0c53108d7d feat(tradein): in-app scheduler — UI-managed schedule (replaces SSH crontab)
Background scheduler в FastAPI lifespan: each tick (60s) checks scrape_schedules
table, triggers city sweep run если due (next_run_at <= NOW() + enabled + no
running run), recomputes next_run_at для следующих суток в random время в окне.

- 052_scrape_schedules.sql: NEW table (source, enabled, window_start/end_hour UTC,
  default_params jsonb, last_run_id, last_run_at, next_run_at, updated_at).
  Seed: 'avito_city_sweep' enabled, window 2-5 UTC (05:00-08:00 МСК).
- scheduler.py NEW:
  * compute_next_run_at(start, end) — random datetime в окне для +1 day. Cross-midnight OK.
  * scheduler_loop() — tick каждые 60s
  * get_due_schedules / has_running_run / trigger_avito_city_sweep_run
  * reap_zombies() — runs running > 6h без heartbeat → status='zombie'
- main.py: FastAPI lifespan launches scheduler_task; clean shutdown через cancel
- admin.py: 2 endpoints
  * GET  /admin/scrape/schedules — list all (now single row, extensible)
  * PUT  /admin/scrape/schedules/{source} — update config (enabled, window, params)
    + auto-recompute next_run_at
- schemas/trade_in.py: ScheduleConfig + ScheduleConfigUpdate
- tests/test_scheduler.py: 6 offline tests (normal window, cross-midnight, 1h window,
  future check, timezone-aware check)

UI: frontend PR добавит секцию 6 на /scrapers/avito (parallel PR).
Cron-script deploy/avito-city-sweep.sh остаётся как backup, opt-in.
2026-05-23 17:49:00 +03:00

83 lines
2.9 KiB
Python

"""Trade-In MVP — FastAPI entry point.
Standalone версия, выделена из основного gendesign repo для локальной разработки.
Только trade-in фича; никаких других роутеров (concepts/parcels/analytics/etc) нет.
"""
from __future__ import annotations
import asyncio
import logging
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
import sentry_sdk
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.api.v1 import admin, brand, geocode, trade_in
from app.core.config import settings
from app.core.ratelimit import RateLimitMiddleware
from app.services.scheduler import scheduler_loop
logger = logging.getLogger(__name__)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
)
# Мониторинг ошибок — GlitchTip (Sentry-совместимый, #396).
# DSN из env GLITCHTIP_DSN; пусто (dev) → мониторинг не инициализируется.
if settings.glitchtip_dsn:
sentry_sdk.init(
dsn=settings.glitchtip_dsn,
environment=settings.environment,
traces_sample_rate=0.0, # только ошибки, без performance-трейсов
send_default_pii=False, # не шлём client_name / client_phone в отчёты
)
logging.getLogger("app.main").info("GlitchTip monitoring enabled")
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
# Startup: launch scheduler background task
scheduler_task = asyncio.create_task(scheduler_loop())
logger.info("FastAPI lifespan: scheduler task spawned")
yield
# Shutdown: cancel scheduler
scheduler_task.cancel()
try:
await scheduler_task
except asyncio.CancelledError:
pass
logger.info("FastAPI lifespan: scheduler cancelled cleanly")
app = FastAPI(
title="Trade-In MVP API",
description="Оценка вторичного жилья (выкупная стоимость) — копия trade-in feature из gendesign", # noqa: E501
version="0.1.0",
lifespan=lifespan,
)
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Rate-limit публичного API (per-IP sliding window) — защита от абуза.
app.add_middleware(RateLimitMiddleware)
@app.get("/health")
def health() -> dict[str, str]:
return {"status": "ok", "environment": settings.environment}
app.include_router(geocode.router, prefix="/api/v1/geocode", tags=["geocode"])
app.include_router(admin.router, prefix="/api/v1/admin", tags=["admin"])
app.include_router(brand.router, prefix="/api/v1/brand", tags=["brand"])
app.include_router(trade_in.router, prefix="/api/v1/trade-in", tags=["trade-in"])