switch details cli command to new Python implementation

This commit is contained in:
Sarah Hoffmann
2023-02-03 10:43:54 +01:00
parent 1924beeb20
commit 104722a56a
11 changed files with 474 additions and 69 deletions

View File

@@ -28,3 +28,4 @@ from .results import (SourceTable as SourceTable,
WordInfo as WordInfo, WordInfo as WordInfo,
WordInfos as WordInfos, WordInfos as WordInfos,
SearchResult as SearchResult) SearchResult as SearchResult)
from .localization import (Locales as Locales)

View File

@@ -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)

View File

@@ -7,11 +7,11 @@
""" """
Helper classes and functions for formating results into API responses. 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 from collections import defaultdict
T = TypeVar('T') # pylint: disable=invalid-name T = TypeVar('T') # pylint: disable=invalid-name
FormatFunc = Callable[[T], str] FormatFunc = Callable[[T, Mapping[str, Any]], str]
class FormatDispatcher: class FormatDispatcher:
@@ -47,10 +47,10 @@ class FormatDispatcher:
return fmt in self.format_functions[result_type] 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. """ Convert the given result into a string using the given format.
The format is expected to be in the list returned by The format is expected to be in the list returned by
`list_formats()`. `list_formats()`.
""" """
return self.format_functions[type(result)][fmt](result) return self.format_functions[type(result)][fmt](result, options)

View File

@@ -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'
}

View File

@@ -7,22 +7,26 @@
""" """
Output formatters for API version v1. 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.result_formatting import FormatDispatcher
from nominatim.api import StatusResult from nominatim.api.v1.classtypes import ICONS
from nominatim.utils.json_writer import JsonWriter from nominatim.utils.json_writer import JsonWriter
dispatch = FormatDispatcher() dispatch = FormatDispatcher()
@dispatch.format_func(StatusResult, 'text') @dispatch.format_func(napi.StatusResult, 'text')
def _format_status_text(result: StatusResult) -> str: def _format_status_text(result: napi.StatusResult, _: Mapping[str, Any]) -> str:
if result.status: if result.status:
return f"ERROR: {result.message}" return f"ERROR: {result.message}"
return 'OK' return 'OK'
@dispatch.format_func(StatusResult, 'json') @dispatch.format_func(napi.StatusResult, 'json')
def _format_status_json(result: StatusResult) -> str: def _format_status_json(result: napi.StatusResult, _: Mapping[str, Any]) -> str:
out = JsonWriter() out = JsonWriter()
out.start_object()\ out.start_object()\
@@ -35,3 +39,125 @@ def _format_status_json(result: StatusResult) -> str:
.end_object() .end_object()
return out() 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()

View File

@@ -143,7 +143,7 @@ async def status_endpoint(api: napi.NominatimAPIAsync, params: ASGIAdaptor) -> A
else: else:
status_code = 200 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) status=status_code)
EndpointFunc = Callable[[napi.NominatimAPIAsync, ASGIAdaptor], Any] EndpointFunc = Callable[[napi.NominatimAPIAsync, ASGIAdaptor], Any]

View File

@@ -10,11 +10,13 @@ Subcommand definitions for API calls from the command line.
from typing import Mapping, Dict from typing import Mapping, Dict
import argparse import argparse
import logging import logging
import json
import sys
from nominatim.tools.exec_utils import run_api_script from nominatim.tools.exec_utils import run_api_script
from nominatim.errors import UsageError from nominatim.errors import UsageError
from nominatim.clicmd.args import NominatimArgs from nominatim.clicmd.args import NominatimArgs
from nominatim.api import NominatimAPI, StatusResult import nominatim.api as napi
import nominatim.api.v1 as api_output import nominatim.api.v1 as api_output
# Do not repeat documentation of subcommand classes. # Do not repeat documentation of subcommand classes.
@@ -38,15 +40,6 @@ EXTRADATA_PARAMS = (
('namedetails', 'Include a list of alternative names') ('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: def _add_api_output_arguments(parser: argparse.ArgumentParser) -> None:
group = parser.add_argument_group('Output arguments') group = parser.add_argument_group('Output arguments')
group.add_argument('--format', default='jsonv2', group.add_argument('--format', default='jsonv2',
@@ -240,29 +233,66 @@ class APIDetails:
"of the same object.")) "of the same object."))
group = parser.add_argument_group('Output arguments') group = parser.add_argument_group('Output arguments')
for name, desc in DETAILS_SWITCHES: group.add_argument('--addressdetails', action='store_true',
group.add_argument('--' + name, action='store_true', help=desc) 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', group.add_argument('--lang', '--accept-language', metavar='LANGS',
help='Preferred language order for presenting search results') help='Preferred language order for presenting search results')
def run(self, args: NominatimArgs) -> int: def run(self, args: NominatimArgs) -> int:
place: napi.PlaceRef
if args.node: if args.node:
params = dict(osmtype='N', osmid=args.node) place = napi.OsmID('N', args.node, args.object_class)
elif args.way: elif args.way:
params = dict(osmtype='W', osmid=args.way) place = napi.OsmID('W', args.way, args.object_class)
elif args.relation: elif args.relation:
params = dict(osmtype='R', osmid=args.relation) place = napi.OsmID('R', args.relation, args.object_class)
else: else:
params = dict(place_id=args.place_id) assert args.place_id is not None
if args.object_class: place = napi.PlaceID(args.place_id)
params['class'] = args.object_class
for name, _ in DETAILS_SWITCHES:
params[name] = '1' if getattr(args, name) else '0'
if args.lang:
params['accept-language'] = args.lang
return _run_api('details', args, params) 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:
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
LOG.error("Object not found in database.")
return 42
class APIStatus: class APIStatus:
@@ -276,13 +306,13 @@ class APIStatus:
""" """
def add_args(self, parser: argparse.ArgumentParser) -> None: 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 = parser.add_argument_group('API parameters')
group.add_argument('--format', default=formats[0], choices=formats, group.add_argument('--format', default=formats[0], choices=formats,
help='Format of result') help='Format of result')
def run(self, args: NominatimArgs) -> int: def run(self, args: NominatimArgs) -> int:
status = NominatimAPI(args.project_dir).status() status = napi.NominatimAPI(args.project_dir).status()
print(api_output.format_result(status, args.format)) print(api_output.format_result(status, args.format, {}))
return 0 return 0

View File

@@ -168,6 +168,11 @@ class NominatimArgs:
# Arguments to 'details' # Arguments to 'details'
object_class: Optional[str] object_class: Optional[str]
linkedplaces: bool
hierarchy: bool
keywords: bool
polygon_geojson: bool
group_hierarchy: bool
def osm2pgsql_options(self, default_cache: int, def osm2pgsql_options(self, default_cache: int,

View File

@@ -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

View File

@@ -32,17 +32,17 @@ def test_status_unsupported():
def test_status_format_text(): 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(): 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(): def test_status_format_json_minimal():
status = StatusResult(700, 'Bad format.') 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, ) 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.data_updated = dt.datetime(2010, 2, 7, 20, 20, 3, 0, tzinfo=dt.timezone.utc)
status.database_version = '5.6' 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, ) assert result == '{"status":0,"message":"OK","data_updated":"2010-02-07T20:20:03+00:00","software_version":"%s","database_version":"5.6"}' % (NOMINATIM_VERSION, )

View File

@@ -25,11 +25,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'),
('reverse', '--lat', '0', '--lon', '0', '--zoom', '13'), ('reverse', '--lat', '0', '--lon', '0', '--zoom', '13'),
('lookup', '--id', 'N1'), ('lookup', '--id', 'N1')])
('details', '--node', '1'),
('details', '--way', '1'),
('details', '--relation', '1'),
('details', '--place_id', '10001')])
class TestCliApiCallPhp: class TestCliApiCallPhp:
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
@@ -79,6 +75,29 @@ class TestCliStatusCall:
json.loads(capsys.readouterr().out) 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 = { QUERY_PARAMS = {
'search': ('--query', 'somewhere'), 'search': ('--query', 'somewhere'),
'reverse': ('--lat', '20', '--lon', '30'), '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), assert cli_call('search', *QUERY_PARAMS['search'], '--project-dir', str(project_env.project_dir),
'--no-dedupe') == 0 '--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"""<?php
exit($_GET['class'] == 'highway' ? 0 : 10);
""")
assert cli_call('details', *QUERY_PARAMS['details'], '--project-dir', str(project_env.project_dir),
'--class', 'highway') == 0
@pytest.mark.parametrize('param', ('lang', 'accept-language'))
def test_cli_details_param_lang(cli_call, project_env, param):
webdir = project_env.project_dir / 'website'
webdir.mkdir()
(webdir / 'details.php').write_text(f"""<?php
exit($_GET['accept-language'] == 'es' ? 0 : 10);
""")
assert cli_call('details', *QUERY_PARAMS['details'], '--project-dir', str(project_env.project_dir),
'--' + param, 'es') == 0