Files
monitoring-tg/src/parser_bot/main.py
2026-06-04 16:10:13 +03:00

123 lines
3.3 KiB
Python

from contextlib import asynccontextmanager
import structlog
import uvicorn
from fastapi import Depends, FastAPI
from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
from fastapi.openapi.utils import get_openapi
from fastapi.responses import JSONResponse
from parser_bot.access import require_admin
from parser_bot.api.routes import router
from parser_bot.config import settings
from parser_bot.scheduler.poller import build_scheduler
from parser_bot.telegram.client import is_authorized, start_client, stop_client
structlog.configure(
processors=[
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.add_log_level,
structlog.processors.JSONRenderer(),
]
)
log = structlog.get_logger()
@asynccontextmanager
async def lifespan(app: FastAPI):
await start_client()
scheduler = build_scheduler()
scheduler.start()
authorized = await is_authorized()
log.info(
"startup", poll_interval=settings.poll_interval_seconds, authorized=authorized
)
if not authorized:
log.warning("not_authorized", action="open monitoring-tg in portal")
try:
yield
finally:
scheduler.shutdown(wait=False)
await stop_client()
log.info("shutdown")
def create_app() -> FastAPI:
public_base = settings.public_base_path.rstrip("/")
# Disable the default /docs, /redoc and /openapi.json — we serve our own
# admin-gated versions below.
app = FastAPI(
title="parser-tg-bot",
lifespan=lifespan,
docs_url=None,
redoc_url=None,
openapi_url=None,
)
app.include_router(router, prefix="/api/v1")
@app.get("/healthz")
async def healthz() -> dict[str, str]:
return {"status": "ok"}
@app.get("/", include_in_schema=False)
async def index() -> dict[str, str]:
return {"service": "monitoring-tg", "ui": "portal"}
# Admin-only: OpenAPI surface. Custom routes so we can wrap them in
# `require_admin`; the auto-generated ones from FastAPI bypass it.
@app.get(
"/openapi.json",
include_in_schema=False,
dependencies=[Depends(require_admin)],
)
async def openapi_json() -> JSONResponse:
return JSONResponse(
get_openapi(
title=app.title,
version=app.version,
openapi_version=app.openapi_version,
description=app.description,
routes=app.routes,
)
)
@app.get(
"/docs",
include_in_schema=False,
dependencies=[Depends(require_admin)],
)
async def docs():
return get_swagger_ui_html(
openapi_url=f"{public_base}/openapi.json" if public_base else "/openapi.json",
title=app.title + " — docs",
)
@app.get(
"/redoc",
include_in_schema=False,
dependencies=[Depends(require_admin)],
)
async def redoc():
return get_redoc_html(
openapi_url=f"{public_base}/openapi.json" if public_base else "/openapi.json",
title=app.title + " — redoc",
)
return app
app = create_app()
def main() -> None:
uvicorn.run(
"parser_bot.main:app",
host=settings.api_host,
port=settings.api_port,
log_config=None,
)
if __name__ == "__main__":
main()