File size: 5,853 Bytes
c95b19e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1dd2ce3
c95b19e
 
1470b69
c95b19e
 
 
 
 
 
 
 
 
 
 
e544feb
 
 
c95b19e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d77da37
 
 
 
 
 
 
 
1470b69
d77da37
 
c95b19e
1470b69
 
 
c95b19e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1470b69
1dd2ce3
c95b19e
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
"""Application entrypoint β€” single monolithic FastAPI app.

Routes are grouped by the deployed service they replace (executor, csv,
db, chatbot, interpreter, embeddings). All request validation, error handling,
CORS and auth wiring lives here.
"""

from __future__ import annotations

import asyncio
from contextlib import asynccontextmanager

from fastapi import FastAPI, HTTPException, Request, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse

from app.config import settings
from app.core.banner import print_startup_banner
from app.core.deps import ServiceAuth
from app.core.exceptions import register_exception_handlers
from app.core.logging import setup_logging, get_logger
from app.core.pool import shutdown
from app.core.redis import get_redis
    # from app.monitoring.manager import manager as ws_manager
    # from app.monitoring.metrics import get_snapshot, system_sampler
    # from app.monitoring.middleware import MonitoringMiddleware
    # from app.monitoring.router import router as monitoring_router
from app.routers import chatbot, csv_service, db, embeddings, executor, files, interpreter, report_jobs, db_sync_jobs
from app.services.db.mongo import close_mongo_clients
from app.services.db.sql import close_db_pools
from app.services.cron_scheduler import start_cron_scheduler

setup_logging("DEBUG" if settings.debug else "INFO")
logger = get_logger(__name__)

# Extreme top: ASCII banner β†’ details β†’ huge horizontal line, then the app starts.
print_startup_banner()


async def _self_ping() -> None:
    """Background self-ping: keeps the HF Space warm via its https /ping URL.

    Sends Bearer <HF_TOKEN (fallback app auth token)>. /ping itself is
    public, so the header is ignored today β€” it only matters if /ping ever
    becomes protected. Runs every SELF_PING_INTERVAL seconds.
    """
    if not settings.self_ping_enabled:
        logger.info("self-ping disabled (SELF_PING_ENABLED=false) β€” background task exiting")
        return
    url = settings.effective_self_ping_url
    if not url or not url.startswith("https://"):
        logger.warning("self-ping enabled but no https URL resolved (set SELF_PING_URL or SPACE_ID) β€” skipping")
        return
    interval = int(settings.self_ping_interval or 600)
    token = settings.effective_ping_token
    headers = {"Authorization": f"Bearer {token}"} if token else None
    logger.info("self-ping enabled: url=%s interval=%ss auth=%s", url, interval, "Bearer ***" if token else "none")
    import httpx

    while True:
        try:
            async with httpx.AsyncClient(timeout=30.0) as client:
                resp = await client.get(url, headers=headers)
            if resp.status_code == 200:
                logger.info("self-ping successful: %s", url)
            else:
                logger.warning("self-ping returned: %s β€” %s", url, resp.status_code)
        except Exception as exc:  # noqa: BLE001 β€” keep the loop alive
            logger.warning("self-ping error: %s", exc)
        await asyncio.sleep(interval)


@asynccontextmanager
async def lifespan(app: FastAPI):
    logger.info("app starting", extra={"version": settings.app_version})
    # sampler_task = asyncio.create_task(system_sampler())
    # broadcast_task = asyncio.create_task(
    #     ws_manager.broadcast_loop(get_snapshot)
    # )
    if settings.self_ping_enabled:
        logger.info(
            "scheduling self-ping task -> %s (every %ss)",
            settings.effective_self_ping_url,
            settings.self_ping_interval,
        )
        asyncio.create_task(_self_ping())
    else:
        logger.info("self-ping disabled (SELF_PING_ENABLED=false) β€” not scheduling")
    if settings.cron_enabled:
        logger.info(
            "scheduling frontend credit crons -> %s/api/cron + %s/api/v1/cron (daily 00:00)",
            (settings.frontend_base_url or "").rstrip("/") or "(FRONTEND_BASE_URL missing)",
            (settings.frontend_base_url or "").rstrip("/") or "(FRONTEND_BASE_URL missing)",
        )
    else:
        logger.info("frontend credit crons disabled (CRON_ENABLED=false) β€” not scheduling /api/cron nor /api/v1/cron")
    cron_scheduler = start_cron_scheduler()
    if cron_scheduler is None:
        logger.warning("frontend credit cron scheduler NOT running β€” /api/cron and /api/v1/cron will not fire (see reason above)")
    yield
    if cron_scheduler is not None:
        cron_scheduler.shutdown(wait=False)
        logger.info("cron scheduler stopped")
    # sampler_task.cancel()
    # broadcast_task.cancel()
    await shutdown()
    await close_db_pools()
    await close_mongo_clients()
    await get_redis().close()
    logger.info("app stopped")


app = FastAPI(
    title=settings.app_name,
    version=settings.app_version,
    lifespan=lifespan,
    docs_url="/docs" if settings.debug else None,
    redoc_url="/redoc" if settings.debug else None,
)

# Monitoring middleware β€” must be first to capture all requests
# app.add_middleware(MonitoringMiddleware)

app.add_middleware(
    CORSMiddleware,
    allow_origins=settings.allowed_origins,
    allow_credentials=False,
    allow_methods=["*"],
    allow_headers=["*"],
)

register_exception_handlers(app)

# app.include_router(monitoring_router)
app.include_router(executor.router)
app.include_router(csv_service.router)
app.include_router(db.router)
app.include_router(chatbot.router)
app.include_router(interpreter.router)
app.include_router(embeddings.router)
app.include_router(files.router)
app.include_router(report_jobs.router)
app.include_router(db_sync_jobs.router)


@app.get("/", tags=["meta"])
async def root() -> dict[str, str]:
    return {"message": f"{settings.app_name} is running"}


@app.get("/health", tags=["meta"])
async def health() -> dict[str, str]:
    return {"status": "ok"}