switch CLI lookup command to Python implementation

This commit is contained in:
Sarah Hoffmann
2023-04-03 14:38:40 +02:00
parent 86c4897c9b
commit 1dce2b98b4
7 changed files with 77 additions and 42 deletions

View File

@@ -10,7 +10,7 @@ Hard-coded information about tag catagories.
These tables have been copied verbatim from the old PHP code. For future These tables have been copied verbatim from the old PHP code. For future
version a more flexible formatting is required. 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 """ Compute a bounding box for the result. For ways and relations
a given boundingbox is used. For all other object, a box is computed a given boundingbox is used. For all other object, a box is computed
around the centroid according to dimensions dereived from the around the centroid according to dimensions dereived from the

View 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, return format_xml.format_base_xml(results,
options, False, 'searchresults', options, False, 'searchresults',
{'querystring': 'TODO'}) {'querystring': 'TODO'})
@dispatch.format_func(napi.SearchResults, 'geojson') @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: options: Mapping[str, Any]) -> str:
return format_json.format_base_geojson(results, options, False) return format_json.format_base_geojson(results, options, False)
@dispatch.format_func(napi.SearchResults, 'geocodejson') @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: options: Mapping[str, Any]) -> str:
return format_json.format_base_geocodejson(results, options, False) return format_json.format_base_geocodejson(results, options, False)
@dispatch.format_func(napi.SearchResults, 'json') @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: options: Mapping[str, Any]) -> str:
return format_json.format_base_json(results, options, False, return format_json.format_base_json(results, options, False,
class_label='class') class_label='class')
@dispatch.format_func(napi.SearchResults, 'jsonv2') @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: options: Mapping[str, Any]) -> str:
return format_json.format_base_json(results, options, False, return format_json.format_base_json(results, options, False,
class_label='category') class_label='category')

View 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 as napi
import nominatim.api.v1.classtypes as cl import nominatim.api.v1.classtypes as cl
from nominatim.utils.json_writer import JsonWriter 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: def _write_osm_id(out: JsonWriter, osm_object: Optional[Tuple[str, int]]) -> None:
if osm_object is not None: if osm_object is not None:
out.keyval_not_none('osm_type', cl.OSM_TYPE_NAME.get(osm_object[0], 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, options: Mapping[str, Any], simple: bool,
class_label: str) -> str: class_label: str) -> str:
""" Return the result list as a simple json string in custom Nominatim format. """ 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], options: Mapping[str, Any],
simple: bool) -> str: simple: bool) -> str:
""" Return the result list as a geojson string. """ 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: options: Mapping[str, Any], simple: bool) -> str:
""" Return the result list as a geocodejson string. """ Return the result list as a geocodejson string.
""" """

View 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 datetime as dt
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
import nominatim.api as napi import nominatim.api as napi
import nominatim.api.v1.classtypes as cl import nominatim.api.v1.classtypes as cl
#pylint: disable=too-many-branches
def _write_xml_address(root: ET.Element, address: napi.AddressLines, def _write_xml_address(root: ET.Element, address: napi.AddressLines,
country_code: Optional[str]) -> None: country_code: Optional[str]) -> None:
parts = {} 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, root: ET.Element, simple: bool,
locales: napi.Locales) -> ET.Element: locales: napi.Locales) -> ET.Element:
if result.address_rows: 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], options: Mapping[str, Any],
simple: bool, xml_root_tag: str, simple: bool, xml_root_tag: str,
xml_extra_info: Mapping[str, str]) -> str: xml_extra_info: Mapping[str, str]) -> str:

View 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 numgeoms = 0
if self.get_bool('polygon_geojson', False): if self.get_bool('polygon_geojson', False):
details.geometry_output |= napi.GeometryFormat.GEOJSON 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) details = params.parse_geometry_details(fmt)
places = [] places = []
for oid in params.get('osm_ids', '').split(','): for oid in (params.get('osm_ids') or '').split(','):
oid = oid.strip() oid = oid.strip()
if len(oid) > 1 and oid[0] in 'RNWrnw' and oid[1:].isdigit(): if len(oid) > 1 and oid[0] in 'RNWrnw' and oid[1:].isdigit():
places.append(napi.OsmID(oid[0], int(oid[1:]))) places.append(napi.OsmID(oid[0], int(oid[1:])))

View 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: details = napi.LookupDetails(address_details=True, # needed for display name
if getattr(args, param): geometry_output=args.get_geometry_output(),
params[param] = '1' geometry_simplification=args.polygon_threshold or 0.0)
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
return _run_api('lookup', args, params) places = [napi.OsmID(o[0], int(o[1:])) for o in args.ids]
results = api.lookup(places, details)
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:

View 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'), ('search', '--city', 'Berlin')])
('lookup', '--id', 'N1')])
class TestCliApiCallPhp: class TestCliApiCallPhp:
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
@@ -156,32 +155,49 @@ class TestCliReverseCall:
assert out['name'] == 'Nom' assert out['name'] == 'Nom'
QUERY_PARAMS = { class TestCliLookupCall:
'search': ('--query', 'somewhere'),
'reverse': ('--lat', '20', '--lon', '30'), @pytest.fixture(autouse=True)
'lookup': ('--id', 'R345345'), def setup_lookup_mock(self, monkeypatch):
'details': ('--node', '324') 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: class TestCliApiCommonParameters:
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def setup_website_dir(self, cli_call, project_env, endpoint): def setup_website_dir(self, cli_call, project_env):
self.endpoint = endpoint
self.cli_call = cli_call self.cli_call = cli_call
self.project_dir = project_env.project_dir self.project_dir = project_env.project_dir
(self.project_dir / 'website').mkdir() (self.project_dir / 'website').mkdir()
def expect_param(self, param, expected): 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); exit($_GET['{param}'] == '{expected}' ? 0 : 10);
""") """)
def call_nominatim(self, *params): 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