Skip to content

openfoodfacts_proxy.app

[docs] module openfoodfacts_proxy.app

  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
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
import logging
from typing import TYPE_CHECKING

from fastapi import FastAPI
from pymongo import MongoClient

from openfoodfacts_proxy.core.application_container import ApplicationContainer
from openfoodfacts_proxy.infrastructure.database import ensure_indexes
from openfoodfacts_proxy.infrastructure.settings import settings
from openfoodfacts_proxy.routes import router

if TYPE_CHECKING:
    from openfoodfacts_proxy.services.image_url_mapper import OpenFoodFactsUrlMapper

logger = logging.getLogger(__name__)


def _is_enabled(settings_obj: object, attribute: str, *, default: bool) -> bool:
    value = getattr(settings_obj, attribute, default)
    if isinstance(value, bool):
        return value
    if isinstance(value, str):
        normalized_value = value.strip().lower()
        if normalized_value in {"1", "true", "yes", "on"}:
            return True
        if normalized_value in {"0", "false", "no", "off"}:
            return False
    if value is None:
        return default
    return bool(value)


@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
    """Manage application lifespan: init/close clients and scheduler."""
    current_settings = getattr(app.state, "settings", settings)
    app.state.settings = current_settings

    owns_mongo_client = False
    mongo_client = getattr(app.state, "mongo_client", None)
    if mongo_client is None:
        mongo_client = MongoClient(current_settings.mongodb_uri)
        owns_mongo_client = True
    app.state.mongo_client = mongo_client

    db = getattr(app.state, "db", None)
    if db is None:
        db = mongo_client[current_settings.mongodb_db]
    app.state.db = db

    owns_container = False
    container = getattr(app.state, "container", None)
    if container is None:
        container = ApplicationContainer(
            current_settings,
            db,
            image_url_mapper=getattr(app.state, "image_url_mapper", None),
        )
        owns_container = True
    app.state.container = container

    startup_sync_enabled = _is_enabled(current_settings, "startup_sync_enabled", default=True)
    scheduler_enabled = _is_enabled(current_settings, "scheduler_enabled", default=True)

    try:
        ensure_indexes(db)
    except Exception as e:
        logger.warning(f"Could not create indexes on startup: {e}")

    if startup_sync_enabled:
        try:
            container.startup_sync_service.start()
        except Exception as e:
            logger.warning(f"Could not check DB state on startup: {e}")

    if scheduler_enabled:
        container.scheduler_service.start()

    logger.info(
        "Application started. startup_sync_enabled=%s scheduler_enabled=%s",
        startup_sync_enabled,
        scheduler_enabled,
    )

    yield

    if scheduler_enabled:
        container.scheduler_service.stop()
    if owns_container:
        await container.close()
    if owns_mongo_client:
        mongo_client.close()
    logger.info("Application shutdown complete.")


def create_app(*, image_url_mapper: "OpenFoodFactsUrlMapper | None" = None) -> FastAPI:
    """Create and configure the FastAPI application."""
    app = FastAPI(
        title="OpenFoodFacts Proxy",
        description="A reverse proxy for the OpenFoodFacts API with local caching and rate limit management.",
        version="1.0.0",
        lifespan=lifespan,
    )
    app.state.image_url_mapper = image_url_mapper

    app.include_router(router)

    return app