"""
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
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
('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)
(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:
"""\
Execute a search query.
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),
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
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')
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.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),
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
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:
- if args.format == 'debug':
- loglib.set_log_output('text')
+ formatter = napi.load_format_dispatcher('v1', args.project_dir)
- api = napi.NominatimAPI(args.project_dir)
+ if args.list_formats:
+ return _list_formats(formatter, napi.ReverseResults)
if args.format == 'debug':
- print(loglib.get_and_disable())
- return 0
+ 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.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
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',
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)
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,
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.")
"""
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
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'}
# 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__}"}}'
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"}}'
('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',
)
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,
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]}
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]}
('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'
('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
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': '',
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',
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',
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'},
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'},
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']
('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)
@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)
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')
]))
reverse.localize(napi.Locales())
- raw = api_impl.format_result(napi.ReverseResults([reverse]), fmt,
+ raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
{'addressdetails': True})
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']
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})
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':
('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':
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':
('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':
('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)
('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)