-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
177 lines (136 loc) · 4.96 KB
/
Copy pathmain.py
File metadata and controls
177 lines (136 loc) · 4.96 KB
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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
from __future__ import annotations
import inspect
import logging
import os
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager, suppress
from functools import lru_cache
from typing import Any
import redis.asyncio as redis
from ctao_shared.logging_config import setup_logging
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from starlette.staticfiles import StaticFiles
from api.basket import basket_router
from api.config import ApiSettings, get_api_settings
from api.coords import coord_router
from api.db import close_engine
from api.metrics import setup_metrics
from api.opus import router as opus_router
from api.query_history import query_history_router
from api.redis_client import close_redis, get_api_redis_pool
from api.routers.config import router as config_router
from api.routers.datalink import router as datalink_router
from api.routers.health import router as health_router
from api.routers.object_lookup import router as object_lookup_router
from api.routers.search import router as search_router
from api.routers.time import router as time_router
@lru_cache
def _settings() -> ApiSettings:
return get_api_settings()
setup_logging(
level=_settings().LOG_LEVEL,
include_access=_settings().LOG_INCLUDE_ACCESS,
json=_settings().LOG_JSON,
)
logger = logging.getLogger(__name__)
def _is_testing_env() -> bool:
v = os.getenv("TESTING", "")
return v.lower() in {"1", "true", "yes", "on"} or "PYTEST_CURRENT_TEST" in os.environ
def _init_redis_for_app(app: FastAPI) -> redis.ConnectionPool | None:
if _is_testing_env():
from ctao_shared.testing.fakeredis import FakeRedis
app.state.redis = FakeRedis()
logger.info("Using in-memory FakeRedis for tests.")
return None
pool = get_api_redis_pool()
app.state.redis = redis.Redis(connection_pool=pool, decode_responses=True)
logger.info("Redis pool initialised.")
return pool
async def _safe_close(obj: Any) -> None:
"""Call aclose/close/disconnect if present; await if needed; ignore RuntimeError on shutdown."""
close = (
getattr(obj, "aclose", None)
or getattr(obj, "close", None)
or getattr(obj, "disconnect", None)
)
if not close:
return
with suppress(RuntimeError):
res = close()
if inspect.isawaitable(res):
await res
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
logger.info("API starting up")
pool = _init_redis_for_app(app)
try:
yield
finally:
r = getattr(app.state, "redis", None)
if r is not None:
await _safe_close(r)
if pool is not None:
await _safe_close(pool)
await close_redis()
await close_engine()
logger.info("API resources closed.")
def _env_truthy(name: str, default: str = "0") -> bool:
return os.getenv(name, default).strip().lower() in {"1", "true", "yes", "on"}
def create_app() -> FastAPI:
docs_enabled = _settings().ENABLE_DOCS
app = FastAPI(
title="CTAO Data Explorer API",
description="An API to access and analyse high-energy astrophysics data from CTAO",
version="1.0.0",
lifespan=lifespan,
docs_url="/docs" if docs_enabled else None,
redoc_url=None,
openapi_url="/openapi.json" if docs_enabled else None,
)
setup_metrics(app)
origins = [
"http://localhost:3000",
"http://127.0.0.1:3000",
"http://localhost:8000",
"http://127.0.0.1:8000",
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(health_router)
app.include_router(time_router)
app.include_router(search_router)
app.include_router(object_lookup_router)
app.include_router(datalink_router)
app.include_router(basket_router)
app.include_router(opus_router)
app.include_router(query_history_router)
app.include_router(coord_router)
app.include_router(config_router)
serve_frontend = _env_truthy("SERVE_FRONTEND", "0")
static_dir = os.getenv("STATIC_DIR", "./js/build")
if serve_frontend and os.path.isdir(static_dir):
logger.info(
"SERVE_FRONTEND enabled: mounting static SPA from '%s' at '/'.",
static_dir,
)
app.mount("/", StaticFiles(directory=static_dir, html=True), name="js")
else:
if serve_frontend:
logger.warning(
"SERVE_FRONTEND enabled but static build dir '%s' not found; not mounting SPA.",
static_dir,
)
@app.get("/", include_in_schema=False)
def root() -> dict[str, str]:
return {"status": "ok", "app": "CTAO Data Explorer API"}
return app
app = create_app()
if __name__ == "__main__":
import uvicorn
uvicorn.run("api.main:app", host="127.0.0.1", port=8000, reload=True)