X-Git-Url: https://git.openstreetmap.org./nominatim.git/blobdiff_plain/c314a3092c5b51c7782015f6fa9ac093b46fa174..689bcbd6ea1bb83f815075f1f4cc53b8dc1f6393:/src/nominatim_api/server/falcon/server.py diff --git a/src/nominatim_api/server/falcon/server.py b/src/nominatim_api/server/falcon/server.py index bc9850b2..13e79311 100644 --- a/src/nominatim_api/server/falcon/server.py +++ b/src/nominatim_api/server/falcon/server.py @@ -7,7 +7,7 @@ """ Server implementation using the falcon webserver framework. """ -from typing import Optional, Mapping, cast, Any, List +from typing import Optional, Mapping, Any, List from pathlib import Path import datetime as dt import asyncio @@ -17,7 +17,10 @@ from falcon.asgi import App, Request, Response from ...config import Configuration from ...core import NominatimAPIAsync from ... import v1 as api_impl +from ...result_formatting import FormatDispatcher, load_format_dispatcher from ... import logging as loglib +from ..asgi_adaptor import ASGIAdaptor, EndpointFunc + class HTTPNominatimError(Exception): """ A special exception class for errors raised during processing. @@ -28,7 +31,7 @@ class HTTPNominatimError(Exception): self.content_type = content_type -async def nominatim_error_handler(req: Request, resp: Response, #pylint: disable=unused-argument +async def nominatim_error_handler(req: Request, resp: Response, exception: HTTPNominatimError, _: Any) -> None: """ Special error handler that passes message and content type as @@ -39,8 +42,8 @@ async def nominatim_error_handler(req: Request, resp: Response, #pylint: disable resp.content_type = exception.content_type -async def timeout_error_handler(req: Request, resp: Response, #pylint: disable=unused-argument - exception: TimeoutError, #pylint: disable=unused-argument +async def timeout_error_handler(req: Request, resp: Response, + exception: TimeoutError, _: Any) -> None: """ Special error handler that passes message and content type as per exception info. @@ -57,57 +60,58 @@ async def timeout_error_handler(req: Request, resp: Response, #pylint: disable=u resp.content_type = 'text/plain; charset=utf-8' -class ParamWrapper(api_impl.ASGIAdaptor): +class ParamWrapper(ASGIAdaptor): """ Adaptor class for server glue to Falcon framework. """ def __init__(self, req: Request, resp: Response, - config: Configuration) -> None: + config: Configuration, formatter: FormatDispatcher) -> None: self.request = req self.response = resp self._config = config - + self._formatter = formatter def get(self, name: str, default: Optional[str] = None) -> Optional[str]: - return cast(Optional[str], self.request.get_param(name, default=default)) - + return self.request.get_param(name, default=default) def get_header(self, name: str, default: Optional[str] = None) -> Optional[str]: - return cast(Optional[str], self.request.get_header(name, default=default)) - + return self.request.get_header(name, default=default) def error(self, msg: str, status: int = 400) -> HTTPNominatimError: return HTTPNominatimError(msg, status, self.content_type) - def create_response(self, status: int, output: str, num_results: int) -> None: self.response.context.num_results = num_results self.response.status = status self.response.text = output self.response.content_type = self.content_type - def base_uri(self) -> str: - return cast (str, self.request.forwarded_prefix) + return self.request.forwarded_prefix def config(self) -> Configuration: return self._config + def formatting(self) -> FormatDispatcher: + return self._formatter + class EndpointWrapper: """ Converter for server glue endpoint functions to Falcon request handlers. """ - def __init__(self, name: str, func: api_impl.EndpointFunc, api: NominatimAPIAsync) -> None: + def __init__(self, name: str, func: EndpointFunc, api: NominatimAPIAsync, + formatter: FormatDispatcher) -> None: self.name = name self.func = func self.api = api - + self.formatter = formatter async def on_get(self, req: Request, resp: Response) -> None: """ Implementation of the endpoint. """ - await self.func(self.api, ParamWrapper(req, resp, self.api.config)) + await self.func(self.api, ParamWrapper(req, resp, self.api.config, + self.formatter)) class FileLoggingMiddleware: @@ -115,15 +119,13 @@ class FileLoggingMiddleware: """ def __init__(self, file_name: str): - self.fd = open(file_name, 'a', buffering=1, encoding='utf8') # pylint: disable=R1732 - + self.fd = open(file_name, 'a', buffering=1, encoding='utf8') async def process_request(self, req: Request, _: Response) -> None: """ Callback before the request starts timing. """ req.context.start = dt.datetime.now(tz=dt.timezone.utc) - async def process_response(self, req: Request, resp: Response, resource: Optional[EndpointWrapper], req_succeeded: bool) -> None: @@ -131,7 +133,7 @@ class FileLoggingMiddleware: writes logs for successful requests for search, reverse and lookup. """ if not req_succeeded or resource is None or resp.status != 200\ - or resource.name not in ('reverse', 'search', 'lookup', 'details'): + or resource.name not in ('reverse', 'search', 'lookup', 'details'): return finish = dt.datetime.now(tz=dt.timezone.utc) @@ -145,12 +147,36 @@ class FileLoggingMiddleware: f'{resource.name} "{params}"\n') -class APIShutdown: - """ Middleware that closes any open database connections. +class APIMiddleware: + """ Middleware managing the Nominatim database connection. """ - def __init__(self, api: NominatimAPIAsync) -> None: - self.api = api + def __init__(self, project_dir: Path, environ: Optional[Mapping[str, str]]) -> None: + self.api = NominatimAPIAsync(project_dir, environ) + self.app: Optional[App] = None + + @property + def config(self) -> Configuration: + """ Get the configuration for Nominatim. + """ + return self.api.config + + def set_app(self, app: App) -> None: + """ Set the Falcon application this middleware is connected to. + """ + self.app = app + + async def process_startup(self, *_: Any) -> None: + """ Process the ASGI lifespan startup event. + """ + assert self.app is not None + legacy_urls = self.api.config.get_bool('SERVE_LEGACY_URLS') + formatter = load_format_dispatcher('v1', self.api.config.project_dir) + for name, func in await api_impl.get_routes(self.api): + endpoint = EndpointWrapper(name, func, self.api, formatter) + self.app.add_route(f"/{name}", endpoint) + if legacy_urls: + self.app.add_route(f"/{name}.php", endpoint) async def process_shutdown(self, *_: Any) -> None: """Process the ASGI lifespan shutdown event. @@ -162,26 +188,21 @@ def get_application(project_dir: Path, environ: Optional[Mapping[str, str]] = None) -> App: """ Create a Nominatim Falcon ASGI application. """ - api = NominatimAPIAsync(project_dir, environ) + apimw = APIMiddleware(project_dir, environ) - middleware: List[object] = [APIShutdown(api)] - log_file = api.config.LOG_FILE + middleware: List[object] = [apimw] + log_file = apimw.config.LOG_FILE if log_file: middleware.append(FileLoggingMiddleware(log_file)) - app = App(cors_enable=api.config.get_bool('CORS_NOACCESSCONTROL'), + app = App(cors_enable=apimw.config.get_bool('CORS_NOACCESSCONTROL'), middleware=middleware) + + apimw.set_app(app) app.add_error_handler(HTTPNominatimError, nominatim_error_handler) app.add_error_handler(TimeoutError, timeout_error_handler) # different from TimeoutError in Python <= 3.10 - app.add_error_handler(asyncio.TimeoutError, timeout_error_handler) - - legacy_urls = api.config.get_bool('SERVE_LEGACY_URLS') - for name, func in api_impl.ROUTES: - endpoint = EndpointWrapper(name, func, api) - app.add_route(f"/{name}", endpoint) - if legacy_urls: - app.add_route(f"/{name}.php", endpoint) + app.add_error_handler(asyncio.TimeoutError, timeout_error_handler) # type: ignore[arg-type] return app