1 # SPDX-License-Identifier: GPL-3.0-or-later
3 # This file is part of Nominatim. (https://nominatim.org)
5 # Copyright (C) 2024 by the Nominatim developer community.
6 # For a full list of authors see the git log.
8 Base abstraction for implementing based on different ASGI frameworks.
10 from typing import Optional, Any, NoReturn, Callable
14 from ..config import Configuration
15 from .. import logging as loglib
16 from ..core import NominatimAPIAsync
17 from ..result_formatting import FormatDispatcher
19 CONTENT_TEXT = 'text/plain; charset=utf-8'
20 CONTENT_XML = 'text/xml; charset=utf-8'
21 CONTENT_HTML = 'text/html; charset=utf-8'
22 CONTENT_JSON = 'application/json; charset=utf-8'
24 CONTENT_TYPE = {'text': CONTENT_TEXT, 'xml': CONTENT_XML, 'debug': CONTENT_HTML}
26 class ASGIAdaptor(abc.ABC):
27 """ Adapter class for the different ASGI frameworks.
28 Wraps functionality over concrete requests and responses.
30 content_type: str = CONTENT_TEXT
34 def get(self, name: str, default: Optional[str] = None) -> Optional[str]:
35 """ Return an input parameter as a string. If the parameter was
36 not provided, return the 'default' value.
40 def get_header(self, name: str, default: Optional[str] = None) -> Optional[str]:
41 """ Return a HTTP header parameter as a string. If the parameter was
42 not provided, return the 'default' value.
47 def error(self, msg: str, status: int = 400) -> Exception:
48 """ Construct an appropriate exception from the given error message.
49 The exception must result in a HTTP error with the given status.
54 def create_response(self, status: int, output: str, num_results: int) -> Any:
55 """ Create a response from the given parameters. The result will
56 be returned by the endpoint functions. The adaptor may also
57 return None when the response is created internally with some
60 The response must return the HTTP given status code 'status', set
61 the HTTP content-type headers to the string provided and the
62 body of the response to 'output'.
67 def base_uri(self) -> str:
68 """ Return the URI of the original request.
73 def config(self) -> Configuration:
74 """ Return the current configuration object.
79 def formatting(self) -> FormatDispatcher:
80 """ Return the formatting object to use.
84 def get_int(self, name: str, default: Optional[int] = None) -> int:
85 """ Return an input parameter as an int. Raises an exception if
86 the parameter is given but not in an integer format.
88 If 'default' is given, then it will be returned when the parameter
89 is missing completely. When 'default' is None, an error will be
90 raised on a missing parameter.
92 value = self.get(name)
95 if default is not None:
98 self.raise_error(f"Parameter '{name}' missing.")
103 self.raise_error(f"Parameter '{name}' must be a number.")
108 def get_float(self, name: str, default: Optional[float] = None) -> float:
109 """ Return an input parameter as a flaoting-point number. Raises an
110 exception if the parameter is given but not in an float format.
112 If 'default' is given, then it will be returned when the parameter
113 is missing completely. When 'default' is None, an error will be
114 raised on a missing parameter.
116 value = self.get(name)
119 if default is not None:
122 self.raise_error(f"Parameter '{name}' missing.")
127 self.raise_error(f"Parameter '{name}' must be a number.")
129 if math.isnan(fval) or math.isinf(fval):
130 self.raise_error(f"Parameter '{name}' must be a number.")
135 def get_bool(self, name: str, default: Optional[bool] = None) -> bool:
136 """ Return an input parameter as bool. Only '0' is accepted as
137 an input for 'false' all other inputs will be interpreted as 'true'.
139 If 'default' is given, then it will be returned when the parameter
140 is missing completely. When 'default' is None, an error will be
141 raised on a missing parameter.
143 value = self.get(name)
146 if default is not None:
149 self.raise_error(f"Parameter '{name}' missing.")
154 def raise_error(self, msg: str, status: int = 400) -> NoReturn:
155 """ Raise an exception resulting in the given HTTP status and
156 message. The message will be formatted according to the
157 output format chosen by the request.
159 if self.content_type == CONTENT_XML:
160 msg = f"""<?xml version="1.0" encoding="UTF-8" ?>
162 <code>{status}</code>
163 <message>{msg}</message>
166 elif self.content_type == CONTENT_JSON:
167 msg = f"""{{"error":{{"code":{status},"message":"{msg}"}}}}"""
168 elif self.content_type == CONTENT_HTML:
169 loglib.log().section('Execution error')
170 loglib.log().var_dump('Status', status)
171 loglib.log().var_dump('Message', msg)
172 msg = loglib.get_and_disable()
174 raise self.error(msg, status)
177 EndpointFunc = Callable[[NominatimAPIAsync, ASGIAdaptor], Any]