1 # SPDX-License-Identifier: GPL-2.0-only
3 # This file is part of Nominatim. (https://nominatim.org)
5 # Copyright (C) 2023 by the Nominatim developer community.
6 # For a full list of authors see the git log.
8 Generic part of the server implementation of the v1 API.
9 Combine with the scaffolding provided for the various Python ASGI frameworks.
11 from typing import Optional, Any, Type, Callable
14 from nominatim.config import Configuration
15 import nominatim.api as napi
16 import nominatim.api.logging as loglib
17 from nominatim.api.v1.format import dispatch as formatting
20 'text': 'text/plain; charset=utf-8',
21 'xml': 'text/xml; charset=utf-8',
22 'jsonp': 'application/javascript',
23 'debug': 'text/html; charset=utf-8'
27 class ASGIAdaptor(abc.ABC):
28 """ Adapter class for the different ASGI frameworks.
29 Wraps functionality over concrete requests and responses.
33 def get(self, name: str, default: Optional[str] = None) -> Optional[str]:
34 """ Return an input parameter as a string. If the parameter was
35 not provided, return the 'default' value.
39 def get_header(self, name: str, default: Optional[str] = None) -> Optional[str]:
40 """ Return a HTTP header parameter as a string. If the parameter was
41 not provided, return the 'default' value.
46 def error(self, msg: str, status: int = 400) -> Exception:
47 """ Construct an appropriate exception from the given error message.
48 The exception must result in a HTTP error with the given status.
53 def create_response(self, status: int, output: str, content_type: str) -> Any:
54 """ Create a response from the given parameters. The result will
55 be returned by the endpoint functions. The adaptor may also
56 return None when the response is created internally with some
59 The response must return the HTTP given status code 'status', set
60 the HTTP content-type headers to the string provided and the
61 body of the response to 'output'.
66 def config(self) -> Configuration:
67 """ Return the current configuration object.
71 def build_response(self, output: str, media_type: str, status: int = 200) -> Any:
72 """ Create a response from the given output. Wraps a JSONP function
73 around the response, if necessary.
75 if media_type == 'json' and status == 200:
76 jsonp = self.get('json_callback')
78 if any(not part.isidentifier() for part in jsonp.split('.')):
79 raise self.error('Invalid json_callback value')
80 output = f"{jsonp}({output})"
83 return self.create_response(status, output,
84 CONTENT_TYPE.get(media_type, 'application/json'))
87 def get_int(self, name: str, default: Optional[int] = None) -> int:
88 """ Return an input parameter as an int. Raises an exception if
89 the parameter is given but not in an integer format.
91 If 'default' is given, then it will be returned when the parameter
92 is missing completely. When 'default' is None, an error will be
93 raised on a missing parameter.
95 value = self.get(name)
98 if default is not None:
101 raise self.error(f"Parameter '{name}' missing.")
105 except ValueError as exc:
106 raise self.error(f"Parameter '{name}' must be a number.") from exc
109 def get_bool(self, name: str, default: Optional[bool] = None) -> bool:
110 """ Return an input parameter as bool. Only '0' is accepted as
111 an input for 'false' all other inputs will be interpreted as 'true'.
113 If 'default' is given, then it will be returned when the parameter
114 is missing completely. When 'default' is None, an error will be
115 raised on a missing parameter.
117 value = self.get(name)
120 if default is not None:
123 raise self.error(f"Parameter '{name}' missing.")
128 def get_accepted_languages(self) -> str:
129 """ Return the accepted langauges.
131 return self.get('accept-language')\
132 or self.get_header('http_accept_language')\
133 or self.config().DEFAULT_LANGUAGE
136 def setup_debugging(self) -> bool:
137 """ Set up collection of debug information if requested.
139 Return True when debugging was requested.
141 if self.get_bool('debug', False):
142 loglib.set_log_output('html')
148 def parse_format(params: ASGIAdaptor, result_type: Type[Any], default: str) -> str:
149 """ Get and check the 'format' parameter and prepare the formatter.
150 `fmtter` is a formatter and `default` the
151 format value to assume when no parameter is present.
153 fmt = params.get('format', default=default)
154 assert fmt is not None
156 if not formatting.supports_format(result_type, fmt):
157 raise params.error("Parameter 'format' must be one of: " +
158 ', '.join(formatting.list_formats(result_type)))
163 async def status_endpoint(api: napi.NominatimAPIAsync, params: ASGIAdaptor) -> Any:
164 """ Server glue for /status endpoint. See API docs for details.
166 result = await api.status()
168 fmt = parse_format(params, napi.StatusResult, 'text')
170 if fmt == 'text' and result.status:
175 return params.build_response(formatting.format_result(result, fmt, {}), fmt,
179 async def details_endpoint(api: napi.NominatimAPIAsync, params: ASGIAdaptor) -> Any:
180 """ Server glue for /details endpoint. See API docs for details.
182 place_id = params.get_int('place_id', 0)
185 place = napi.PlaceID(place_id)
187 osmtype = params.get('osmtype')
189 raise params.error("Missing ID parameter 'place_id' or 'osmtype'.")
190 place = napi.OsmID(osmtype, params.get_int('osmid'), params.get('class'))
192 debug = params.setup_debugging()
194 details = napi.LookupDetails(address_details=params.get_bool('addressdetails', False),
195 linked_places=params.get_bool('linkedplaces', False),
196 parented_places=params.get_bool('hierarchy', False),
197 keywords=params.get_bool('keywords', False))
199 if params.get_bool('polygon_geojson', False):
200 details.geometry_output = napi.GeometryFormat.GEOJSON
202 locales = napi.Locales.from_accept_languages(params.get_accepted_languages())
204 result = await api.lookup(place, details)
207 return params.build_response(loglib.get_and_disable(), 'debug')
210 raise params.error('No place with that OSM ID found.', status=404)
212 output = formatting.format_result(
216 'group_hierarchy': params.get_bool('group_hierarchy', False),
217 'icon_base_url': params.config().MAPICON_URL})
219 return params.build_response(output, 'json')
222 EndpointFunc = Callable[[napi.NominatimAPIAsync, ASGIAdaptor], Any]
225 ('status', status_endpoint),
226 ('details', details_endpoint)