mirror of
https://github.com/osm-search/Nominatim.git
synced 2026-02-26 11:08:13 +00:00
add server glue for reverse API call
This commit is contained in:
@@ -8,7 +8,7 @@
|
|||||||
Generic part of the server implementation of the v1 API.
|
Generic part of the server implementation of the v1 API.
|
||||||
Combine with the scaffolding provided for the various Python ASGI frameworks.
|
Combine with the scaffolding provided for the various Python ASGI frameworks.
|
||||||
"""
|
"""
|
||||||
from typing import Optional, Any, Type, Callable, NoReturn
|
from typing import Optional, Any, Type, Callable, NoReturn, TypeVar
|
||||||
import abc
|
import abc
|
||||||
|
|
||||||
from nominatim.config import Configuration
|
from nominatim.config import Configuration
|
||||||
@@ -22,6 +22,7 @@ CONTENT_TYPE = {
|
|||||||
'debug': 'text/html; charset=utf-8'
|
'debug': 'text/html; charset=utf-8'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ConvT = TypeVar('ConvT', int, float)
|
||||||
|
|
||||||
class ASGIAdaptor(abc.ABC):
|
class ASGIAdaptor(abc.ABC):
|
||||||
""" Adapter class for the different ASGI frameworks.
|
""" Adapter class for the different ASGI frameworks.
|
||||||
@@ -106,9 +107,10 @@ class ASGIAdaptor(abc.ABC):
|
|||||||
raise self.error(msg, status)
|
raise self.error(msg, status)
|
||||||
|
|
||||||
|
|
||||||
def get_int(self, name: str, default: Optional[int] = None) -> int:
|
def _get_typed(self, name: str, dest_type: Type[ConvT], type_name: str,
|
||||||
""" Return an input parameter as an int. Raises an exception if
|
default: Optional[ConvT] = None) -> ConvT:
|
||||||
the parameter is given but not in an integer format.
|
""" Return an input parameter as the type 'dest_type'. Raises an
|
||||||
|
exception if the parameter is given but not in the given format.
|
||||||
|
|
||||||
If 'default' is given, then it will be returned when the parameter
|
If 'default' is given, then it will be returned when the parameter
|
||||||
is missing completely. When 'default' is None, an error will be
|
is missing completely. When 'default' is None, an error will be
|
||||||
@@ -123,12 +125,35 @@ class ASGIAdaptor(abc.ABC):
|
|||||||
self.raise_error(f"Parameter '{name}' missing.")
|
self.raise_error(f"Parameter '{name}' missing.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
intval = int(value)
|
intval = dest_type(value)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self.raise_error(f"Parameter '{name}' must be a number.")
|
self.raise_error(f"Parameter '{name}' must be a {type_name}.")
|
||||||
|
|
||||||
return intval
|
return intval
|
||||||
|
|
||||||
|
|
||||||
|
def get_int(self, name: str, default: Optional[int] = None) -> int:
|
||||||
|
""" Return an input parameter as an int. Raises an exception if
|
||||||
|
the parameter is given but not in an integer format.
|
||||||
|
|
||||||
|
If 'default' is given, then it will be returned when the parameter
|
||||||
|
is missing completely. When 'default' is None, an error will be
|
||||||
|
raised on a missing parameter.
|
||||||
|
"""
|
||||||
|
return self._get_typed(name, int, 'number', default)
|
||||||
|
|
||||||
|
|
||||||
|
def get_float(self, name: str, default: Optional[float] = None) -> int:
|
||||||
|
""" Return an input parameter as a flaoting-point number. Raises an
|
||||||
|
exception if the parameter is given but not in an float format.
|
||||||
|
|
||||||
|
If 'default' is given, then it will be returned when the parameter
|
||||||
|
is missing completely. When 'default' is None, an error will be
|
||||||
|
raised on a missing parameter.
|
||||||
|
"""
|
||||||
|
return self._get_typed(name, float, 'number', default)
|
||||||
|
|
||||||
|
|
||||||
def get_bool(self, name: str, default: Optional[bool] = None) -> bool:
|
def get_bool(self, name: str, default: Optional[bool] = None) -> bool:
|
||||||
""" Return an input parameter as bool. Only '0' is accepted as
|
""" Return an input parameter as bool. Only '0' is accepted as
|
||||||
an input for 'false' all other inputs will be interpreted as 'true'.
|
an input for 'false' all other inputs will be interpreted as 'true'.
|
||||||
@@ -169,6 +194,17 @@ class ASGIAdaptor(abc.ABC):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_layers(self) -> napi.DataLayer:
|
||||||
|
""" Return a parsed version of the layer parameter.
|
||||||
|
"""
|
||||||
|
param = self.get('layer', None)
|
||||||
|
if param is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return reduce(napi.DataLayer.__or__,
|
||||||
|
(getattr(napi.DataLayer, s.upper()) for s in param.split(',')))
|
||||||
|
|
||||||
|
|
||||||
def parse_format(self, result_type: Type[Any], default: str) -> str:
|
def parse_format(self, result_type: Type[Any], default: str) -> str:
|
||||||
""" Get and check the 'format' parameter and prepare the formatter.
|
""" Get and check the 'format' parameter and prepare the formatter.
|
||||||
`result_type` is the type of result to be returned by the function
|
`result_type` is the type of result to be returned by the function
|
||||||
@@ -243,9 +279,83 @@ async def details_endpoint(api: napi.NominatimAPIAsync, params: ASGIAdaptor) ->
|
|||||||
return params.build_response(output)
|
return params.build_response(output)
|
||||||
|
|
||||||
|
|
||||||
|
async def reverse_endpoint(api: napi.NominatimAPIAsync, params: ASGIAdaptor) -> Any:
|
||||||
|
""" Server glue for /reverse endpoint. See API docs for details.
|
||||||
|
"""
|
||||||
|
fmt = params.parse_format(napi.ReverseResults, 'xml')
|
||||||
|
debug = params.setup_debugging()
|
||||||
|
coord = napi.Point(params.get_float('lon'), params.get_float('lat'))
|
||||||
|
locales = napi.Locales.from_accept_languages(params.get_accepted_languages())
|
||||||
|
|
||||||
|
zoom = max(0, min(18, params.get_int('zoom', 18)))
|
||||||
|
|
||||||
|
# Negation makes sure that NaN is handled. Don't change.
|
||||||
|
if not abs(coord[0]) <= 180 or not abs(coord[1]) <= 90:
|
||||||
|
params.raise_error('Invalid coordinates.')
|
||||||
|
|
||||||
|
details = napi.LookupDetails(address_details=True,
|
||||||
|
geometry_simplification=params.get_float('polygon_threshold', 0.0))
|
||||||
|
numgeoms = 0
|
||||||
|
if params.get_bool('polygon_geojson', False):
|
||||||
|
details.geometry_output |= napi.GeometryFormat.GEOJSON
|
||||||
|
numgeoms += 1
|
||||||
|
if fmt not in ('geojson', 'geocodejson'):
|
||||||
|
if params.get_bool('polygon_text', False):
|
||||||
|
details.geometry_output |= napi.GeometryFormat.TEXT
|
||||||
|
numgeoms += 1
|
||||||
|
if params.get_bool('polygon_kml', False):
|
||||||
|
details.geometry_output |= napi.GeometryFormat.KML
|
||||||
|
numgeoms += 1
|
||||||
|
if params.get_bool('polygon_svg', False):
|
||||||
|
details.geometry_output |= napi.GeometryFormat.SVG
|
||||||
|
numgeoms += 1
|
||||||
|
|
||||||
|
if numgeoms > params.config().get_int('POLYGON_OUTPUT_MAX_TYPES'):
|
||||||
|
params.raise_error(f'Too many polgyon output options selected.')
|
||||||
|
|
||||||
|
result = await api.reverse(coord, REVERSE_MAX_RANKS[zoom],
|
||||||
|
params.get_layers() or
|
||||||
|
napi.DataLayer.ADDRESS | napi.DataLayer.POI,
|
||||||
|
details)
|
||||||
|
|
||||||
|
if debug:
|
||||||
|
return params.build_response(loglib.get_and_disable())
|
||||||
|
|
||||||
|
fmt_options = {'locales': locales,
|
||||||
|
'extratags': params.get_bool('extratags', False),
|
||||||
|
'namedetails': params.get_bool('namedetails', False),
|
||||||
|
'addressdetails': params.get_bool('addressdetails', True),
|
||||||
|
'single_result': True}
|
||||||
|
if fmt == 'xml':
|
||||||
|
fmt_options['xml_roottag'] = 'reversegeocode'
|
||||||
|
fmt_options['xml_extra_info'] = {'querystring': 'TODO'}
|
||||||
|
|
||||||
|
output = formatting.format_result(napi.ReverseResults([result] if result else []),
|
||||||
|
fmt, fmt_options)
|
||||||
|
|
||||||
|
return params.build_response(output)
|
||||||
|
|
||||||
|
|
||||||
EndpointFunc = Callable[[napi.NominatimAPIAsync, ASGIAdaptor], Any]
|
EndpointFunc = Callable[[napi.NominatimAPIAsync, ASGIAdaptor], Any]
|
||||||
|
|
||||||
|
REVERSE_MAX_RANKS = [2, 2, 2, # 0-2 Continent/Sea
|
||||||
|
4, 4, # 3-4 Country
|
||||||
|
8, # 5 State
|
||||||
|
10, 10, # 6-7 Region
|
||||||
|
12, 12, # 8-9 County
|
||||||
|
16, 17, # 10-11 City
|
||||||
|
18, # 12 Town
|
||||||
|
19, # 13 Village/Suburb
|
||||||
|
22, # 14 Hamlet/Neighbourhood
|
||||||
|
25, # 15 Localities
|
||||||
|
26, # 16 Major Streets
|
||||||
|
27, # 17 Minor Streets
|
||||||
|
30 # 18 Building
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
ROUTES = [
|
ROUTES = [
|
||||||
('status', status_endpoint),
|
('status', status_endpoint),
|
||||||
('details', details_endpoint)
|
('details', details_endpoint),
|
||||||
|
('reverse', reverse_endpoint)
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"""
|
"""
|
||||||
Server implementation using the falcon webserver framework.
|
Server implementation using the falcon webserver framework.
|
||||||
"""
|
"""
|
||||||
from typing import Optional, Mapping, cast
|
from typing import Optional, Mapping, cast, Any
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import falcon
|
import falcon
|
||||||
@@ -17,6 +17,22 @@ from nominatim.api import NominatimAPIAsync
|
|||||||
import nominatim.api.v1 as api_impl
|
import nominatim.api.v1 as api_impl
|
||||||
from nominatim.config import Configuration
|
from nominatim.config import Configuration
|
||||||
|
|
||||||
|
class HTTPNominatimError(Exception):
|
||||||
|
""" A special exception class for errors raised during processing.
|
||||||
|
"""
|
||||||
|
def __init__(self, msg: str, status: int, content_type: str) -> None:
|
||||||
|
self.msg = msg
|
||||||
|
self.status = status
|
||||||
|
self.content_type = content_type
|
||||||
|
|
||||||
|
|
||||||
|
async def nominatim_error_handler(req: Request, resp: Response,
|
||||||
|
exception: HTTPNominatimError,
|
||||||
|
_: Any) -> None:
|
||||||
|
resp.status = exception.status
|
||||||
|
resp.text = exception.msg
|
||||||
|
resp.content_type = exception.content_type
|
||||||
|
|
||||||
|
|
||||||
class ParamWrapper(api_impl.ASGIAdaptor):
|
class ParamWrapper(api_impl.ASGIAdaptor):
|
||||||
""" Adaptor class for server glue to Falcon framework.
|
""" Adaptor class for server glue to Falcon framework.
|
||||||
@@ -37,13 +53,8 @@ class ParamWrapper(api_impl.ASGIAdaptor):
|
|||||||
return cast(Optional[str], self.request.get_header(name, default=default))
|
return cast(Optional[str], self.request.get_header(name, default=default))
|
||||||
|
|
||||||
|
|
||||||
def error(self, msg: str, status: int = 400) -> falcon.HTTPError:
|
def error(self, msg: str, status: int = 400) -> HTTPNominatimError:
|
||||||
if status == 400:
|
return HTTPNominatimError(msg, status, self.content_type)
|
||||||
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) -> None:
|
def create_response(self, status: int, output: str) -> None:
|
||||||
@@ -78,6 +89,7 @@ def get_application(project_dir: Path,
|
|||||||
api = NominatimAPIAsync(project_dir, environ)
|
api = NominatimAPIAsync(project_dir, environ)
|
||||||
|
|
||||||
app = App(cors_enable=api.config.get_bool('CORS_NOACCESSCONTROL'))
|
app = App(cors_enable=api.config.get_bool('CORS_NOACCESSCONTROL'))
|
||||||
|
app.add_error_handler(HTTPNominatimError, nominatim_error_handler)
|
||||||
|
|
||||||
legacy_urls = api.config.get_bool('SERVE_LEGACY_URLS')
|
legacy_urls = api.config.get_bool('SERVE_LEGACY_URLS')
|
||||||
for name, func in api_impl.ROUTES:
|
for name, func in api_impl.ROUTES:
|
||||||
|
|||||||
@@ -35,7 +35,10 @@ class ParamWrapper(api_impl.ASGIAdaptor):
|
|||||||
|
|
||||||
|
|
||||||
def error(self, msg: str, status: int = 400) -> SanicException:
|
def error(self, msg: str, status: int = 400) -> SanicException:
|
||||||
return SanicException(msg, status_code=status)
|
exception = SanicException(msg, status_code=status)
|
||||||
|
exception.headers = {'content-type': self.content_type}
|
||||||
|
|
||||||
|
return exception
|
||||||
|
|
||||||
|
|
||||||
def create_response(self, status: int, output: str) -> HTTPResponse:
|
def create_response(self, status: int, output: str) -> HTTPResponse:
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ class ParamWrapper(api_impl.ASGIAdaptor):
|
|||||||
|
|
||||||
|
|
||||||
def error(self, msg: str, status: int = 400) -> HTTPException:
|
def error(self, msg: str, status: int = 400) -> HTTPException:
|
||||||
return HTTPException(status, detail=msg)
|
return HTTPException(status, detail=msg,
|
||||||
|
headers={'content-type': self.content_type})
|
||||||
|
|
||||||
|
|
||||||
def create_response(self, status: int, output: str) -> Response:
|
def create_response(self, status: int, output: str) -> Response:
|
||||||
|
|||||||
Reference in New Issue
Block a user