123 lines
3.3 KiB
Python
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()
|