Skip to content

Commit d7cf81c

Browse files
authored
Merge pull request #3515 from lonvia/custom-result-formatting
Add the capability to define custom formatting functions for API output
2 parents 4f4a288 + 19eb4d9 commit d7cf81c

14 files changed

+615
-299
lines changed

docs/customize/Overview.md

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ the following configurable parts:
77
can be set in your local `.env` configuration
88
* [Import styles](Import-Styles.md) explains how to write your own import style
99
in order to control what kind of OSM data will be imported
10+
* [API Result Formatting](Result-Formatting.md) shows how to change the
11+
output of the Nominatim API
1012
* [Place ranking](Ranking.md) describes the configuration around classifing
1113
places in terms of their importance and their role in an address
1214
* [Tokenizers](Tokenizers.md) describes the configuration of the module

docs/customize/Result-Formatting.md

+176
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
# Changing the Appearance of Results in the Server API
2+
3+
The Nominatim Server API offers a number of formatting options that
4+
present search results in [different output formats](../api/Output.md).
5+
These results only contain a subset of all the information that Nominatim
6+
has about the result. This page explains how to adapt the result output
7+
or add additional result formatting.
8+
9+
## Defining custom result formatting
10+
11+
To change the result output, you need to place a file `api/v1/format.py`
12+
into your project directory. This file needs to define a single variable
13+
`dispatch` containing a [FormatDispatcher](#formatdispatcher). This class
14+
serves to collect the functions for formatting the different result types
15+
and offers helper functions to apply the formatters.
16+
17+
There are two ways to define the `dispatch` variable. If you want to reuse
18+
the default output formatting and just make some changes or add an additional
19+
format type, then import the dispatch object from the default API:
20+
21+
``` python
22+
from nominatim_api.v1.format import dispatch as dispatch
23+
```
24+
25+
If you prefer to define a completely new result output, then you can
26+
create an empty dispatcher object:
27+
28+
``` python
29+
from nominatim_api import FormatDispatcher
30+
31+
dispatch = FormatDispatcher()
32+
```
33+
34+
## The formatting function
35+
36+
The dispatcher organises the formatting functions by format and result type.
37+
The format corresponds to the `format` parameter of the API. It can contain
38+
one of the predefined format names or you can invent your own new format.
39+
40+
API calls return data classes or an array of a data class which represent
41+
the result. You need to make sure there are formatters defined for the
42+
following result types:
43+
44+
* StatusResult (single object, returned by `/status`)
45+
* DetailedResult (single object, returned by `/details`)
46+
* SearchResults (list of objects, returned by `/search`)
47+
* ReverseResults (list of objects, returned by `/reverse` and `/lookup`)
48+
* RawDataList (simple object, returned by `/deletable` and `/polygons`)
49+
50+
A formatter function has the following signature:
51+
52+
``` python
53+
def format_func(result: ResultType, options: Mapping[str, Any]) -> str
54+
```
55+
56+
The options dictionary contains additional information about the original
57+
query. See the [reference below](#options-for-different-result-types)
58+
about the possible options.
59+
60+
To set the result formatter for a certain result type and format, you need
61+
to write the format function and decorate it with the
62+
[`format_func`](#nominatim_api.FormatDispatcher.format_func)
63+
decorator.
64+
65+
For example, let us extend the result for the status call in text format
66+
and add the server URL. Such a formatter would look like this:
67+
68+
``` python
69+
@dispatch.format_func(StatusResult, 'text')
70+
def _format_status_text(result, _):
71+
header = 'Status for server nominatim.openstreetmap.org'
72+
if result.status:
73+
return f"{header}\n\nERROR: {result.message}"
74+
75+
return f"{header}\n\nOK"
76+
```
77+
78+
If your dispatcher is derived from the default one, then this definition
79+
will overwrite the original formatter function. This way it is possible
80+
to customize the output of selected results.
81+
82+
## Adding new formats
83+
84+
You may also define a completely different output format. This is as simple
85+
as adding formatting functions for all result types using the custom
86+
format name:
87+
88+
``` python
89+
@dispatch.format_func(StatusResult, 'chatty')
90+
def _format_status_text(result, _):
91+
if result.status:
92+
return f"The server is currently not running. {result.message}"
93+
94+
return f"Good news! The server is running just fine."
95+
```
96+
97+
That's all. Nominatim will automatically pick up the new format name and
98+
will allow the user to use it. Make sure to really define formatters for
99+
**all** result types. If they are for endpoints that you do not intend to
100+
use, you can simply return some static string but the function needs to be
101+
there.
102+
103+
All responses will be returned with the content type application/json by
104+
default. If your format produces a different content type, you need
105+
to configure the content type with the `set_content_type()` function.
106+
107+
For example, the 'chatty' format above returns just simple text. So the
108+
content type should be set up as:
109+
110+
``` python
111+
from nominatim_api.server.content_types import CONTENT_TEXT
112+
113+
dispatch.set_content_type('chatty', CONTENT_TEXT)
114+
```
115+
116+
The `content_types` module used above provides constants for the most
117+
frequent content types. You set the content type to an arbitrary string,
118+
if the content type you need is not available.
119+
120+
## Reference
121+
122+
### FormatDispatcher
123+
124+
::: nominatim_api.FormatDispatcher
125+
options:
126+
heading_level: 6
127+
group_by_category: False
128+
129+
### JsonWriter
130+
131+
::: nominatim_api.utils.json_writer.JsonWriter
132+
options:
133+
heading_level: 6
134+
group_by_category: False
135+
136+
### Options for different result types
137+
138+
This section lists the options that may be handed in with the different result
139+
types in the v1 version of the Nominatim API.
140+
141+
#### StatusResult
142+
143+
_None._
144+
145+
#### DetailedResult
146+
147+
| Option | Description |
148+
|-----------------|-------------|
149+
| locales | [Locale](../library/Result-Handling.md#locale) object for the requested language(s) |
150+
| group_hierarchy | Setting of [group_hierarchy](../api/Details.md#output-details) parameter |
151+
| icon_base_url | (optional) URL pointing to icons as set in [NOMINATIM_MAPICON_URL](Settings.md#nominatim_mapicon_url) |
152+
153+
#### SearchResults
154+
155+
| Option | Description |
156+
|-----------------|-------------|
157+
| query | Original query string |
158+
| more_url | URL for requesting additional results for the same query |
159+
| exclude_place_ids | List of place IDs already returned |
160+
| viewbox | Setting of [viewbox](../api/Search.md#result-restriction) parameter |
161+
| extratags | Setting of [extratags](../api/Search.md#output-details) parameter |
162+
| namedetails | Setting of [namedetails](../api/Search.md#output-details) parameter |
163+
| addressdetails | Setting of [addressdetails](../api/Search.md#output-details) parameter |
164+
165+
#### ReverseResults
166+
167+
| Option | Description |
168+
|-----------------|-------------|
169+
| query | Original query string |
170+
| extratags | Setting of [extratags](../api/Search.md#output-details) parameter |
171+
| namedetails | Setting of [namedetails](../api/Search.md#output-details) parameter |
172+
| addressdetails | Setting of [addressdetails](../api/Search.md#output-details) parameter |
173+
174+
#### RawDataList
175+
176+
_None._

mkdocs.yml

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ nav:
3535
- 'Overview': 'customize/Overview.md'
3636
- 'Import Styles': 'customize/Import-Styles.md'
3737
- 'Configuration Settings': 'customize/Settings.md'
38+
- 'API Result Formatting': 'customize/Result-Formatting.md'
3839
- 'Per-Country Data': 'customize/Country-Settings.md'
3940
- 'Place Ranking' : 'customize/Ranking.md'
4041
- 'Importance' : 'customize/Importance.md'

src/nominatim_api/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,6 @@
3939
SearchResult as SearchResult,
4040
SearchResults as SearchResults)
4141
from .localization import (Locales as Locales)
42+
from .result_formatting import (FormatDispatcher as FormatDispatcher)
4243

4344
from .version import NOMINATIM_API_VERSION as __version__

src/nominatim_api/result_formatting.py

+75-4
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,28 @@
77
"""
88
Helper classes and functions for formatting results into API responses.
99
"""
10-
from typing import Type, TypeVar, Dict, List, Callable, Any, Mapping
10+
from typing import Type, TypeVar, Dict, List, Callable, Any, Mapping, Optional, cast
1111
from collections import defaultdict
12+
from pathlib import Path
13+
import importlib
14+
15+
from .server.content_types import CONTENT_JSON
1216

1317
T = TypeVar('T') # pylint: disable=invalid-name
1418
FormatFunc = Callable[[T, Mapping[str, Any]], str]
19+
ErrorFormatFunc = Callable[[str, str, int], str]
1520

1621

1722
class FormatDispatcher:
18-
""" Helper class to conveniently create formatting functions in
19-
a module using decorators.
23+
""" Container for formatting functions for results.
24+
Functions can conveniently be added by using decorated functions.
2025
"""
2126

22-
def __init__(self) -> None:
27+
def __init__(self, content_types: Optional[Mapping[str, str]] = None) -> None:
28+
self.error_handler: ErrorFormatFunc = lambda ct, msg, status: f"ERROR {status}: {msg}"
29+
self.content_types: Dict[str, str] = {}
30+
if content_types:
31+
self.content_types.update(content_types)
2332
self.format_functions: Dict[Type[Any], Dict[str, FormatFunc[Any]]] = defaultdict(dict)
2433

2534

@@ -35,6 +44,15 @@ def decorator(func: FormatFunc[T]) -> FormatFunc[T]:
3544
return decorator
3645

3746

47+
def error_format_func(self, func: ErrorFormatFunc) -> ErrorFormatFunc:
48+
""" Decorator for a function that formats error messges.
49+
There is only one error formatter per dispatcher. Using
50+
the decorator repeatedly will overwrite previous functions.
51+
"""
52+
self.error_handler = func
53+
return func
54+
55+
3856
def list_formats(self, result_type: Type[Any]) -> List[str]:
3957
""" Return a list of formats supported by this formatter.
4058
"""
@@ -54,3 +72,56 @@ def format_result(self, result: Any, fmt: str, options: Mapping[str, Any]) -> st
5472
`list_formats()`.
5573
"""
5674
return self.format_functions[type(result)][fmt](result, options)
75+
76+
77+
def format_error(self, content_type: str, msg: str, status: int) -> str:
78+
""" Convert the given error message into a response string
79+
taking the requested content_type into account.
80+
81+
Change the format using the error_format_func decorator.
82+
"""
83+
return self.error_handler(content_type, msg, status)
84+
85+
86+
def set_content_type(self, fmt: str, content_type: str) -> None:
87+
""" Set the content type for the given format. This is the string
88+
that will be returned in the Content-Type header of the HTML
89+
response, when the given format is choosen.
90+
"""
91+
self.content_types[fmt] = content_type
92+
93+
94+
def get_content_type(self, fmt: str) -> str:
95+
""" Return the content type for the given format.
96+
97+
If no explicit content type has been defined, then
98+
JSON format is assumed.
99+
"""
100+
return self.content_types.get(fmt, CONTENT_JSON)
101+
102+
103+
def load_format_dispatcher(api_name: str, project_dir: Optional[Path]) -> FormatDispatcher:
104+
""" Load the dispatcher for the given API.
105+
106+
The function first tries to find a module api/<api_name>/format.py
107+
in the project directory. This file must export a single variable
108+
`dispatcher`.
109+
110+
If the function does not exist, the default formatter is loaded.
111+
"""
112+
if project_dir is not None:
113+
priv_module = project_dir / 'api' / api_name / 'format.py'
114+
if priv_module.is_file():
115+
spec = importlib.util.spec_from_file_location(f'api.{api_name},format',
116+
str(priv_module))
117+
if spec:
118+
module = importlib.util.module_from_spec(spec)
119+
# Do not add to global modules because there is no standard
120+
# module name that Python can resolve.
121+
assert spec.loader is not None
122+
spec.loader.exec_module(module)
123+
124+
return cast(FormatDispatcher, module.dispatch)
125+
126+
return cast(FormatDispatcher,
127+
importlib.import_module(f'nominatim_api.{api_name}.format').dispatch)

0 commit comments

Comments
 (0)