implement details endpoint in Python servers

This commit is contained in:
Sarah Hoffmann
2023-02-03 21:14:33 +01:00
parent 104722a56a
commit 3ac70f7cc2
6 changed files with 100 additions and 19 deletions

View File

@@ -11,6 +11,7 @@ Combine with the scaffolding provided for the various Python ASGI frameworks.
from typing import Optional, Any, Type, Callable
import abc
from nominatim.config import Configuration
import nominatim.api as napi
from nominatim.api.v1.format import dispatch as formatting
@@ -40,9 +41,9 @@ class ASGIAdaptor(abc.ABC):
@abc.abstractmethod
def error(self, msg: str) -> Exception:
def error(self, msg: str, status: int = 400) -> Exception:
""" Construct an appropriate exception from the given error message.
The exception must result in a HTTP 400 error.
The exception must result in a HTTP error with the given status.
"""
@@ -59,6 +60,12 @@ class ASGIAdaptor(abc.ABC):
"""
@abc.abstractmethod
def config(self) -> Configuration:
""" Return the current configuration object.
"""
def build_response(self, output: str, media_type: str, status: int = 200) -> Any:
""" Create a response from the given output. Wraps a JSONP function
around the response, if necessary.
@@ -116,6 +123,14 @@ class ASGIAdaptor(abc.ABC):
return value != '0'
def get_accepted_languages(self) -> str:
""" Return the accepted langauges.
"""
return self.get('accept-language')\
or self.get_header('http_accept_language')\
or self.config().DEFAULT_LANGUAGE
def parse_format(params: ASGIAdaptor, result_type: Type[Any], default: str) -> str:
""" Get and check the 'format' parameter and prepare the formatter.
`fmtter` is a formatter and `default` the
@@ -146,8 +161,49 @@ async def status_endpoint(api: napi.NominatimAPIAsync, params: ASGIAdaptor) -> A
return params.build_response(formatting.format_result(result, fmt, {}), fmt,
status=status_code)
async def details_endpoint(api: napi.NominatimAPIAsync, params: ASGIAdaptor) -> Any:
""" Server glue for /details endpoint. See API docs for details.
"""
place_id = params.get_int('place_id', 0)
place: napi.PlaceRef
if place_id:
place = napi.PlaceID(place_id)
else:
osmtype = params.get('osmtype')
if osmtype is None:
raise params.error("Missing ID parameter 'place_id' or 'osmtype'.")
place = napi.OsmID(osmtype, params.get_int('osmid'), params.get('class'))
details = napi.LookupDetails(address_details=params.get_bool('addressdetails', False),
linked_places=params.get_bool('linkedplaces', False),
parented_places=params.get_bool('hierarchy', False),
keywords=params.get_bool('keywords', False))
if params.get_bool('polygon_geojson', False):
details.geometry_output = napi.GeometryFormat.GEOJSON
locales = napi.Locales.from_accept_languages(params.get_accepted_languages())
print(locales.languages)
result = await api.lookup(place, details)
if result is None:
raise params.error('No place with that OSM ID found.', status=404)
output = formatting.format_result(
result,
'details-json',
{'locales': locales,
'group_hierarchy': params.get_bool('group_hierarchy', False),
'icon_base_url': params.config().MAPICON_URL})
return params.build_response(output, 'json')
EndpointFunc = Callable[[napi.NominatimAPIAsync, ASGIAdaptor], Any]
ROUTES = [
('status', status_endpoint)
('status', status_endpoint),
('details', details_endpoint)
]

View File

@@ -15,15 +15,18 @@ from falcon.asgi import App, Request, Response
from nominatim.api import NominatimAPIAsync
import nominatim.api.v1 as api_impl
from nominatim.config import Configuration
class ParamWrapper(api_impl.ASGIAdaptor):
""" Adaptor class for server glue to Falcon framework.
"""
def __init__(self, req: Request, resp: Response) -> None:
def __init__(self, req: Request, resp: Response,
config: Configuration) -> None:
self.request = req
self.response = resp
self._config = config
def get(self, name: str, default: Optional[str] = None) -> Optional[str]:
@@ -34,8 +37,13 @@ class ParamWrapper(api_impl.ASGIAdaptor):
return cast(Optional[str], self.request.get_header(name, default=default))
def error(self, msg: str) -> falcon.HTTPBadRequest:
return falcon.HTTPBadRequest(description=msg)
def error(self, msg: str, status: int = 400) -> falcon.HTTPError:
if status == 400:
return falcon.HTTPBadRequest(description=msg)
if status == 404:
return falcon.HTTPNotFound(description=msg)
return falcon.HTTPError(status, description=msg)
def create_response(self, status: int, output: str, content_type: str) -> None:
@@ -44,6 +52,10 @@ class ParamWrapper(api_impl.ASGIAdaptor):
self.response.content_type = content_type
def config(self) -> Configuration:
return self._config
class EndpointWrapper:
""" Converter for server glue endpoint functions to Falcon request handlers.
"""
@@ -56,7 +68,7 @@ class EndpointWrapper:
async def on_get(self, req: Request, resp: Response) -> None:
""" Implementation of the endpoint.
"""
await self.func(self.api, ParamWrapper(req, resp))
await self.func(self.api, ParamWrapper(req, resp, self.api.config))
def get_application(project_dir: Path,

View File

@@ -16,6 +16,7 @@ from sanic.response import text as TextResponse
from nominatim.api import NominatimAPIAsync
import nominatim.api.v1 as api_impl
from nominatim.config import Configuration
class ParamWrapper(api_impl.ASGIAdaptor):
""" Adaptor class for server glue to Sanic framework.
@@ -33,8 +34,8 @@ class ParamWrapper(api_impl.ASGIAdaptor):
return cast(Optional[str], self.request.headers.get(name, default))
def error(self, msg: str) -> SanicException:
return SanicException(msg, status_code=400)
def error(self, msg: str, status: int = 400) -> SanicException:
return SanicException(msg, status_code=status)
def create_response(self, status: int, output: str,
@@ -42,6 +43,10 @@ class ParamWrapper(api_impl.ASGIAdaptor):
return TextResponse(output, status=status, content_type=content_type)
def config(self) -> Configuration:
return cast(Configuration, self.request.app.ctx.api.config)
def _wrap_endpoint(func: api_impl.EndpointFunc)\
-> Callable[[Request], Coroutine[Any, Any, HTTPResponse]]:
async def _callback(request: Request) -> HTTPResponse:

View File

@@ -18,9 +18,9 @@ from starlette.requests import Request
from starlette.middleware import Middleware
from starlette.middleware.cors import CORSMiddleware
from nominatim.config import Configuration
from nominatim.api import NominatimAPIAsync
import nominatim.api.v1 as api_impl
from nominatim.config import Configuration
class ParamWrapper(api_impl.ASGIAdaptor):
""" Adaptor class for server glue to Starlette framework.
@@ -38,14 +38,18 @@ class ParamWrapper(api_impl.ASGIAdaptor):
return self.request.headers.get(name, default)
def error(self, msg: str) -> HTTPException:
return HTTPException(400, detail=msg)
def error(self, msg: str, status: int = 400) -> HTTPException:
return HTTPException(status, detail=msg)
def create_response(self, status: int, output: str, content_type: str) -> Response:
return Response(output, status_code=status, media_type=content_type)
def config(self) -> Configuration:
return cast(Configuration, self.request.app.state.API.config)
def _wrap_endpoint(func: api_impl.EndpointFunc)\
-> Callable[[Request], Coroutine[Any, Any, Response]]:
async def _callback(request: Request) -> Response: