use custom result formatters in CLI commands

This commit is contained in:
Sarah Hoffmann
2024-08-16 19:30:57 +02:00
parent 69369c08c8
commit 8e8f7a641b
7 changed files with 177 additions and 120 deletions

View File

@@ -39,6 +39,7 @@ from .results import (SourceTable as SourceTable,
SearchResult as SearchResult, SearchResult as SearchResult,
SearchResults as SearchResults) SearchResults as SearchResults)
from .localization import (Locales as Locales) 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__ 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 #pylint: disable=useless-import-alias
from .server_glue import ROUTES as ROUTES 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

@@ -7,7 +7,7 @@
""" """
Subcommand definitions for API calls from the command line. 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 argparse
import logging import logging
import json import json
@@ -15,9 +15,8 @@ import sys
from functools import reduce from functools import reduce
import nominatim_api as napi 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.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 import nominatim_api.logging as loglib
from ..errors import UsageError from ..errors import UsageError
from .args import NominatimArgs from .args import NominatimArgs
@@ -44,11 +43,16 @@ EXTRADATA_PARAMS = (
('namedetails', 'Include a list of alternative names') ('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: def _add_api_output_arguments(parser: argparse.ArgumentParser) -> None:
group = parser.add_argument_group('Output arguments') group = parser.add_argument_group('Output formatting')
group.add_argument('--format', default='jsonv2', group.add_argument('--format', type=str, default='jsonv2',
choices=formatting.list_formats(napi.SearchResults) + ['debug'], help='Format of result (use --list-format to see supported formats)')
help='Format of result')
for name, desc in EXTRADATA_PARAMS: for name, desc in EXTRADATA_PARAMS:
group.add_argument('--' + name, action='store_true', help=desc) group.add_argument('--' + name, action='store_true', help=desc)
@@ -105,6 +109,24 @@ def _get_layers(args: NominatimArgs, default: napi.DataLayer) -> Optional[napi.D
(napi.DataLayer[s.upper()] for s in args.layers)) (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
json.dump(json.loads(output), sys.stdout, indent=4, ensure_ascii=False)
else:
sys.stdout.write(output)
sys.stdout.write('\n')
class APISearch: class APISearch:
"""\ """\
Execute a search query. Execute a search query.
@@ -135,18 +157,24 @@ class APISearch:
help='Preferred area to find search results') help='Preferred area to find search results')
group.add_argument('--bounded', action='store_true', group.add_argument('--bounded', action='store_true',
help='Strictly restrict results to viewbox area') help='Strictly restrict results to viewbox area')
group = parser.add_argument_group('Other arguments')
group.add_argument('--no-dedupe', action='store_false', dest='dedupe', group.add_argument('--no-dedupe', action='store_false', dest='dedupe',
help='Do not remove duplicates from the result list') help='Do not remove duplicates from the result list')
_add_list_format(parser)
def run(self, args: NominatimArgs) -> int: 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': if args.format == 'debug':
loglib.set_log_output('text') 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) api = napi.NominatimAPI(args.project_dir)
params: Dict[str, Any] = {'max_results': args.limit + min(args.limit, 10), params: Dict[str, Any] = {'max_results': args.limit + min(args.limit, 10),
'address_details': True, # needed for display name 'address_details': True, # needed for display name
'geometry_output': _get_geometry_output(args), 'geometry_output': _get_geometry_output(args),
@@ -177,19 +205,10 @@ class APISearch:
print(loglib.get_and_disable()) print(loglib.get_and_disable())
return 0 return 0
output = api_output.format_result( _print_output(formatter, results, args.format,
results, {'extratags': args.extratags,
args.format, 'namedetails': args.namedetails,
{'extratags': args.extratags, 'addressdetails': args.addressdetails})
'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')
return 0 return 0
@@ -205,9 +224,9 @@ class APIReverse:
def add_args(self, parser: argparse.ArgumentParser) -> None: def add_args(self, parser: argparse.ArgumentParser) -> None:
group = parser.add_argument_group('Query arguments') 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)') 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)') help='Longitude of coordinate to look up (in WGS84)')
group.add_argument('--zoom', type=int, group.add_argument('--zoom', type=int,
help='Level of detail required for the address') help='Level of detail required for the address')
@@ -217,14 +236,25 @@ class APIReverse:
help='OSM id to lookup in format <NRW><id> (may be repeated)') help='OSM id to lookup in format <NRW><id> (may be repeated)')
_add_api_output_arguments(parser) _add_api_output_arguments(parser)
_add_list_format(parser)
def run(self, args: NominatimArgs) -> int: 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': if args.format == 'debug':
loglib.set_log_output('text') 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) api = napi.NominatimAPI(args.project_dir)
result = api.reverse(napi.Point(args.lon, args.lat), result = api.reverse(napi.Point(args.lon, args.lat),
max_rank=zoom_to_rank(args.zoom or 18), max_rank=zoom_to_rank(args.zoom or 18),
layers=_get_layers(args, napi.DataLayer.ADDRESS | napi.DataLayer.POI), layers=_get_layers(args, napi.DataLayer.ADDRESS | napi.DataLayer.POI),
@@ -238,18 +268,10 @@ class APIReverse:
return 0 return 0
if result: if result:
output = api_output.format_result( _print_output(formatter, napi.ReverseResults([result]), args.format,
napi.ReverseResults([result]), {'extratags': args.extratags,
args.format, 'namedetails': args.namedetails,
{'extratags': args.extratags, 'addressdetails': args.addressdetails})
'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')
return 0 return 0
@@ -271,43 +293,45 @@ class APILookup:
def add_args(self, parser: argparse.ArgumentParser) -> None: def add_args(self, parser: argparse.ArgumentParser) -> None:
group = parser.add_argument_group('Query arguments') group = parser.add_argument_group('Query arguments')
group.add_argument('--id', metavar='OSMID', 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)') help='OSM id to lookup in format <NRW><id> (may be repeated)')
_add_api_output_arguments(parser) _add_api_output_arguments(parser)
_add_list_format(parser)
def run(self, args: NominatimArgs) -> int: 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': if args.format == 'debug':
loglib.set_log_output('text') 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.ids is None:
raise UsageError("'id' parameter required.")
if args.format == 'debug':
print(loglib.get_and_disable())
return 0
places = [napi.OsmID(o[0], int(o[1:])) for o in args.ids] places = [napi.OsmID(o[0], int(o[1:])) for o in args.ids]
api = napi.NominatimAPI(args.project_dir)
results = api.lookup(places, results = api.lookup(places,
address_details=True, # needed for display name address_details=True, # needed for display name
geometry_output=_get_geometry_output(args), geometry_output=_get_geometry_output(args),
geometry_simplification=args.polygon_threshold or 0.0, geometry_simplification=args.polygon_threshold or 0.0,
locales=_get_locales(args, api.config.DEFAULT_LANGUAGE)) locales=_get_locales(args, api.config.DEFAULT_LANGUAGE))
output = api_output.format_result( if args.format == 'debug':
results, print(loglib.get_and_disable())
args.format, return 0
{'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 return 0
@@ -323,20 +347,21 @@ class APIDetails:
def add_args(self, parser: argparse.ArgumentParser) -> None: def add_args(self, parser: argparse.ArgumentParser) -> None:
group = parser.add_argument_group('Query arguments') group = parser.add_argument_group('Query arguments')
objs = group.add_mutually_exclusive_group(required=True) group.add_argument('--node', '-n', type=int,
objs.add_argument('--node', '-n', type=int, help="Look up the OSM node with the given ID.")
help="Look up the OSM node with the given ID.") group.add_argument('--way', '-w', type=int,
objs.add_argument('--way', '-w', type=int, help="Look up the OSM way with the given ID.")
help="Look up the OSM way with the given ID.") group.add_argument('--relation', '-r', type=int,
objs.add_argument('--relation', '-r', type=int, help="Look up the OSM relation with the given ID.")
help="Look up the OSM relation with the given ID.") group.add_argument('--place_id', '-p', type=int,
objs.add_argument('--place_id', '-p', type=int, help='Database internal identifier of the OSM object to look up')
help='Database internal identifier of the OSM object to look up')
group.add_argument('--class', dest='object_class', group.add_argument('--class', dest='object_class',
help=("Class type to disambiguated multiple entries " help=("Class type to disambiguated multiple entries "
"of the same object.")) "of the same object."))
group = parser.add_argument_group('Output arguments') 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', group.add_argument('--addressdetails', action='store_true',
help='Include a breakdown of the address into elements') help='Include a breakdown of the address into elements')
group.add_argument('--keywords', action='store_true', group.add_argument('--keywords', action='store_true',
@@ -351,9 +376,21 @@ class APIDetails:
help='Include geometry of result') help='Include geometry of result')
group.add_argument('--lang', '--accept-language', metavar='LANGS', group.add_argument('--lang', '--accept-language', metavar='LANGS',
help='Preferred language order for presenting search results') help='Preferred language order for presenting search results')
_add_list_format(parser)
def run(self, args: NominatimArgs) -> int: 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 place: napi.PlaceRef
if args.node: if args.node:
place = napi.OsmID('N', args.node, args.object_class) place = napi.OsmID('N', args.node, args.object_class)
@@ -361,12 +398,13 @@ class APIDetails:
place = napi.OsmID('W', args.way, args.object_class) place = napi.OsmID('W', args.way, args.object_class)
elif args.relation: elif args.relation:
place = napi.OsmID('R', args.relation, args.object_class) place = napi.OsmID('R', args.relation, args.object_class)
else: elif args.place_id is not None:
assert args.place_id is not None
place = napi.PlaceID(args.place_id) 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) api = napi.NominatimAPI(args.project_dir)
locales = _get_locales(args, api.config.DEFAULT_LANGUAGE) locales = _get_locales(args, api.config.DEFAULT_LANGUAGE)
result = api.details(place, result = api.details(place,
address_details=args.addressdetails, address_details=args.addressdetails,
@@ -378,17 +416,14 @@ class APIDetails:
else napi.GeometryFormat.NONE, else napi.GeometryFormat.NONE,
locales=locales) locales=locales)
if args.format == 'debug':
print(loglib.get_and_disable())
return 0
if result: if result:
output = api_output.format_result( _print_output(formatter, result, args.format or 'json',
result, {'locales': locales,
'json', 'group_hierarchy': args.group_hierarchy})
{'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')
return 0 return 0
LOG.error("Object not found in database.") LOG.error("Object not found in database.")
@@ -406,13 +441,30 @@ class APIStatus:
""" """
def add_args(self, parser: argparse.ArgumentParser) -> None: def add_args(self, parser: argparse.ArgumentParser) -> None:
formats = api_output.list_formats(napi.StatusResult)
group = parser.add_argument_group('API parameters') group = parser.add_argument_group('API parameters')
group.add_argument('--format', default=formats[0], choices=formats, group.add_argument('--format', type=str, default='text',
help='Format of result') help='Format of result (use --list-formats to see supported formats)')
_add_list_format(parser)
def run(self, args: NominatimArgs) -> int: 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() 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 return 0

View File

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

View File

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

View File

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

View File

@@ -13,6 +13,15 @@ import pytest
import nominatim_db.clicmd.api import nominatim_db.clicmd.api
import nominatim_api as napi 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: class TestCliStatusCall: