Merge pull request #3517 from lonvia/improve-custom-formatter

Extend use of custom result formatters to CLI tool
This commit is contained in:
Sarah Hoffmann
2024-08-18 10:20:53 +02:00
committed by GitHub
9 changed files with 273 additions and 126 deletions

View File

@@ -66,6 +66,8 @@ For example, let us extend the result for the status call in text format
and add the server URL. Such a formatter would look like this:
``` python
from nominatim_api import StatusResult
@dispatch.format_func(StatusResult, 'text')
def _format_status_text(result, _):
header = 'Status for server nominatim.openstreetmap.org'
@@ -86,19 +88,39 @@ as adding formatting functions for all result types using the custom
format name:
``` python
from nominatim_api import StatusResult
@dispatch.format_func(StatusResult, 'chatty')
def _format_status_text(result, _):
if result.status:
return f"The server is currently not running. {result.message}"
return f"Good news! The server is running just fine."
return "Good news! The server is running just fine."
```
That's all. Nominatim will automatically pick up the new format name and
will allow the user to use it. Make sure to really define formatters for
**all** result types. If they are for endpoints that you do not intend to
use, you can simply return some static string but the function needs to be
there.
will allow the user to use it. There is no need to implement formatter
functions for all the result types, when you invent a new one. The
available formats will be determined for each API endpoint separately.
To find out which formats are available, you can use the `--list-formats`
option of the CLI tool:
```
me@machine:planet-project$ nominatim status --list-formats
2024-08-16 19:54:00: Using project directory: /home/nominatim/planet-project
text
json
chatty
debug
me@machine:planet-project$
```
The `debug` format listed in the last line will always appear. It is a
special format that enables debug output via the command line (the same
as the `debug=1` parameter enables for the server API). To not clash
with this built-in function, you shouldn't name your own format 'debug'.
### Content type of new formats
All responses will be returned with the content type application/json by
default. If your format produces a different content type, you need
@@ -117,6 +139,67 @@ The `content_types` module used above provides constants for the most
frequent content types. You set the content type to an arbitrary string,
if the content type you need is not available.
## Formatting error messages
Any exception thrown during processing of a request is given to
a special error formatting function. It takes the requested content type,
the status code and the error message. It should return the error message
in a form appropriate for the given content type.
You can overwrite the default formatting function with the decorator
`error_format_func`:
``` python
import nominatim_api.server.content_types as ct
@dispatch.error_format_func
def _format_error(content_type: str, msg: str, status: int) -> str:
if content_type == ct.CONTENT_XML:
return f"""<?xml version="1.0" encoding="UTF-8" ?>
<message>{msg}</message>
"""
if content_type == ct.CONTENT_JSON:
return f'"{msg}"'
return f"ERROR: {msg}"
```
## Debugging custom formatters
The easiest way to try out your custom formatter is by using the Nominatim
CLI commands. Custom formats can be chosen with the `--format` parameter:
```
me@machine:planet-project$ nominatim status --format chatty
2024-08-16 19:54:00: Using project directory: /home/nominatim/planet-project
Good news! The server is running just fine.
me@machine:planet-project$
```
They will also emit full error messages when there is a problem with the
code you need to debug.
!!! danger
In some cases, when you make an error with your import statement, the
CLI will not give you an error but instead tell you, that the API
commands are no longer available:
me@machine: nominatim status
usage: nominatim [-h] [--version] {import,freeze,replication,special-phrases,add-data,index,refresh,admin} ...
nominatim: error: argument subcommand: invalid choice: 'status'
This happens because the CLI tool is meant to still work when the
nominatim-api package is not installed. Import errors involving
`nominatim_api` are interpreted as "package not installed".
Use the help command to find out which is the offending import that
could not be found:
me@machine: nominatim -h
... [other help text] ...
Nominatim API package not found (was looking for module: nominatim_api.xxx).
## Reference
### FormatDispatcher

View File

@@ -39,6 +39,7 @@ from .results import (SourceTable as SourceTable,
SearchResult as SearchResult,
SearchResults as SearchResults)
from .localization import (Locales as Locales)
from .result_formatting import (FormatDispatcher as FormatDispatcher)
from .result_formatting import (FormatDispatcher as FormatDispatcher,
load_format_dispatcher as load_format_dispatcher)
from .version import NOMINATIM_API_VERSION as __version__

View File

@@ -11,9 +11,3 @@ Implementation of API version v1 (aka the legacy version).
#pylint: disable=useless-import-alias
from .server_glue import ROUTES as ROUTES
from . import format as _format
list_formats = _format.dispatch.list_formats
supports_format = _format.dispatch.supports_format
format_result = _format.dispatch.format_result

View File

@@ -243,7 +243,8 @@ def get_set_parser() -> CommandlineParser:
raise ex
parser.parser.epilog = \
'\n\nNominatim API package not found. The following commands are not available:'\
f'\n\nNominatim API package not found (was looking for module: {ex.name}).'\
'\nThe following commands are not available:'\
'\n export, convert, serve, search, reverse, lookup, details, status'\
"\n\nRun 'pip install nominatim-api' to install the package."

View File

@@ -7,7 +7,7 @@
"""
Subcommand definitions for API calls from the command line.
"""
from typing import Dict, Any, Optional
from typing import Dict, Any, Optional, Type, Mapping
import argparse
import logging
import json
@@ -15,9 +15,8 @@ import sys
from functools import reduce
import nominatim_api as napi
import nominatim_api.v1 as api_output
from nominatim_api.v1.helpers import zoom_to_rank, deduplicate_results
from nominatim_api.v1.format import dispatch as formatting
from nominatim_api.server.content_types import CONTENT_JSON
import nominatim_api.logging as loglib
from ..errors import UsageError
from .args import NominatimArgs
@@ -44,11 +43,16 @@ EXTRADATA_PARAMS = (
('namedetails', 'Include a list of alternative names')
)
def _add_list_format(parser: argparse.ArgumentParser) -> None:
group = parser.add_argument_group('Other options')
group.add_argument('--list-formats', action='store_true',
help='List supported output formats and exit.')
def _add_api_output_arguments(parser: argparse.ArgumentParser) -> None:
group = parser.add_argument_group('Output arguments')
group.add_argument('--format', default='jsonv2',
choices=formatting.list_formats(napi.SearchResults) + ['debug'],
help='Format of result')
group = parser.add_argument_group('Output formatting')
group.add_argument('--format', type=str, default='jsonv2',
help='Format of result (use --list-format to see supported formats)')
for name, desc in EXTRADATA_PARAMS:
group.add_argument('--' + name, action='store_true', help=desc)
@@ -105,6 +109,30 @@ def _get_layers(args: NominatimArgs, default: napi.DataLayer) -> Optional[napi.D
(napi.DataLayer[s.upper()] for s in args.layers))
def _list_formats(formatter: napi.FormatDispatcher, rtype: Type[Any]) -> int:
for fmt in formatter.list_formats(rtype):
print(fmt)
print('debug')
return 0
def _print_output(formatter: napi.FormatDispatcher, result: Any,
fmt: str, options: Mapping[str, Any]) -> None:
output = formatter.format_result(result, fmt, options)
if formatter.get_content_type(fmt) == CONTENT_JSON:
# reformat the result, so it is pretty-printed
try:
json.dump(json.loads(output), sys.stdout, indent=4, ensure_ascii=False)
except json.decoder.JSONDecodeError as err:
# Catch the error here, so that data can be debugged,
# when people are developping custom result formatters.
LOG.fatal("Parsing json failed: %s\nUnformatted output:\n%s", err, output)
else:
sys.stdout.write(output)
sys.stdout.write('\n')
class APISearch:
"""\
Execute a search query.
@@ -135,18 +163,24 @@ class APISearch:
help='Preferred area to find search results')
group.add_argument('--bounded', action='store_true',
help='Strictly restrict results to viewbox area')
group = parser.add_argument_group('Other arguments')
group.add_argument('--no-dedupe', action='store_false', dest='dedupe',
help='Do not remove duplicates from the result list')
_add_list_format(parser)
def run(self, args: NominatimArgs) -> int:
formatter = napi.load_format_dispatcher('v1', args.project_dir)
if args.list_formats:
return _list_formats(formatter, napi.SearchResults)
if args.format == 'debug':
loglib.set_log_output('text')
elif not formatter.supports_format(napi.SearchResults, args.format):
raise UsageError(f"Unsupported format '{args.format}'. "
'Use --list-formats to see supported formats.')
api = napi.NominatimAPI(args.project_dir)
params: Dict[str, Any] = {'max_results': args.limit + min(args.limit, 10),
'address_details': True, # needed for display name
'geometry_output': _get_geometry_output(args),
@@ -177,19 +211,10 @@ class APISearch:
print(loglib.get_and_disable())
return 0
output = api_output.format_result(
results,
args.format,
{'extratags': args.extratags,
'namedetails': args.namedetails,
'addressdetails': args.addressdetails})
if args.format != 'xml':
# reformat the result, so it is pretty-printed
json.dump(json.loads(output), sys.stdout, indent=4, ensure_ascii=False)
else:
sys.stdout.write(output)
sys.stdout.write('\n')
_print_output(formatter, results, args.format,
{'extratags': args.extratags,
'namedetails': args.namedetails,
'addressdetails': args.addressdetails})
return 0
@@ -205,9 +230,9 @@ class APIReverse:
def add_args(self, parser: argparse.ArgumentParser) -> None:
group = parser.add_argument_group('Query arguments')
group.add_argument('--lat', type=float, required=True,
group.add_argument('--lat', type=float,
help='Latitude of coordinate to look up (in WGS84)')
group.add_argument('--lon', type=float, required=True,
group.add_argument('--lon', type=float,
help='Longitude of coordinate to look up (in WGS84)')
group.add_argument('--zoom', type=int,
help='Level of detail required for the address')
@@ -217,14 +242,25 @@ class APIReverse:
help='OSM id to lookup in format <NRW><id> (may be repeated)')
_add_api_output_arguments(parser)
_add_list_format(parser)
def run(self, args: NominatimArgs) -> int:
formatter = napi.load_format_dispatcher('v1', args.project_dir)
if args.list_formats:
return _list_formats(formatter, napi.ReverseResults)
if args.format == 'debug':
loglib.set_log_output('text')
elif not formatter.supports_format(napi.ReverseResults, args.format):
raise UsageError(f"Unsupported format '{args.format}'. "
'Use --list-formats to see supported formats.')
if args.lat is None or args.lon is None:
raise UsageError("lat' and 'lon' parameters are required.")
api = napi.NominatimAPI(args.project_dir)
result = api.reverse(napi.Point(args.lon, args.lat),
max_rank=zoom_to_rank(args.zoom or 18),
layers=_get_layers(args, napi.DataLayer.ADDRESS | napi.DataLayer.POI),
@@ -238,18 +274,10 @@ class APIReverse:
return 0
if result:
output = api_output.format_result(
napi.ReverseResults([result]),
args.format,
{'extratags': args.extratags,
'namedetails': args.namedetails,
'addressdetails': args.addressdetails})
if args.format != 'xml':
# reformat the result, so it is pretty-printed
json.dump(json.loads(output), sys.stdout, indent=4, ensure_ascii=False)
else:
sys.stdout.write(output)
sys.stdout.write('\n')
_print_output(formatter, napi.ReverseResults([result]), args.format,
{'extratags': args.extratags,
'namedetails': args.namedetails,
'addressdetails': args.addressdetails})
return 0
@@ -271,43 +299,45 @@ class APILookup:
def add_args(self, parser: argparse.ArgumentParser) -> None:
group = parser.add_argument_group('Query arguments')
group.add_argument('--id', metavar='OSMID',
action='append', required=True, dest='ids',
action='append', dest='ids',
help='OSM id to lookup in format <NRW><id> (may be repeated)')
_add_api_output_arguments(parser)
_add_list_format(parser)
def run(self, args: NominatimArgs) -> int:
formatter = napi.load_format_dispatcher('v1', args.project_dir)
if args.list_formats:
return _list_formats(formatter, napi.ReverseResults)
if args.format == 'debug':
loglib.set_log_output('text')
elif not formatter.supports_format(napi.ReverseResults, args.format):
raise UsageError(f"Unsupported format '{args.format}'. "
'Use --list-formats to see supported formats.')
api = napi.NominatimAPI(args.project_dir)
if args.format == 'debug':
print(loglib.get_and_disable())
return 0
if args.ids is None:
raise UsageError("'id' parameter required.")
places = [napi.OsmID(o[0], int(o[1:])) for o in args.ids]
api = napi.NominatimAPI(args.project_dir)
results = api.lookup(places,
address_details=True, # needed for display name
geometry_output=_get_geometry_output(args),
geometry_simplification=args.polygon_threshold or 0.0,
locales=_get_locales(args, api.config.DEFAULT_LANGUAGE))
output = api_output.format_result(
results,
args.format,
{'extratags': args.extratags,
'namedetails': args.namedetails,
'addressdetails': args.addressdetails})
if args.format != 'xml':
# reformat the result, so it is pretty-printed
json.dump(json.loads(output), sys.stdout, indent=4, ensure_ascii=False)
else:
sys.stdout.write(output)
sys.stdout.write('\n')
if args.format == 'debug':
print(loglib.get_and_disable())
return 0
_print_output(formatter, results, args.format,
{'extratags': args.extratags,
'namedetails': args.namedetails,
'addressdetails': args.addressdetails})
return 0
@@ -323,20 +353,21 @@ class APIDetails:
def add_args(self, parser: argparse.ArgumentParser) -> None:
group = parser.add_argument_group('Query arguments')
objs = group.add_mutually_exclusive_group(required=True)
objs.add_argument('--node', '-n', type=int,
help="Look up the OSM node with the given ID.")
objs.add_argument('--way', '-w', type=int,
help="Look up the OSM way with the given ID.")
objs.add_argument('--relation', '-r', type=int,
help="Look up the OSM relation with the given ID.")
objs.add_argument('--place_id', '-p', type=int,
help='Database internal identifier of the OSM object to look up')
group.add_argument('--node', '-n', type=int,
help="Look up the OSM node with the given ID.")
group.add_argument('--way', '-w', type=int,
help="Look up the OSM way with the given ID.")
group.add_argument('--relation', '-r', type=int,
help="Look up the OSM relation with the given ID.")
group.add_argument('--place_id', '-p', type=int,
help='Database internal identifier of the OSM object to look up')
group.add_argument('--class', dest='object_class',
help=("Class type to disambiguated multiple entries "
"of the same object."))
group = parser.add_argument_group('Output arguments')
group.add_argument('--format', type=str, default='json',
help='Format of result (use --list-formats to see supported formats)')
group.add_argument('--addressdetails', action='store_true',
help='Include a breakdown of the address into elements')
group.add_argument('--keywords', action='store_true',
@@ -351,9 +382,21 @@ class APIDetails:
help='Include geometry of result')
group.add_argument('--lang', '--accept-language', metavar='LANGS',
help='Preferred language order for presenting search results')
_add_list_format(parser)
def run(self, args: NominatimArgs) -> int:
formatter = napi.load_format_dispatcher('v1', args.project_dir)
if args.list_formats:
return _list_formats(formatter, napi.DetailedResult)
if args.format == 'debug':
loglib.set_log_output('text')
elif not formatter.supports_format(napi.DetailedResult, args.format):
raise UsageError(f"Unsupported format '{args.format}'. "
'Use --list-formats to see supported formats.')
place: napi.PlaceRef
if args.node:
place = napi.OsmID('N', args.node, args.object_class)
@@ -361,12 +404,13 @@ class APIDetails:
place = napi.OsmID('W', args.way, args.object_class)
elif args.relation:
place = napi.OsmID('R', args.relation, args.object_class)
else:
assert args.place_id is not None
elif args.place_id is not None:
place = napi.PlaceID(args.place_id)
else:
raise UsageError('One of the arguments --node/-n --way/-w '
'--relation/-r --place_id/-p is required/')
api = napi.NominatimAPI(args.project_dir)
locales = _get_locales(args, api.config.DEFAULT_LANGUAGE)
result = api.details(place,
address_details=args.addressdetails,
@@ -378,17 +422,14 @@ class APIDetails:
else napi.GeometryFormat.NONE,
locales=locales)
if args.format == 'debug':
print(loglib.get_and_disable())
return 0
if result:
output = api_output.format_result(
result,
'json',
{'locales': locales,
'group_hierarchy': args.group_hierarchy})
# reformat the result, so it is pretty-printed
json.dump(json.loads(output), sys.stdout, indent=4, ensure_ascii=False)
sys.stdout.write('\n')
_print_output(formatter, result, args.format or 'json',
{'locales': locales,
'group_hierarchy': args.group_hierarchy})
return 0
LOG.error("Object not found in database.")
@@ -406,13 +447,30 @@ class APIStatus:
"""
def add_args(self, parser: argparse.ArgumentParser) -> None:
formats = api_output.list_formats(napi.StatusResult)
group = parser.add_argument_group('API parameters')
group.add_argument('--format', default=formats[0], choices=formats,
help='Format of result')
group.add_argument('--format', type=str, default='text',
help='Format of result (use --list-formats to see supported formats)')
_add_list_format(parser)
def run(self, args: NominatimArgs) -> int:
formatter = napi.load_format_dispatcher('v1', args.project_dir)
if args.list_formats:
return _list_formats(formatter, napi.StatusResult)
if args.format == 'debug':
loglib.set_log_output('text')
elif not formatter.supports_format(napi.StatusResult, args.format):
raise UsageError(f"Unsupported format '{args.format}'. "
'Use --list-formats to see supported formats.')
status = napi.NominatimAPI(args.project_dir).status()
print(api_output.format_result(status, args.format, {}))
if args.format == 'debug':
print(loglib.get_and_disable())
return 0
_print_output(formatter, status, args.format, {})
return 0

View File

@@ -137,6 +137,7 @@ class NominatimArgs:
# Arguments to all query functions
format: str
list_formats: bool
addressdetails: bool
extratags: bool
namedetails: bool

View File

@@ -15,7 +15,7 @@ import json
import pytest
import nominatim_api.v1 as api_impl
from nominatim_api.v1.format import dispatch as v1_format
import nominatim_api as napi
STATUS_FORMATS = {'text', 'json'}
@@ -23,30 +23,30 @@ STATUS_FORMATS = {'text', 'json'}
# StatusResult
def test_status_format_list():
assert set(api_impl.list_formats(napi.StatusResult)) == STATUS_FORMATS
assert set(v1_format.list_formats(napi.StatusResult)) == STATUS_FORMATS
@pytest.mark.parametrize('fmt', list(STATUS_FORMATS))
def test_status_supported(fmt):
assert api_impl.supports_format(napi.StatusResult, fmt)
assert v1_format.supports_format(napi.StatusResult, fmt)
def test_status_unsupported():
assert not api_impl.supports_format(napi.StatusResult, 'gagaga')
assert not v1_format.supports_format(napi.StatusResult, 'gagaga')
def test_status_format_text():
assert api_impl.format_result(napi.StatusResult(0, 'message here'), 'text', {}) == 'OK'
assert v1_format.format_result(napi.StatusResult(0, 'message here'), 'text', {}) == 'OK'
def test_status_format_text():
assert api_impl.format_result(napi.StatusResult(500, 'message here'), 'text', {}) == 'ERROR: message here'
assert v1_format.format_result(napi.StatusResult(500, 'message here'), 'text', {}) == 'ERROR: message here'
def test_status_format_json_minimal():
status = napi.StatusResult(700, 'Bad format.')
result = api_impl.format_result(status, 'json', {})
result = v1_format.format_result(status, 'json', {})
assert result == \
f'{{"status":700,"message":"Bad format.","software_version":"{napi.__version__}"}}'
@@ -57,7 +57,7 @@ def test_status_format_json_full():
status.data_updated = dt.datetime(2010, 2, 7, 20, 20, 3, 0, tzinfo=dt.timezone.utc)
status.database_version = '5.6'
result = api_impl.format_result(status, 'json', {})
result = v1_format.format_result(status, 'json', {})
assert result == \
f'{{"status":0,"message":"OK","data_updated":"2010-02-07T20:20:03+00:00","software_version":"{napi.__version__}","database_version":"5.6"}}'
@@ -70,7 +70,7 @@ def test_search_details_minimal():
('place', 'thing'),
napi.Point(1.0, 2.0))
result = api_impl.format_result(search, 'json', {})
result = v1_format.format_result(search, 'json', {})
assert json.loads(result) == \
{'category': 'place',
@@ -114,7 +114,7 @@ def test_search_details_full():
)
search.localize(napi.Locales())
result = api_impl.format_result(search, 'json', {})
result = v1_format.format_result(search, 'json', {})
assert json.loads(result) == \
{'place_id': 37563,
@@ -153,7 +153,7 @@ def test_search_details_no_geometry(gtype, isarea):
napi.Point(1.0, 2.0),
geometry={'type': gtype})
result = api_impl.format_result(search, 'json', {})
result = v1_format.format_result(search, 'json', {})
js = json.loads(result)
assert js['geometry'] == {'type': 'Point', 'coordinates': [1.0, 2.0]}
@@ -166,7 +166,7 @@ def test_search_details_with_geometry():
napi.Point(1.0, 2.0),
geometry={'geojson': '{"type":"Point","coordinates":[56.947,-87.44]}'})
result = api_impl.format_result(search, 'json', {})
result = v1_format.format_result(search, 'json', {})
js = json.loads(result)
assert js['geometry'] == {'type': 'Point', 'coordinates': [56.947, -87.44]}
@@ -178,7 +178,7 @@ def test_search_details_with_icon_available():
('amenity', 'restaurant'),
napi.Point(1.0, 2.0))
result = api_impl.format_result(search, 'json', {'icon_base_url': 'foo'})
result = v1_format.format_result(search, 'json', {'icon_base_url': 'foo'})
js = json.loads(result)
assert js['icon'] == 'foo/food_restaurant.p.20.png'
@@ -189,7 +189,7 @@ def test_search_details_with_icon_not_available():
('amenity', 'tree'),
napi.Point(1.0, 2.0))
result = api_impl.format_result(search, 'json', {'icon_base_url': 'foo'})
result = v1_format.format_result(search, 'json', {'icon_base_url': 'foo'})
js = json.loads(result)
assert 'icon' not in js
@@ -212,7 +212,7 @@ def test_search_details_with_address_minimal():
distance=0.0)
])
result = api_impl.format_result(search, 'json', {})
result = v1_format.format_result(search, 'json', {})
js = json.loads(result)
assert js['address'] == [{'localname': '',
@@ -245,7 +245,7 @@ def test_search_details_with_further_infos(field, outfield):
distance=0.034)
])
result = api_impl.format_result(search, 'json', {})
result = v1_format.format_result(search, 'json', {})
js = json.loads(result)
assert js[outfield] == [{'localname': 'Trespass',
@@ -279,7 +279,7 @@ def test_search_details_grouped_hierarchy():
distance=0.034)
])
result = api_impl.format_result(search, 'json', {'group_hierarchy': True})
result = v1_format.format_result(search, 'json', {'group_hierarchy': True})
js = json.loads(result)
assert js['hierarchy'] == {'note': [{'localname': 'Trespass',
@@ -303,7 +303,7 @@ def test_search_details_keywords_name():
napi.WordInfo(23, 'foo', 'mefoo'),
napi.WordInfo(24, 'foo', 'bafoo')])
result = api_impl.format_result(search, 'json', {'keywords': True})
result = v1_format.format_result(search, 'json', {'keywords': True})
js = json.loads(result)
assert js['keywords'] == {'name': [{'id': 23, 'token': 'foo'},
@@ -319,7 +319,7 @@ def test_search_details_keywords_address():
napi.WordInfo(23, 'foo', 'mefoo'),
napi.WordInfo(24, 'foo', 'bafoo')])
result = api_impl.format_result(search, 'json', {'keywords': True})
result = v1_format.format_result(search, 'json', {'keywords': True})
js = json.loads(result)
assert js['keywords'] == {'address': [{'id': 23, 'token': 'foo'},

View File

@@ -15,7 +15,7 @@ import xml.etree.ElementTree as ET
import pytest
import nominatim_api.v1 as api_impl
from nominatim_api.v1.format import dispatch as v1_format
import nominatim_api as napi
FORMATS = ['json', 'jsonv2', 'geojson', 'geocodejson', 'xml']
@@ -26,7 +26,7 @@ def test_format_reverse_minimal(fmt):
('amenity', 'post_box'),
napi.Point(0.3, -8.9))
raw = api_impl.format_result(napi.ReverseResults([reverse]), fmt, {})
raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt, {})
if fmt == 'xml':
root = ET.fromstring(raw)
@@ -38,7 +38,7 @@ def test_format_reverse_minimal(fmt):
@pytest.mark.parametrize('fmt', FORMATS)
def test_format_reverse_no_result(fmt):
raw = api_impl.format_result(napi.ReverseResults(), fmt, {})
raw = v1_format.format_result(napi.ReverseResults(), fmt, {})
if fmt == 'xml':
root = ET.fromstring(raw)
@@ -55,7 +55,7 @@ def test_format_reverse_with_osm_id(fmt):
place_id=5564,
osm_object=('N', 23))
raw = api_impl.format_result(napi.ReverseResults([reverse]), fmt, {})
raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt, {})
if fmt == 'xml':
root = ET.fromstring(raw).find('result')
@@ -103,7 +103,7 @@ def test_format_reverse_with_address(fmt):
]))
reverse.localize(napi.Locales())
raw = api_impl.format_result(napi.ReverseResults([reverse]), fmt,
raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
{'addressdetails': True})
@@ -167,7 +167,7 @@ def test_format_reverse_geocodejson_special_parts():
reverse.localize(napi.Locales())
raw = api_impl.format_result(napi.ReverseResults([reverse]), 'geocodejson',
raw = v1_format.format_result(napi.ReverseResults([reverse]), 'geocodejson',
{'addressdetails': True})
props = json.loads(raw)['features'][0]['properties']['geocoding']
@@ -183,7 +183,7 @@ def test_format_reverse_with_address_none(fmt):
napi.Point(1.0, 2.0),
address_rows=napi.AddressLines())
raw = api_impl.format_result(napi.ReverseResults([reverse]), fmt,
raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
{'addressdetails': True})
@@ -213,7 +213,7 @@ def test_format_reverse_with_extratags(fmt):
napi.Point(1.0, 2.0),
extratags={'one': 'A', 'two':'B'})
raw = api_impl.format_result(napi.ReverseResults([reverse]), fmt,
raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
{'extratags': True})
if fmt == 'xml':
@@ -235,7 +235,7 @@ def test_format_reverse_with_extratags_none(fmt):
('place', 'thing'),
napi.Point(1.0, 2.0))
raw = api_impl.format_result(napi.ReverseResults([reverse]), fmt,
raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
{'extratags': True})
if fmt == 'xml':
@@ -258,7 +258,7 @@ def test_format_reverse_with_namedetails_with_name(fmt):
napi.Point(1.0, 2.0),
names={'name': 'A', 'ref':'1'})
raw = api_impl.format_result(napi.ReverseResults([reverse]), fmt,
raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
{'namedetails': True})
if fmt == 'xml':
@@ -280,7 +280,7 @@ def test_format_reverse_with_namedetails_without_name(fmt):
('place', 'thing'),
napi.Point(1.0, 2.0))
raw = api_impl.format_result(napi.ReverseResults([reverse]), fmt,
raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
{'namedetails': True})
if fmt == 'xml':
@@ -302,7 +302,7 @@ def test_search_details_with_icon_available(fmt):
('amenity', 'restaurant'),
napi.Point(1.0, 2.0))
result = api_impl.format_result(napi.ReverseResults([reverse]), fmt,
result = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
{'icon_base_url': 'foo'})
js = json.loads(result)
@@ -316,7 +316,7 @@ def test_search_details_with_icon_not_available(fmt):
('amenity', 'tree'),
napi.Point(1.0, 2.0))
result = api_impl.format_result(napi.ReverseResults([reverse]), fmt,
result = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
{'icon_base_url': 'foo'})
assert 'icon' not in json.loads(result)

View File

@@ -13,6 +13,15 @@ import pytest
import nominatim_db.clicmd.api
import nominatim_api as napi
@pytest.mark.parametrize('call', ['search', 'reverse', 'lookup', 'details', 'status'])
def test_list_format(cli_call, call):
assert 0 == cli_call(call, '--list-formats')
@pytest.mark.parametrize('call', ['search', 'reverse', 'lookup', 'details', 'status'])
def test_bad_format(cli_call, call):
assert 1 == cli_call(call, '--format', 'rsdfsdfsdfsaefsdfsd')
class TestCliStatusCall: