"""Service entry point. The application runs behind the AppFactory reverse proxy under /apps/. The prefix is stripped before the request reaches the container, so only the OpenAPI document has to know about it. That is what root_path does. """ from __future__ import annotations import logging from contextlib import asynccontextmanager from fastapi import FastAPI, Request from fastapi.responses import JSONResponse from .config import get_settings from .deps import Container, set_container from .engines.chromium import ChromiumEngine from .engines.weasy import WeasyPrintEngine from .errors import ConversionError from .logging_setup import setup_logging from .routers import convert as convert_router from .routers import health as health_router from .routers import jobs as jobs_router from .services.jobs import JobManager from .services.pipeline import ConversionPipeline from .services.storage import Storage logger = logging.getLogger(__name__) DESCRIPTION = """ Sluzba prevadi HTML dokument na PDF. Prijme adresu dokumentu nebo HTML primo v tele requestu a vrati soubor PDF. Pro dokumenty o stovkach az tisicich stranek pouzijte asynchronni endpoint POST /jobs. Synchronni POST /convert je urceny pro mensi dokumenty. Dva render enginy: - weasyprint je vychozi, ma spravne strankovani a nizkou pametovou narocnost, nespousti JavaScript - chromium zvladne i dokumenty dokreslovane JavaScriptem, ale nema pouzitelne CSS countery, takze cisla stranek se dopisuji do hotoveho PDF """ @asynccontextmanager async def lifespan(app: FastAPI): settings = get_settings() setup_logging(settings.log_level) engines: dict = {} weasy = WeasyPrintEngine() if await weasy.available(): engines["weasyprint"] = weasy else: logger.error("WeasyPrint engine is unavailable, the service will rely on Chromium only") chromium = ChromiumEngine() if settings.chromium_enabled: engines["chromium"] = chromium else: logger.info("Chromium engine is disabled by configuration") if not engines: logger.error("No render engine is available, conversion requests will fail") storage = Storage(settings.storage_dir) pipeline = ConversionPipeline(engines, settings) manager = JobManager(pipeline, storage, settings) set_container( Container( settings=settings, storage=storage, pipeline=pipeline, jobs=manager, engines=engines, ) ) await manager.start() logger.info( "Service started", extra={"engines": sorted(engines), "root_path": settings.root_path}, ) try: yield finally: await manager.stop() for engine in engines.values(): await engine.shutdown() logger.info("Service stopped") settings = get_settings() setup_logging(settings.log_level) app = FastAPI( title=settings.app_name, version=settings.app_version, description=DESCRIPTION, root_path=settings.root_path, lifespan=lifespan, ) @app.exception_handler(ConversionError) async def conversion_error_handler(request: Request, exc: ConversionError) -> JSONResponse: logger.warning( "Request failed", extra={"error_code": exc.error_code, "path": request.url.path, "status_code": exc.status_code}, ) return JSONResponse(status_code=exc.status_code, content=exc.to_dict()) @app.exception_handler(Exception) async def unhandled_error_handler(request: Request, exc: Exception) -> JSONResponse: logger.exception("Unhandled error", extra={"path": request.url.path}) return JSONResponse( status_code=500, content={ "error_code": "internal_error", "message": "Doslo k neocekavane chybe sluzby.", }, ) app.include_router(health_router.router) app.include_router(convert_router.router) app.include_router(jobs_router.router)