From: Sarah Hoffmann Date: Mon, 3 Apr 2023 12:38:40 +0000 (+0200) Subject: switch CLI lookup command to Python implementation X-Git-Tag: v4.3.0~83^2 X-Git-Url: https://git.openstreetmap.org./nominatim.git/commitdiff_plain/1dce2b98b49ba79c40ee26598bad8d3b669427f6 switch CLI lookup command to Python implementation --- diff --git a/nominatim/api/v1/classtypes.py b/nominatim/api/v1/classtypes.py index 27faa174..273fe2f5 100644 --- a/nominatim/api/v1/classtypes.py +++ b/nominatim/api/v1/classtypes.py @@ -10,7 +10,7 @@ Hard-coded information about tag catagories. These tables have been copied verbatim from the old PHP code. For future version a more flexible formatting is required. """ -from typing import Tuple, Optional, Mapping +from typing import Tuple, Optional, Mapping, Union import nominatim.api as napi @@ -41,7 +41,7 @@ def get_label_tag(category: Tuple[str, str], extratags: Optional[Mapping[str, st return label.lower().replace(' ', '_') -def bbox_from_result(result: napi.ReverseResult) -> napi.Bbox: +def bbox_from_result(result: Union[napi.ReverseResult, napi.SearchResult]) -> napi.Bbox: """ Compute a bounding box for the result. For ways and relations a given boundingbox is used. For all other object, a box is computed around the centroid according to dimensions dereived from the diff --git a/nominatim/api/v1/format.py b/nominatim/api/v1/format.py index b50a2346..2e1caa99 100644 --- a/nominatim/api/v1/format.py +++ b/nominatim/api/v1/format.py @@ -198,33 +198,33 @@ def _format_reverse_jsonv2(results: napi.ReverseResults, @dispatch.format_func(napi.SearchResults, 'xml') -def _format_reverse_xml(results: napi.SearchResults, options: Mapping[str, Any]) -> str: +def _format_search_xml(results: napi.SearchResults, options: Mapping[str, Any]) -> str: return format_xml.format_base_xml(results, options, False, 'searchresults', {'querystring': 'TODO'}) @dispatch.format_func(napi.SearchResults, 'geojson') -def _format_reverse_geojson(results: napi.SearchResults, +def _format_search_geojson(results: napi.SearchResults, options: Mapping[str, Any]) -> str: return format_json.format_base_geojson(results, options, False) @dispatch.format_func(napi.SearchResults, 'geocodejson') -def _format_reverse_geocodejson(results: napi.SearchResults, +def _format_search_geocodejson(results: napi.SearchResults, options: Mapping[str, Any]) -> str: return format_json.format_base_geocodejson(results, options, False) @dispatch.format_func(napi.SearchResults, 'json') -def _format_reverse_json(results: napi.SearchResults, +def _format_search_json(results: napi.SearchResults, options: Mapping[str, Any]) -> str: return format_json.format_base_json(results, options, False, class_label='class') @dispatch.format_func(napi.SearchResults, 'jsonv2') -def _format_reverse_jsonv2(results: napi.SearchResults, +def _format_search_jsonv2(results: napi.SearchResults, options: Mapping[str, Any]) -> str: return format_json.format_base_json(results, options, False, class_label='category') diff --git a/nominatim/api/v1/format_json.py b/nominatim/api/v1/format_json.py index a4fa7655..c82681e9 100644 --- a/nominatim/api/v1/format_json.py +++ b/nominatim/api/v1/format_json.py @@ -7,12 +7,14 @@ """ Helper functions for output of results in json formats. """ -from typing import Mapping, Any, Optional, Tuple +from typing import Mapping, Any, Optional, Tuple, Union import nominatim.api as napi import nominatim.api.v1.classtypes as cl from nominatim.utils.json_writer import JsonWriter +#pylint: disable=too-many-branches + def _write_osm_id(out: JsonWriter, osm_object: Optional[Tuple[str, int]]) -> None: if osm_object is not None: out.keyval_not_none('osm_type', cl.OSM_TYPE_NAME.get(osm_object[0], None))\ @@ -61,7 +63,7 @@ def _write_geocodejson_address(out: JsonWriter, out.keyval('country_code', country_code) -def format_base_json(results: napi.ReverseResults, #pylint: disable=too-many-branches +def format_base_json(results: Union[napi.ReverseResults, napi.SearchResults], options: Mapping[str, Any], simple: bool, class_label: str) -> str: """ Return the result list as a simple json string in custom Nominatim format. @@ -141,7 +143,7 @@ def format_base_json(results: napi.ReverseResults, #pylint: disable=too-many-bra return out() -def format_base_geojson(results: napi.ReverseResults, +def format_base_geojson(results: Union[napi.ReverseResults, napi.SearchResults], options: Mapping[str, Any], simple: bool) -> str: """ Return the result list as a geojson string. @@ -210,7 +212,7 @@ def format_base_geojson(results: napi.ReverseResults, return out() -def format_base_geocodejson(results: napi.ReverseResults, +def format_base_geocodejson(results: Union[napi.ReverseResults, napi.SearchResults], options: Mapping[str, Any], simple: bool) -> str: """ Return the result list as a geocodejson string. """ diff --git a/nominatim/api/v1/format_xml.py b/nominatim/api/v1/format_xml.py index 3fe3b7fe..1fd0675a 100644 --- a/nominatim/api/v1/format_xml.py +++ b/nominatim/api/v1/format_xml.py @@ -7,13 +7,15 @@ """ Helper functions for output of results in XML format. """ -from typing import Mapping, Any, Optional +from typing import Mapping, Any, Optional, Union import datetime as dt import xml.etree.ElementTree as ET import nominatim.api as napi import nominatim.api.v1.classtypes as cl +#pylint: disable=too-many-branches + def _write_xml_address(root: ET.Element, address: napi.AddressLines, country_code: Optional[str]) -> None: parts = {} @@ -34,7 +36,7 @@ def _write_xml_address(root: ET.Element, address: napi.AddressLines, ET.SubElement(root, 'country_code').text = country_code -def _create_base_entry(result: napi.ReverseResult, #pylint: disable=too-many-branches +def _create_base_entry(result: Union[napi.ReverseResult, napi.SearchResult], root: ET.Element, simple: bool, locales: napi.Locales) -> ET.Element: if result.address_rows: @@ -86,7 +88,7 @@ def _create_base_entry(result: napi.ReverseResult, #pylint: disable=too-many-bra return place -def format_base_xml(results: napi.ReverseResults, +def format_base_xml(results: Union[napi.ReverseResults, napi.SearchResults], options: Mapping[str, Any], simple: bool, xml_root_tag: str, xml_extra_info: Mapping[str, str]) -> str: diff --git a/nominatim/api/v1/server_glue.py b/nominatim/api/v1/server_glue.py index 40081c03..68cf58c2 100644 --- a/nominatim/api/v1/server_glue.py +++ b/nominatim/api/v1/server_glue.py @@ -227,8 +227,11 @@ class ASGIAdaptor(abc.ABC): def parse_geometry_details(self, fmt: str) -> napi.LookupDetails: + """ Create details strucutre from the supplied geometry parameters. + """ details = napi.LookupDetails(address_details=True, - geometry_simplification=self.get_float('polygon_threshold', 0.0)) + geometry_simplification= + self.get_float('polygon_threshold', 0.0)) numgeoms = 0 if self.get_bool('polygon_geojson', False): details.geometry_output |= napi.GeometryFormat.GEOJSON @@ -348,7 +351,7 @@ async def lookup_endpoint(api: napi.NominatimAPIAsync, params: ASGIAdaptor) -> A details = params.parse_geometry_details(fmt) places = [] - for oid in params.get('osm_ids', '').split(','): + for oid in (params.get('osm_ids') or '').split(','): oid = oid.strip() if len(oid) > 1 and oid[0] in 'RNWrnw' and oid[1:].isdigit(): places.append(napi.OsmID(oid[0], int(oid[1:]))) diff --git a/nominatim/clicmd/api.py b/nominatim/clicmd/api.py index e198e541..58edbea4 100644 --- a/nominatim/clicmd/api.py +++ b/nominatim/clicmd/api.py @@ -214,19 +214,31 @@ class APILookup: def run(self, args: NominatimArgs) -> int: - params: Dict[str, object] = dict(osm_ids=','.join(args.ids), format=args.format) + api = napi.NominatimAPI(args.project_dir) - for param, _ in EXTRADATA_PARAMS: - if getattr(args, param): - params[param] = '1' - if args.lang: - params['accept-language'] = args.lang - if args.polygon_output: - params['polygon_' + args.polygon_output] = '1' - if args.polygon_threshold: - params['polygon_threshold'] = args.polygon_threshold + details = napi.LookupDetails(address_details=True, # needed for display name + geometry_output=args.get_geometry_output(), + geometry_simplification=args.polygon_threshold or 0.0) + + places = [napi.OsmID(o[0], int(o[1:])) for o in args.ids] + + results = api.lookup(places, details) - return _run_api('lookup', args, params) + output = api_output.format_result( + results, + args.format, + {'locales': args.get_locales(api.config.DEFAULT_LANGUAGE), + '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') + + return 0 class APIDetails: diff --git a/test/python/cli/test_cmd_api.py b/test/python/cli/test_cmd_api.py index b794bccd..e8c447aa 100644 --- a/test/python/cli/test_cmd_api.py +++ b/test/python/cli/test_cmd_api.py @@ -23,8 +23,7 @@ def test_no_api_without_phpcgi(endpoint): @pytest.mark.parametrize("params", [('search', '--query', 'new'), - ('search', '--city', 'Berlin'), - ('lookup', '--id', 'N1')]) + ('search', '--city', 'Berlin')]) class TestCliApiCallPhp: @pytest.fixture(autouse=True) @@ -156,32 +155,49 @@ class TestCliReverseCall: assert out['name'] == 'Nom' -QUERY_PARAMS = { - 'search': ('--query', 'somewhere'), - 'reverse': ('--lat', '20', '--lon', '30'), - 'lookup': ('--id', 'R345345'), - 'details': ('--node', '324') -} +class TestCliLookupCall: + + @pytest.fixture(autouse=True) + def setup_lookup_mock(self, monkeypatch): + result = napi.SearchResult(napi.SourceTable.PLACEX, ('place', 'thing'), + napi.Point(1.0, -3.0), + names={'name':'Name', 'name:fr': 'Nom'}, + extratags={'extra':'Extra'}) + + monkeypatch.setattr(napi.NominatimAPI, 'lookup', + lambda *args: napi.SearchResults([result])) + + def test_lookup_simple(self, cli_call, tmp_path, capsys): + result = cli_call('lookup', '--project-dir', str(tmp_path), + '--id', 'N34') + + assert result == 0 + + out = json.loads(capsys.readouterr().out) + assert len(out) == 1 + assert out[0]['name'] == 'Name' + assert 'address' not in out[0] + assert 'extratags' not in out[0] + assert 'namedetails' not in out[0] + -@pytest.mark.parametrize("endpoint", (('search', 'lookup'))) class TestCliApiCommonParameters: @pytest.fixture(autouse=True) - def setup_website_dir(self, cli_call, project_env, endpoint): - self.endpoint = endpoint + def setup_website_dir(self, cli_call, project_env): self.cli_call = cli_call self.project_dir = project_env.project_dir (self.project_dir / 'website').mkdir() def expect_param(self, param, expected): - (self.project_dir / 'website' / (self.endpoint + '.php')).write_text(f"""