]> git.openstreetmap.org Git - nominatim.git/commitdiff
switch CLI lookup command to Python implementation
authorSarah Hoffmann <lonvia@denofr.de>
Mon, 3 Apr 2023 12:38:40 +0000 (14:38 +0200)
committerSarah Hoffmann <lonvia@denofr.de>
Mon, 3 Apr 2023 12:40:41 +0000 (14:40 +0200)
nominatim/api/v1/classtypes.py
nominatim/api/v1/format.py
nominatim/api/v1/format_json.py
nominatim/api/v1/format_xml.py
nominatim/api/v1/server_glue.py
nominatim/clicmd/api.py
test/python/cli/test_cmd_api.py

index 27faa1746c3a105a946b7eae4194f23f6660cb41..273fe2f5bf97ec39fb8bfa8dd5861779a8e429cd 100644 (file)
@@ -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.
 """
 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
 
 
 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(' ', '_')
 
 
     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
     """ 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
index b50a2346f58d396d462a52853b81edc7678ea278..2e1caa991a91e0ca09eff6c458ed42b5ae8ff99b 100644 (file)
@@ -198,33 +198,33 @@ def _format_reverse_jsonv2(results: napi.ReverseResults,
 
 
 @dispatch.format_func(napi.SearchResults, 'xml')
 
 
 @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')
     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')
                             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')
                                 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')
                          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')
                            options: Mapping[str, Any]) -> str:
     return format_json.format_base_json(results, options, False,
                                         class_label='category')
index a4fa7655353749e66a5e0668ad22e30dbcfeee7a..c82681e91f7078fc7446644fa800a08a97c28637 100644 (file)
@@ -7,12 +7,14 @@
 """
 Helper functions for output of results in json formats.
 """
 """
 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
 
 
 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))\
 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)
 
 
         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.
                      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()
 
 
     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.
                         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()
 
 
     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.
     """
                             options: Mapping[str, Any], simple: bool) -> str:
     """ Return the result list as a geocodejson string.
     """
index 3fe3b7fe7771a428b57062c97a59e9c8f797c79f..1fd0675a36d04f88026ebebdc6730a720605a37d 100644 (file)
@@ -7,13 +7,15 @@
 """
 Helper functions for output of results in XML format.
 """
 """
 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
 
 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 = {}
 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
 
 
         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:
                        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
 
 
     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:
                     options: Mapping[str, Any],
                     simple: bool, xml_root_tag: str,
                     xml_extra_info: Mapping[str, str]) -> str:
index 40081c039cfdc9478205c4639327631521d6de76..68cf58c285b37858dc90828cde711ac79824743a 100644 (file)
@@ -227,8 +227,11 @@ class ASGIAdaptor(abc.ABC):
 
 
     def parse_geometry_details(self, fmt: str) -> napi.LookupDetails:
 
 
     def parse_geometry_details(self, fmt: str) -> napi.LookupDetails:
+        """ Create details strucutre from the supplied geometry parameters.
+        """
         details = napi.LookupDetails(address_details=True,
         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
         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 = []
     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:])))
         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:])))
index e198e541462335b1e719be37a566a4f7bd4232a7..58edbea4b8776d7b5705ea1e123344a57d8b80bf 100644 (file)
@@ -214,19 +214,31 @@ class APILookup:
 
 
     def run(self, args: NominatimArgs) -> int:
 
 
     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:
 
 
 class APIDetails:
index b794bccdb91a6dc76a2699927b7e60e8268a3e15..e8c447aa5f722732770a09fab26283d0838e13fe 100644 (file)
@@ -23,8 +23,7 @@ def test_no_api_without_phpcgi(endpoint):
 
 
 @pytest.mark.parametrize("params", [('search', '--query', 'new'),
 
 
 @pytest.mark.parametrize("params", [('search', '--query', 'new'),
-                                    ('search', '--city', 'Berlin'),
-                                    ('lookup', '--id', 'N1')])
+                                    ('search', '--city', 'Berlin')])
 class TestCliApiCallPhp:
 
     @pytest.fixture(autouse=True)
 class TestCliApiCallPhp:
 
     @pytest.fixture(autouse=True)
@@ -156,32 +155,49 @@ class TestCliReverseCall:
         assert out['name'] == 'Nom'
 
 
         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)
 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.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"""<?php
+        (self.project_dir / 'website' / ('search.php')).write_text(f"""<?php
         exit($_GET['{param}']  == '{expected}' ? 0 : 10);
         """)
 
 
     def call_nominatim(self, *params):
         exit($_GET['{param}']  == '{expected}' ? 0 : 10);
         """)
 
 
     def call_nominatim(self, *params):
-        return self.cli_call(self.endpoint, *QUERY_PARAMS[self.endpoint],
+        return self.cli_call('search', '--query', 'somewhere',
                              '--project-dir', str(self.project_dir), *params)
 
 
                              '--project-dir', str(self.project_dir), *params)
 
 
@@ -221,7 +237,7 @@ def test_cli_search_param_bounded(cli_call, project_env):
         exit($_GET['bounded']  == '1' ? 0 : 10);
         """)
 
         exit($_GET['bounded']  == '1' ? 0 : 10);
         """)
 
-    assert cli_call('search', *QUERY_PARAMS['search'], '--project-dir', str(project_env.project_dir),
+    assert cli_call('search', '--query', 'somewhere', '--project-dir', str(project_env.project_dir),
                     '--bounded') == 0
 
 
                     '--bounded') == 0
 
 
@@ -232,5 +248,5 @@ def test_cli_search_param_dedupe(cli_call, project_env):
         exit($_GET['dedupe']  == '0' ? 0 : 10);
         """)
 
         exit($_GET['dedupe']  == '0' ? 0 : 10);
         """)
 
-    assert cli_call('search', *QUERY_PARAMS['search'], '--project-dir', str(project_env.project_dir),
+    assert cli_call('search', '--query', 'somewhere', '--project-dir', str(project_env.project_dir),
                     '--no-dedupe') == 0
                     '--no-dedupe') == 0