Repository navigation
Expand file tree
/
Copy pathmain.py
More file actions
158 lines (135 loc) · 5.41 KB
/
Copy pathmain.py
File metadata and controls
158 lines (135 loc) · 5.41 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
import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
from starlette.exceptions import HTTPException as StarletteHTTPException
from api.config import Settings, configure_logging, get_settings
from api.middleware.rate_limit import SlidingWindowRateLimiter
from api.routes import batch, feedback, file, health, info, legacy, point
from pytransdatro import __version__ as pytransdatro_version
from pytransdatro.telemetry import SqliteUsageLogger
from pytransdatro.trans_ro import TransRO
logger = logging.getLogger("api.main")
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Application lifespan manager: pre-warms TransRO grid singleton and telemetry."""
settings = getattr(app.state, "settings", None) or get_settings()
configure_logging(settings)
logger.info("Initializing PyTransDatRO Web Service (Log Level: %s)", settings.log_level)
# Initialize background SQLite WAL telemetry logger
telemetry = SqliteUsageLogger(
log_dir=settings.log_dir,
fallback_path=str(settings.resolved_telemetry_fallback_path),
)
app.state.telemetry = telemetry
# Load SPG grid into RAM once on worker boot
logger.info("Loading SPG grid into RAM...")
trans = TransRO(telemetry_logger=telemetry)
app.state.trans = trans
logger.info("TransRO transformation engine pre-warmed and ready.")
yield
# Flush pending queue and gracefully close telemetry worker on exit
logger.info("Shutting down PyTransDatRO Web Service worker.")
if hasattr(app.state, "telemetry") and app.state.telemetry:
app.state.telemetry.close()
def create_app(settings: Settings | None = None) -> FastAPI:
"""Factory creating and configuring the FastAPI instance."""
app_settings = settings or get_settings()
configure_logging(app_settings)
app = FastAPI(
title="PyTransDatRO Web API",
description=(
"High-performance REST API for Romanian geodetic coordinate transformations "
"(Stereo 70 <-> ETRS89), featuring 100% backward compatibility with the legacy "
"TransDatOnline service, modern streaming endpoints, and zero-overhead telemetry."
),
version=pytransdatro_version,
lifespan=lifespan,
docs_url="/docs",
redoc_url="/redoc",
)
# Global uniform exception handlers
@app.exception_handler(Exception)
async def unhandled_exception_handler(request: Request, exc: Exception) -> JSONResponse:
logger.exception(
"Unhandled server exception on %s %s: %s",
request.method,
request.url.path,
exc,
)
return JSONResponse(
status_code=500,
content={
"detail": "Internal Server Error",
"code": "INTERNAL_SERVER_ERROR",
},
)
@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request: Request, exc: StarletteHTTPException) -> JSONResponse:
if exc.status_code >= 500:
logger.error(
"HTTP %d error on %s %s: %s",
exc.status_code,
request.method,
request.url.path,
exc.detail,
)
return JSONResponse(
status_code=exc.status_code,
content={
"detail": exc.detail,
"code": getattr(exc, "code", None),
},
headers=getattr(exc, "headers", None),
)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(
request: Request, exc: RequestValidationError
) -> JSONResponse:
if request.url.path == "/transdatonline/cooOpService":
return JSONResponse(
status_code=400,
content={
"detail": "Invalid request! cooOp or coos variable is missing!",
},
)
return JSONResponse(
status_code=422,
content={
"detail": exc.errors(),
"code": "VALIDATION_ERROR",
},
)
app.state.settings = app_settings
# Enable configurable CORS for web map applications and GIS plugins
app.add_middleware(
CORSMiddleware,
allow_origins=app_settings.parsed_cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# In-memory sliding-window rate limiting middleware configured from settings
app.add_middleware(
SlidingWindowRateLimiter,
point_limit=app_settings.rate_limit_point,
batch_limit=app_settings.rate_limit_batch,
window_seconds=app_settings.rate_limit_window_seconds,
)
# Register routers
app.include_router(legacy.router)
app.include_router(point.router)
app.include_router(batch.router)
app.include_router(file.router)
app.include_router(info.router)
app.include_router(health.router)
app.include_router(feedback.router)
# Mount static web application assets if built
webapp_dist = app_settings.webapp_dist_dir
if webapp_dist.is_dir():
app.mount("/", StaticFiles(directory=str(webapp_dist), html=True), name="webapp")
return app
app = create_app()