X-Git-Url: https://git.openstreetmap.org./nominatim.git/blobdiff_plain/8e8f7a641b8b2eadd99e8e3198f762a3c3ebfd67..e1dc4379e0cd100200ac53752442143ca4846fc5:/src/nominatim_db/clicmd/api.py?ds=sidebyside diff --git a/src/nominatim_db/clicmd/api.py b/src/nominatim_db/clicmd/api.py index 3deb7e45..a922dc89 100644 --- a/src/nominatim_db/clicmd/api.py +++ b/src/nominatim_db/clicmd/api.py @@ -12,6 +12,7 @@ import argparse import logging import json import sys +import pprint from functools import reduce import nominatim_api as napi @@ -21,11 +22,10 @@ import nominatim_api.logging as loglib from ..errors import UsageError from .args import NominatimArgs -# Do not repeat documentation of subcommand classes. -# pylint: disable=C0111 LOG = logging.getLogger() + STRUCTURED_QUERY = ( ('amenity', 'name and/or type of POI'), ('street', 'housenumber and street'), @@ -36,6 +36,7 @@ STRUCTURED_QUERY = ( ('postalcode', 'postcode') ) + EXTRADATA_PARAMS = ( ('addressdetails', 'Include a breakdown of the address into elements'), ('extratags', ("Include additional information if available " @@ -43,6 +44,7 @@ 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', @@ -61,7 +63,7 @@ def _add_api_output_arguments(parser: argparse.ArgumentParser) -> None: group.add_argument('--polygon-output', choices=['geojson', 'kml', 'svg', 'text'], help='Output geometry of results as a GeoJSON, KML, SVG or WKT') - group.add_argument('--polygon-threshold', type=float, default = 0.0, + group.add_argument('--polygon-threshold', type=float, default=0.0, metavar='TOLERANCE', help=("Simplify output geometry." "Parameter is difference tolerance in degrees.")) @@ -113,19 +115,30 @@ def _list_formats(formatter: napi.FormatDispatcher, rtype: Type[Any]) -> int: for fmt in formatter.list_formats(rtype): print(fmt) print('debug') + print('raw') 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) + + if fmt == 'raw': + pprint.pprint(result) else: - sys.stdout.write(output) - sys.stdout.write('\n') + 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: """\ @@ -161,42 +174,44 @@ class APISearch: 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': + if args.format in ('debug', 'raw'): 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), - 'geometry_simplification': args.polygon_threshold, - 'countries': args.countrycodes, - 'excluded': args.exclude_place_ids, - 'viewbox': args.viewbox, - 'bounded_viewbox': args.bounded, - 'locales': _get_locales(args, api.config.DEFAULT_LANGUAGE) - } - - if args.query: - results = api.search(args.query, **params) - else: - results = api.search_address(amenity=args.amenity, - street=args.street, - city=args.city, - county=args.county, - state=args.state, - postalcode=args.postalcode, - country=args.country, - **params) + try: + with napi.NominatimAPI(args.project_dir) as api: + 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), + 'geometry_simplification': args.polygon_threshold, + 'countries': args.countrycodes, + 'excluded': args.exclude_place_ids, + 'viewbox': args.viewbox, + 'bounded_viewbox': args.bounded, + 'locales': _get_locales(args, api.config.DEFAULT_LANGUAGE) + } + + if args.query: + results = api.search(args.query, **params) + else: + results = api.search_address(amenity=args.amenity, + street=args.street, + city=args.city, + county=args.county, + state=args.state, + postalcode=args.postalcode, + country=args.country, + **params) + except napi.UsageError as ex: + raise UsageError(ex) from ex if args.dedupe and len(results) > 1: results = deduplicate_results(results, args.limit) @@ -238,14 +253,13 @@ class APIReverse: _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': + if args.format in ('debug', 'raw'): loglib.set_log_output('text') elif not formatter.supports_format(napi.ReverseResults, args.format): raise UsageError(f"Unsupported format '{args.format}'. " @@ -254,14 +268,19 @@ class APIReverse: 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), - address_details=True, # needed for display name - geometry_output=_get_geometry_output(args), - geometry_simplification=args.polygon_threshold, - locales=_get_locales(args, api.config.DEFAULT_LANGUAGE)) + layers = _get_layers(args, napi.DataLayer.ADDRESS | napi.DataLayer.POI) + + try: + with napi.NominatimAPI(args.project_dir) as api: + result = api.reverse(napi.Point(args.lon, args.lat), + max_rank=zoom_to_rank(args.zoom or 18), + layers=layers, + address_details=True, # needed for display name + geometry_output=_get_geometry_output(args), + geometry_simplification=args.polygon_threshold, + locales=_get_locales(args, api.config.DEFAULT_LANGUAGE)) + except napi.UsageError as ex: + raise UsageError(ex) from ex if args.format == 'debug': print(loglib.get_and_disable()) @@ -279,7 +298,6 @@ class APIReverse: return 42 - class APILookup: """\ Execute API lookup query. @@ -299,14 +317,13 @@ class APILookup: _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': + if args.format in ('debug', 'raw'): loglib.set_log_output('text') elif not formatter.supports_format(napi.ReverseResults, args.format): raise UsageError(f"Unsupported format '{args.format}'. " @@ -317,12 +334,15 @@ class APILookup: 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)) + try: + with napi.NominatimAPI(args.project_dir) as api: + 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)) + except napi.UsageError as ex: + raise UsageError(ex) from ex if args.format == 'debug': print(loglib.get_and_disable()) @@ -378,14 +398,13 @@ class APIDetails: 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': + if args.format in ('debug', 'raw'): loglib.set_log_output('text') elif not formatter.supports_format(napi.DetailedResult, args.format): raise UsageError(f"Unsupported format '{args.format}'. " @@ -398,23 +417,26 @@ class APIDetails: place = napi.OsmID('W', args.way, args.object_class) elif args.relation: place = napi.OsmID('R', args.relation, args.object_class) - elif 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, - linked_places=args.linkedplaces, - parented_places=args.hierarchy, - keywords=args.keywords, - geometry_output=napi.GeometryFormat.GEOJSON - if args.polygon_geojson - else napi.GeometryFormat.NONE, - locales=locales) + try: + with napi.NominatimAPI(args.project_dir) as api: + locales = _get_locales(args, api.config.DEFAULT_LANGUAGE) + result = api.details(place, + address_details=args.addressdetails, + linked_places=args.linkedplaces, + parented_places=args.hierarchy, + keywords=args.keywords, + geometry_output=(napi.GeometryFormat.GEOJSON + if args.polygon_geojson + else napi.GeometryFormat.NONE), + locales=locales) + except napi.UsageError as ex: + raise UsageError(ex) from ex if args.format == 'debug': print(loglib.get_and_disable()) @@ -446,20 +468,23 @@ class APIStatus: 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': + if args.format in ('debug', 'raw'): 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() + try: + with napi.NominatimAPI(args.project_dir) as api: + status = api.status() + except napi.UsageError as ex: + raise UsageError(ex) from ex if args.format == 'debug': print(loglib.get_and_disable())