From: Sarah Hoffmann Date: Fri, 3 Feb 2023 09:43:54 +0000 (+0100) Subject: switch details cli command to new Python implementation X-Git-Tag: v4.3.0~106^2~4 X-Git-Url: https://git.openstreetmap.org./nominatim.git/commitdiff_plain/104722a56a4773eae275b25987b4d340f19d35bb switch details cli command to new Python implementation --- diff --git a/nominatim/api/__init__.py b/nominatim/api/__init__.py index debd9119..ef1ebe32 100644 --- a/nominatim/api/__init__.py +++ b/nominatim/api/__init__.py @@ -28,3 +28,4 @@ from .results import (SourceTable as SourceTable, WordInfo as WordInfo, WordInfos as WordInfos, SearchResult as SearchResult) +from .localization import (Locales as Locales) diff --git a/nominatim/api/localization.py b/nominatim/api/localization.py new file mode 100644 index 00000000..09fe27c5 --- /dev/null +++ b/nominatim/api/localization.py @@ -0,0 +1,97 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of Nominatim. (https://nominatim.org) +# +# Copyright (C) 2023 by the Nominatim developer community. +# For a full list of authors see the git log. +""" +Helper functions for localizing names of results. +""" +from typing import Mapping, List, Optional + +import re + +class Locales: + """ Helper class for localization of names. + + It takes a list of language prefixes in their order of preferred + usage. + """ + + def __init__(self, langs: Optional[List[str]] = None): + self.languages = langs or [] + self.name_tags: List[str] = [] + + # Build the list of supported tags. It is currently hard-coded. + self._add_lang_tags('name') + self._add_tags('name', 'brand') + self._add_lang_tags('official_name', 'short_name') + self._add_tags('official_name', 'short_name', 'ref') + + + def __bool__(self) -> bool: + return len(self.languages) > 0 + + + def _add_tags(self, *tags: str) -> None: + for tag in tags: + self.name_tags.append(tag) + self.name_tags.append(f"_place_{tag}") + + + def _add_lang_tags(self, *tags: str) -> None: + for tag in tags: + for lang in self.languages: + self.name_tags.append(f"{tag}:{lang}") + self.name_tags.append(f"_place_{tag}:{lang}") + + + def display_name(self, names: Optional[Mapping[str, str]]) -> str: + """ Return the best matching name from a dictionary of names + containing different name variants. + + If 'names' is null or empty, an empty string is returned. If no + appropriate localization is found, the first name is returned. + """ + if not names: + return '' + + if len(names) > 1: + for tag in self.name_tags: + if tag in names: + return names[tag] + + # Nothing? Return any of the other names as a default. + return next(iter(names.values())) + + + @staticmethod + def from_accept_languages(langstr: str) -> 'Locales': + """ Create a localization object from a language list in the + format of HTTP accept-languages header. + + The functions tries to be forgiving of format errors by first splitting + the string into comma-separated parts and then parsing each + description separately. Badly formatted parts are then ignored. + """ + # split string into languages + candidates = [] + for desc in langstr.split(','): + m = re.fullmatch(r'\s*([a-z_-]+)(?:;\s*q\s*=\s*([01](?:\.\d+)?))?\s*', + desc, flags=re.I) + if m: + candidates.append((m[1], float(m[2] or 1.0))) + + # sort the results by the weight of each language (preserving order). + candidates.sort(reverse=True, key=lambda e: e[1]) + + # If a language has a region variant, also add the language without + # variant but only if it isn't already in the list to not mess up the weight. + languages = [] + for lid, _ in candidates: + languages.append(lid) + parts = lid.split('-', 1) + if len(parts) > 1 and all(c[0] != parts[0] for c in candidates): + languages.append(parts[0]) + + return Locales(languages) diff --git a/nominatim/api/result_formatting.py b/nominatim/api/result_formatting.py index 09cf7db8..a6bfa91c 100644 --- a/nominatim/api/result_formatting.py +++ b/nominatim/api/result_formatting.py @@ -7,11 +7,11 @@ """ Helper classes and functions for formating results into API responses. """ -from typing import Type, TypeVar, Dict, List, Callable, Any +from typing import Type, TypeVar, Dict, List, Callable, Any, Mapping from collections import defaultdict T = TypeVar('T') # pylint: disable=invalid-name -FormatFunc = Callable[[T], str] +FormatFunc = Callable[[T, Mapping[str, Any]], str] class FormatDispatcher: @@ -47,10 +47,10 @@ class FormatDispatcher: return fmt in self.format_functions[result_type] - def format_result(self, result: Any, fmt: str) -> str: + def format_result(self, result: Any, fmt: str, options: Mapping[str, Any]) -> str: """ Convert the given result into a string using the given format. The format is expected to be in the list returned by `list_formats()`. """ - return self.format_functions[type(result)][fmt](result) + return self.format_functions[type(result)][fmt](result, options) diff --git a/nominatim/api/v1/classtypes.py b/nominatim/api/v1/classtypes.py new file mode 100644 index 00000000..4e3667d3 --- /dev/null +++ b/nominatim/api/v1/classtypes.py @@ -0,0 +1,98 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of Nominatim. (https://nominatim.org) +# +# Copyright (C) 2023 by the Nominatim developer community. +# For a full list of authors see the git log. +""" +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. +""" + +ICONS = { + ('boundary', 'administrative'): 'poi_boundary_administrative', + ('place', 'city'): 'poi_place_city', + ('place', 'town'): 'poi_place_town', + ('place', 'village'): 'poi_place_village', + ('place', 'hamlet'): 'poi_place_village', + ('place', 'suburb'): 'poi_place_village', + ('place', 'locality'): 'poi_place_village', + ('place', 'airport'): 'transport_airport2', + ('aeroway', 'aerodrome'): 'transport_airport2', + ('railway', 'station'): 'transport_train_station2', + ('amenity', 'place_of_worship'): 'place_of_worship_unknown3', + ('amenity', 'pub'): 'food_pub', + ('amenity', 'bar'): 'food_bar', + ('amenity', 'university'): 'education_university', + ('tourism', 'museum'): 'tourist_museum', + ('amenity', 'arts_centre'): 'tourist_art_gallery2', + ('tourism', 'zoo'): 'tourist_zoo', + ('tourism', 'theme_park'): 'poi_point_of_interest', + ('tourism', 'attraction'): 'poi_point_of_interest', + ('leisure', 'golf_course'): 'sport_golf', + ('historic', 'castle'): 'tourist_castle', + ('amenity', 'hospital'): 'health_hospital', + ('amenity', 'school'): 'education_school', + ('amenity', 'theatre'): 'tourist_theatre', + ('amenity', 'library'): 'amenity_library', + ('amenity', 'fire_station'): 'amenity_firestation3', + ('amenity', 'police'): 'amenity_police2', + ('amenity', 'bank'): 'money_bank2', + ('amenity', 'post_office'): 'amenity_post_office', + ('tourism', 'hotel'): 'accommodation_hotel2', + ('amenity', 'cinema'): 'tourist_cinema', + ('tourism', 'artwork'): 'tourist_art_gallery2', + ('historic', 'archaeological_site'): 'tourist_archaeological2', + ('amenity', 'doctors'): 'health_doctors', + ('leisure', 'sports_centre'): 'sport_leisure_centre', + ('leisure', 'swimming_pool'): 'sport_swimming_outdoor', + ('shop', 'supermarket'): 'shopping_supermarket', + ('shop', 'convenience'): 'shopping_convenience', + ('amenity', 'restaurant'): 'food_restaurant', + ('amenity', 'fast_food'): 'food_fastfood', + ('amenity', 'cafe'): 'food_cafe', + ('tourism', 'guest_house'): 'accommodation_bed_and_breakfast', + ('amenity', 'pharmacy'): 'health_pharmacy_dispensing', + ('amenity', 'fuel'): 'transport_fuel', + ('natural', 'peak'): 'poi_peak', + ('natural', 'wood'): 'landuse_coniferous_and_deciduous', + ('shop', 'bicycle'): 'shopping_bicycle', + ('shop', 'clothes'): 'shopping_clothes', + ('shop', 'hairdresser'): 'shopping_hairdresser', + ('shop', 'doityourself'): 'shopping_diy', + ('shop', 'estate_agent'): 'shopping_estateagent2', + ('shop', 'car'): 'shopping_car', + ('shop', 'garden_centre'): 'shopping_garden_centre', + ('shop', 'car_repair'): 'shopping_car_repair', + ('shop', 'bakery'): 'shopping_bakery', + ('shop', 'butcher'): 'shopping_butcher', + ('shop', 'apparel'): 'shopping_clothes', + ('shop', 'laundry'): 'shopping_laundrette', + ('shop', 'beverages'): 'shopping_alcohol', + ('shop', 'alcohol'): 'shopping_alcohol', + ('shop', 'optician'): 'health_opticians', + ('shop', 'chemist'): 'health_pharmacy', + ('shop', 'gallery'): 'tourist_art_gallery2', + ('shop', 'jewelry'): 'shopping_jewelry', + ('tourism', 'information'): 'amenity_information', + ('historic', 'ruins'): 'tourist_ruin', + ('amenity', 'college'): 'education_school', + ('historic', 'monument'): 'tourist_monument', + ('historic', 'memorial'): 'tourist_monument', + ('historic', 'mine'): 'poi_mine', + ('tourism', 'caravan_site'): 'accommodation_caravan_park', + ('amenity', 'bus_station'): 'transport_bus_station', + ('amenity', 'atm'): 'money_atm2', + ('tourism', 'viewpoint'): 'tourist_view_point', + ('tourism', 'guesthouse'): 'accommodation_bed_and_breakfast', + ('railway', 'tram'): 'transport_tram_stop', + ('amenity', 'courthouse'): 'amenity_court', + ('amenity', 'recycling'): 'amenity_recycling', + ('amenity', 'dentist'): 'health_dentist', + ('natural', 'beach'): 'tourist_beach', + ('railway', 'tram_stop'): 'transport_tram_stop', + ('amenity', 'prison'): 'amenity_prison', + ('highway', 'bus_stop'): 'transport_bus_stop2' +} diff --git a/nominatim/api/v1/format.py b/nominatim/api/v1/format.py index 116e2ae6..3643af83 100644 --- a/nominatim/api/v1/format.py +++ b/nominatim/api/v1/format.py @@ -7,22 +7,26 @@ """ Output formatters for API version v1. """ +from typing import Mapping, Any +import collections + +import nominatim.api as napi from nominatim.api.result_formatting import FormatDispatcher -from nominatim.api import StatusResult +from nominatim.api.v1.classtypes import ICONS from nominatim.utils.json_writer import JsonWriter dispatch = FormatDispatcher() -@dispatch.format_func(StatusResult, 'text') -def _format_status_text(result: StatusResult) -> str: +@dispatch.format_func(napi.StatusResult, 'text') +def _format_status_text(result: napi.StatusResult, _: Mapping[str, Any]) -> str: if result.status: return f"ERROR: {result.message}" return 'OK' -@dispatch.format_func(StatusResult, 'json') -def _format_status_json(result: StatusResult) -> str: +@dispatch.format_func(napi.StatusResult, 'json') +def _format_status_json(result: napi.StatusResult, _: Mapping[str, Any]) -> str: out = JsonWriter() out.start_object()\ @@ -35,3 +39,125 @@ def _format_status_json(result: StatusResult) -> str: .end_object() return out() + + +def _add_address_row(writer: JsonWriter, row: napi.AddressLine, + locales: napi.Locales) -> None: + writer.start_object()\ + .keyval('localname', locales.display_name(row.names))\ + .keyval('place_id', row.place_id) + + if row.osm_object is not None: + writer.keyval('osm_id', row.osm_object[1])\ + .keyval('osm_type', row.osm_object[0]) + + if row.extratags: + writer.keyval_not_none('place_type', row.extratags.get('place_type')) + + writer.keyval('class', row.category[0])\ + .keyval('type', row.category[1])\ + .keyval_not_none('admin_level', row.admin_level)\ + .keyval('rank_address', row.rank_address)\ + .keyval('distance', row.distance)\ + .keyval('isaddress', row.isaddress)\ + .end_object() + + +def _add_address_rows(writer: JsonWriter, section: str, rows: napi.AddressLines, + locales: napi.Locales) -> None: + writer.key(section).start_array() + for row in rows: + _add_address_row(writer, row, locales) + writer.next() + writer.end_array().next() + + +def _add_parent_rows_grouped(writer: JsonWriter, rows: napi.AddressLines, + locales: napi.Locales) -> None: + # group by category type + data = collections.defaultdict(list) + for row in rows: + sub = JsonWriter() + _add_address_row(sub, row, locales) + data[row.category[1]].append(sub()) + + writer.key('hierarchy').start_object() + for group, grouped in data.items(): + writer.key(group).start_array() + grouped.sort() # sorts alphabetically by local name + for line in grouped: + writer.raw(line).next() + writer.end_array().next() + + writer.end_object().next() + + +@dispatch.format_func(napi.SearchResult, 'details-json') +def _format_search_json(result: napi.SearchResult, options: Mapping[str, Any]) -> str: + locales = options.get('locales', napi.Locales()) + geom = result.geometry.get('geojson') + centroid = result.centroid_as_geojson() + + out = JsonWriter() + out.start_object()\ + .keyval('place_id', result.place_id)\ + .keyval('parent_place_id', result.parent_place_id) + + if result.osm_object is not None: + out.keyval('osm_type', result.osm_object[0])\ + .keyval('osm_id', result.osm_object[1]) + + out.keyval('category', result.category[0])\ + .keyval('type', result.category[1])\ + .keyval('admin_level', result.admin_level)\ + .keyval('localname', locales.display_name(result.names))\ + .keyval('names', result.names or [])\ + .keyval('addresstags', result.address or [])\ + .keyval('housenumber', result.housenumber)\ + .keyval('calculated_postcode', result.postcode)\ + .keyval('country_code', result.country_code)\ + .keyval_not_none('indexed_date', result.indexed_date, lambda v: v.isoformat())\ + .keyval('importance', result.importance)\ + .keyval('calculated_importance', result.calculated_importance())\ + .keyval('extratags', result.extratags or [])\ + .keyval('calculated_wikipedia', result.wikipedia)\ + .keyval('rank_address', result.rank_address)\ + .keyval('rank_search', result.rank_search)\ + .keyval('isarea', 'Polygon' in (geom or result.geometry.get('type') or ''))\ + .key('centroid').raw(centroid).next()\ + .key('geometry').raw(geom or centroid).next() + + if options.get('icon_base_url', None): + icon = ICONS.get(result.category) + if icon: + out.keyval('icon', f"{options['icon_base_url']}/{icon}.p.20.png") + + if result.address_rows is not None: + _add_address_rows(out, 'address', result.address_rows, locales) + + if result.linked_rows is not None: + _add_address_rows(out, 'linked_places', result.linked_rows, locales) + + if result.name_keywords is not None or result.address_keywords is not None: + out.key('keywords').start_object() + + for sec, klist in (('name', result.name_keywords), ('address', result.address_keywords)): + out.key(sec).start_array() + for word in (klist or []): + out.start_object()\ + .keyval('id', word.word_id)\ + .keyval('token', word.word_token)\ + .end_object().next() + out.end_array().next() + + out.end_object().next() + + if result.parented_rows is not None: + if options.get('group_hierarchy', False): + _add_parent_rows_grouped(out, result.parented_rows, locales) + else: + _add_address_rows(out, 'hierarchy', result.parented_rows, locales) + + out.end_object() + + return out() diff --git a/nominatim/api/v1/server_glue.py b/nominatim/api/v1/server_glue.py index 7444b7aa..52ce747c 100644 --- a/nominatim/api/v1/server_glue.py +++ b/nominatim/api/v1/server_glue.py @@ -143,7 +143,7 @@ async def status_endpoint(api: napi.NominatimAPIAsync, params: ASGIAdaptor) -> A else: status_code = 200 - return params.build_response(formatting.format_result(result, fmt), fmt, + return params.build_response(formatting.format_result(result, fmt, {}), fmt, status=status_code) EndpointFunc = Callable[[napi.NominatimAPIAsync, ASGIAdaptor], Any] diff --git a/nominatim/clicmd/api.py b/nominatim/clicmd/api.py index cc65f5f6..523013a6 100644 --- a/nominatim/clicmd/api.py +++ b/nominatim/clicmd/api.py @@ -10,11 +10,13 @@ Subcommand definitions for API calls from the command line. from typing import Mapping, Dict import argparse import logging +import json +import sys from nominatim.tools.exec_utils import run_api_script from nominatim.errors import UsageError from nominatim.clicmd.args import NominatimArgs -from nominatim.api import NominatimAPI, StatusResult +import nominatim.api as napi import nominatim.api.v1 as api_output # Do not repeat documentation of subcommand classes. @@ -38,15 +40,6 @@ EXTRADATA_PARAMS = ( ('namedetails', 'Include a list of alternative names') ) -DETAILS_SWITCHES = ( - ('addressdetails', 'Include a breakdown of the address into elements'), - ('keywords', 'Include a list of name keywords and address keywords'), - ('linkedplaces', 'Include a details of places that are linked with this one'), - ('hierarchy', 'Include details of places lower in the address hierarchy'), - ('group_hierarchy', 'Group the places by type'), - ('polygon_geojson', 'Include geometry of result') -) - def _add_api_output_arguments(parser: argparse.ArgumentParser) -> None: group = parser.add_argument_group('Output arguments') group.add_argument('--format', default='jsonv2', @@ -240,29 +233,66 @@ class APIDetails: "of the same object.")) group = parser.add_argument_group('Output arguments') - for name, desc in DETAILS_SWITCHES: - group.add_argument('--' + name, action='store_true', help=desc) + 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 a list of name keywords and address keywords') + group.add_argument('--linkedplaces', action='store_true', + help='Include a details of places that are linked with this one') + group.add_argument('--hierarchy', action='store_true', + help='Include details of places lower in the address hierarchy') + group.add_argument('--group_hierarchy', action='store_true', + help='Group the places by type') + group.add_argument('--polygon_geojson', action='store_true', + help='Include geometry of result') group.add_argument('--lang', '--accept-language', metavar='LANGS', help='Preferred language order for presenting search results') def run(self, args: NominatimArgs) -> int: + place: napi.PlaceRef if args.node: - params = dict(osmtype='N', osmid=args.node) + place = napi.OsmID('N', args.node, args.object_class) elif args.way: - params = dict(osmtype='W', osmid=args.way) + place = napi.OsmID('W', args.way, args.object_class) elif args.relation: - params = dict(osmtype='R', osmid=args.relation) + place = napi.OsmID('R', args.relation, args.object_class) else: - params = dict(place_id=args.place_id) - if args.object_class: - params['class'] = args.object_class - for name, _ in DETAILS_SWITCHES: - params[name] = '1' if getattr(args, name) else '0' + assert args.place_id is not None + place = napi.PlaceID(args.place_id) + + api = napi.NominatimAPI(args.project_dir) + + details = napi.LookupDetails(address_details=args.addressdetails, + linked_places=args.linkedplaces, + parented_places=args.hierarchy, + keywords=args.keywords) + if args.polygon_geojson: + details.geometry_output = napi.GeometryFormat.GEOJSON + if args.lang: - params['accept-language'] = args.lang + locales = napi.Locales.from_accept_languages(args.lang) + elif api.config.DEFAULT_LANGUAGE: + locales = napi.Locales.from_accept_languages(api.config.DEFAULT_LANGUAGE) + else: + locales = napi.Locales() + + result = api.lookup(place, details) + + if result: + output = api_output.format_result( + result, + 'details-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) + sys.stdout.write('\n') + + return 0 - return _run_api('details', args, params) + LOG.error("Object not found in database.") + return 42 class APIStatus: @@ -276,13 +306,13 @@ class APIStatus: """ def add_args(self, parser: argparse.ArgumentParser) -> None: - formats = api_output.list_formats(StatusResult) + 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') def run(self, args: NominatimArgs) -> int: - status = NominatimAPI(args.project_dir).status() - print(api_output.format_result(status, args.format)) + status = napi.NominatimAPI(args.project_dir).status() + print(api_output.format_result(status, args.format, {})) return 0 diff --git a/nominatim/clicmd/args.py b/nominatim/clicmd/args.py index e47287b3..9be20b20 100644 --- a/nominatim/clicmd/args.py +++ b/nominatim/clicmd/args.py @@ -168,6 +168,11 @@ class NominatimArgs: # Arguments to 'details' object_class: Optional[str] + linkedplaces: bool + hierarchy: bool + keywords: bool + polygon_geojson: bool + group_hierarchy: bool def osm2pgsql_options(self, default_cache: int, diff --git a/test/python/api/test_localization.py b/test/python/api/test_localization.py new file mode 100644 index 00000000..b704e5a9 --- /dev/null +++ b/test/python/api/test_localization.py @@ -0,0 +1,53 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of Nominatim. (https://nominatim.org) +# +# Copyright (C) 2023 by the Nominatim developer community. +# For a full list of authors see the git log. +""" +Test functions for adapting results to the user's locale. +""" +import pytest + +from nominatim.api import Locales + +def test_display_name_empty_names(): + l = Locales(['en', 'de']) + + assert l.display_name(None) == '' + assert l.display_name({}) == '' + +def test_display_name_none_localized(): + l = Locales() + + assert l.display_name({}) == '' + assert l.display_name({'name:de': 'DE', 'name': 'ALL'}) == 'ALL' + assert l.display_name({'ref': '34', 'name:de': 'DE'}) == '34' + + +def test_display_name_localized(): + l = Locales(['en', 'de']) + + assert l.display_name({}) == '' + assert l.display_name({'name:de': 'DE', 'name': 'ALL'}) == 'DE' + assert l.display_name({'ref': '34', 'name:de': 'DE'}) == 'DE' + + +def test_display_name_preference(): + l = Locales(['en', 'de']) + + assert l.display_name({}) == '' + assert l.display_name({'name:de': 'DE', 'name:en': 'EN'}) == 'EN' + assert l.display_name({'official_name:en': 'EN', 'name:de': 'DE'}) == 'DE' + + +@pytest.mark.parametrize('langstr,langlist', + [('fr', ['fr']), + ('fr-FR', ['fr-FR', 'fr']), + ('de,fr-FR', ['de', 'fr-FR', 'fr']), + ('fr,de,fr-FR', ['fr', 'de', 'fr-FR']), + ('en;q=0.5,fr', ['fr', 'en']), + ('en;q=0.5,fr,en-US', ['fr', 'en-US', 'en']), + ('en,fr;garbage,de', ['en', 'de'])]) +def test_from_language_preferences(langstr, langlist): + assert Locales.from_accept_languages(langstr).languages == langlist diff --git a/test/python/api/test_result_formatting_v1.py b/test/python/api/test_result_formatting_v1.py index 4a5d5989..01cca049 100644 --- a/test/python/api/test_result_formatting_v1.py +++ b/test/python/api/test_result_formatting_v1.py @@ -32,17 +32,17 @@ def test_status_unsupported(): def test_status_format_text(): - assert api_impl.format_result(StatusResult(0, 'message here'), 'text') == 'OK' + assert api_impl.format_result(StatusResult(0, 'message here'), 'text', {}) == 'OK' def test_status_format_text(): - assert api_impl.format_result(StatusResult(500, 'message here'), 'text') == 'ERROR: message here' + assert api_impl.format_result(StatusResult(500, 'message here'), 'text', {}) == 'ERROR: message here' def test_status_format_json_minimal(): status = StatusResult(700, 'Bad format.') - result = api_impl.format_result(status, 'json') + result = api_impl.format_result(status, 'json', {}) assert result == '{"status":700,"message":"Bad format.","software_version":"%s"}' % (NOMINATIM_VERSION, ) @@ -52,6 +52,6 @@ def test_status_format_json_full(): 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 = api_impl.format_result(status, 'json', {}) assert result == '{"status":0,"message":"OK","data_updated":"2010-02-07T20:20:03+00:00","software_version":"%s","database_version":"5.6"}' % (NOMINATIM_VERSION, ) diff --git a/test/python/cli/test_cmd_api.py b/test/python/cli/test_cmd_api.py index b0c2411f..966059c4 100644 --- a/test/python/cli/test_cmd_api.py +++ b/test/python/cli/test_cmd_api.py @@ -25,11 +25,7 @@ def test_no_api_without_phpcgi(endpoint): @pytest.mark.parametrize("params", [('search', '--query', 'new'), ('search', '--city', 'Berlin'), ('reverse', '--lat', '0', '--lon', '0', '--zoom', '13'), - ('lookup', '--id', 'N1'), - ('details', '--node', '1'), - ('details', '--way', '1'), - ('details', '--relation', '1'), - ('details', '--place_id', '10001')]) + ('lookup', '--id', 'N1')]) class TestCliApiCallPhp: @pytest.fixture(autouse=True) @@ -79,6 +75,29 @@ class TestCliStatusCall: json.loads(capsys.readouterr().out) +class TestCliDetailsCall: + + @pytest.fixture(autouse=True) + def setup_status_mock(self, monkeypatch): + result = napi.SearchResult(napi.SourceTable.PLACEX, ('place', 'thing'), + (1.0, -3.0)) + + monkeypatch.setattr(napi.NominatimAPI, 'lookup', + lambda *args: result) + + @pytest.mark.parametrize("params", [('--node', '1'), + ('--way', '1'), + ('--relation', '1'), + ('--place_id', '10001')]) + + def test_status_json_format(self, cli_call, tmp_path, capsys, params): + result = cli_call('details', '--project-dir', str(tmp_path), *params) + + assert result == 0 + + json.loads(capsys.readouterr().out) + + QUERY_PARAMS = { 'search': ('--query', 'somewhere'), 'reverse': ('--lat', '20', '--lon', '30'), @@ -157,27 +176,3 @@ def test_cli_search_param_dedupe(cli_call, project_env): assert cli_call('search', *QUERY_PARAMS['search'], '--project-dir', str(project_env.project_dir), '--no-dedupe') == 0 - - -def test_cli_details_param_class(cli_call, project_env): - webdir = project_env.project_dir / 'website' - webdir.mkdir() - (webdir / 'details.php').write_text(f"""