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
|