mirror of
https://github.com/osm-search/Nominatim.git
synced 2026-02-14 18:37:58 +00:00
Merge pull request #3517 from lonvia/improve-custom-formatter
Extend use of custom result formatters to CLI tool
This commit is contained in:
@@ -66,6 +66,8 @@ For example, let us extend the result for the status call in text format
|
||||
and add the server URL. Such a formatter would look like this:
|
||||
|
||||
``` python
|
||||
from nominatim_api import StatusResult
|
||||
|
||||
@dispatch.format_func(StatusResult, 'text')
|
||||
def _format_status_text(result, _):
|
||||
header = 'Status for server nominatim.openstreetmap.org'
|
||||
@@ -86,19 +88,39 @@ as adding formatting functions for all result types using the custom
|
||||
format name:
|
||||
|
||||
``` python
|
||||
from nominatim_api import StatusResult
|
||||
|
||||
@dispatch.format_func(StatusResult, 'chatty')
|
||||
def _format_status_text(result, _):
|
||||
if result.status:
|
||||
return f"The server is currently not running. {result.message}"
|
||||
|
||||
return f"Good news! The server is running just fine."
|
||||
return "Good news! The server is running just fine."
|
||||
```
|
||||
|
||||
That's all. Nominatim will automatically pick up the new format name and
|
||||
will allow the user to use it. Make sure to really define formatters for
|
||||
**all** result types. If they are for endpoints that you do not intend to
|
||||
use, you can simply return some static string but the function needs to be
|
||||
there.
|
||||
will allow the user to use it. There is no need to implement formatter
|
||||
functions for all the result types, when you invent a new one. The
|
||||
available formats will be determined for each API endpoint separately.
|
||||
To find out which formats are available, you can use the `--list-formats`
|
||||
option of the CLI tool:
|
||||
|
||||
```
|
||||
me@machine:planet-project$ nominatim status --list-formats
|
||||
2024-08-16 19:54:00: Using project directory: /home/nominatim/planet-project
|
||||
text
|
||||
json
|
||||
chatty
|
||||
debug
|
||||
me@machine:planet-project$
|
||||
```
|
||||
|
||||
The `debug` format listed in the last line will always appear. It is a
|
||||
special format that enables debug output via the command line (the same
|
||||
as the `debug=1` parameter enables for the server API). To not clash
|
||||
with this built-in function, you shouldn't name your own format 'debug'.
|
||||
|
||||
### Content type of new formats
|
||||
|
||||
All responses will be returned with the content type application/json by
|
||||
default. If your format produces a different content type, you need
|
||||
@@ -117,6 +139,67 @@ The `content_types` module used above provides constants for the most
|
||||
frequent content types. You set the content type to an arbitrary string,
|
||||
if the content type you need is not available.
|
||||
|
||||
## Formatting error messages
|
||||
|
||||
Any exception thrown during processing of a request is given to
|
||||
a special error formatting function. It takes the requested content type,
|
||||
the status code and the error message. It should return the error message
|
||||
in a form appropriate for the given content type.
|
||||
|
||||
You can overwrite the default formatting function with the decorator
|
||||
`error_format_func`:
|
||||
|
||||
``` python
|
||||
import nominatim_api.server.content_types as ct
|
||||
|
||||
@dispatch.error_format_func
|
||||
def _format_error(content_type: str, msg: str, status: int) -> str:
|
||||
if content_type == ct.CONTENT_XML:
|
||||
return f"""<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<message>{msg}</message>
|
||||
"""
|
||||
if content_type == ct.CONTENT_JSON:
|
||||
return f'"{msg}"'
|
||||
|
||||
return f"ERROR: {msg}"
|
||||
```
|
||||
|
||||
|
||||
## Debugging custom formatters
|
||||
|
||||
The easiest way to try out your custom formatter is by using the Nominatim
|
||||
CLI commands. Custom formats can be chosen with the `--format` parameter:
|
||||
|
||||
```
|
||||
me@machine:planet-project$ nominatim status --format chatty
|
||||
2024-08-16 19:54:00: Using project directory: /home/nominatim/planet-project
|
||||
Good news! The server is running just fine.
|
||||
me@machine:planet-project$
|
||||
```
|
||||
|
||||
They will also emit full error messages when there is a problem with the
|
||||
code you need to debug.
|
||||
|
||||
!!! danger
|
||||
In some cases, when you make an error with your import statement, the
|
||||
CLI will not give you an error but instead tell you, that the API
|
||||
commands are no longer available:
|
||||
|
||||
me@machine: nominatim status
|
||||
usage: nominatim [-h] [--version] {import,freeze,replication,special-phrases,add-data,index,refresh,admin} ...
|
||||
nominatim: error: argument subcommand: invalid choice: 'status'
|
||||
|
||||
This happens because the CLI tool is meant to still work when the
|
||||
nominatim-api package is not installed. Import errors involving
|
||||
`nominatim_api` are interpreted as "package not installed".
|
||||
|
||||
Use the help command to find out which is the offending import that
|
||||
could not be found:
|
||||
|
||||
me@machine: nominatim -h
|
||||
... [other help text] ...
|
||||
Nominatim API package not found (was looking for module: nominatim_api.xxx).
|
||||
|
||||
## Reference
|
||||
|
||||
### FormatDispatcher
|
||||
|
||||
@@ -39,6 +39,7 @@ from .results import (SourceTable as SourceTable,
|
||||
SearchResult as SearchResult,
|
||||
SearchResults as SearchResults)
|
||||
from .localization import (Locales as Locales)
|
||||
from .result_formatting import (FormatDispatcher as FormatDispatcher)
|
||||
from .result_formatting import (FormatDispatcher as FormatDispatcher,
|
||||
load_format_dispatcher as load_format_dispatcher)
|
||||
|
||||
from .version import NOMINATIM_API_VERSION as __version__
|
||||
|
||||
@@ -11,9 +11,3 @@ Implementation of API version v1 (aka the legacy version).
|
||||
#pylint: disable=useless-import-alias
|
||||
|
||||
from .server_glue import ROUTES as ROUTES
|
||||
|
||||
from . import format as _format
|
||||
|
||||
list_formats = _format.dispatch.list_formats
|
||||
supports_format = _format.dispatch.supports_format
|
||||
format_result = _format.dispatch.format_result
|
||||
|
||||
@@ -243,7 +243,8 @@ def get_set_parser() -> CommandlineParser:
|
||||
raise ex
|
||||
|
||||
parser.parser.epilog = \
|
||||
'\n\nNominatim API package not found. The following commands are not available:'\
|
||||
f'\n\nNominatim API package not found (was looking for module: {ex.name}).'\
|
||||
'\nThe following commands are not available:'\
|
||||
'\n export, convert, serve, search, reverse, lookup, details, status'\
|
||||
"\n\nRun 'pip install nominatim-api' to install the package."
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"""
|
||||
Subcommand definitions for API calls from the command line.
|
||||
"""
|
||||
from typing import Dict, Any, Optional
|
||||
from typing import Dict, Any, Optional, Type, Mapping
|
||||
import argparse
|
||||
import logging
|
||||
import json
|
||||
@@ -15,9 +15,8 @@ import sys
|
||||
from functools import reduce
|
||||
|
||||
import nominatim_api as napi
|
||||
import nominatim_api.v1 as api_output
|
||||
from nominatim_api.v1.helpers import zoom_to_rank, deduplicate_results
|
||||
from nominatim_api.v1.format import dispatch as formatting
|
||||
from nominatim_api.server.content_types import CONTENT_JSON
|
||||
import nominatim_api.logging as loglib
|
||||
from ..errors import UsageError
|
||||
from .args import NominatimArgs
|
||||
@@ -44,11 +43,16 @@ EXTRADATA_PARAMS = (
|
||||
('namedetails', 'Include a list of alternative names')
|
||||
)
|
||||
|
||||
def _add_list_format(parser: argparse.ArgumentParser) -> None:
|
||||
group = parser.add_argument_group('Other options')
|
||||
group.add_argument('--list-formats', action='store_true',
|
||||
help='List supported output formats and exit.')
|
||||
|
||||
|
||||
def _add_api_output_arguments(parser: argparse.ArgumentParser) -> None:
|
||||
group = parser.add_argument_group('Output arguments')
|
||||
group.add_argument('--format', default='jsonv2',
|
||||
choices=formatting.list_formats(napi.SearchResults) + ['debug'],
|
||||
help='Format of result')
|
||||
group = parser.add_argument_group('Output formatting')
|
||||
group.add_argument('--format', type=str, default='jsonv2',
|
||||
help='Format of result (use --list-format to see supported formats)')
|
||||
for name, desc in EXTRADATA_PARAMS:
|
||||
group.add_argument('--' + name, action='store_true', help=desc)
|
||||
|
||||
@@ -105,6 +109,30 @@ def _get_layers(args: NominatimArgs, default: napi.DataLayer) -> Optional[napi.D
|
||||
(napi.DataLayer[s.upper()] for s in args.layers))
|
||||
|
||||
|
||||
def _list_formats(formatter: napi.FormatDispatcher, rtype: Type[Any]) -> int:
|
||||
for fmt in formatter.list_formats(rtype):
|
||||
print(fmt)
|
||||
print('debug')
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def _print_output(formatter: napi.FormatDispatcher, result: Any,
|
||||
fmt: str, options: Mapping[str, Any]) -> None:
|
||||
output = formatter.format_result(result, fmt, options)
|
||||
if formatter.get_content_type(fmt) == CONTENT_JSON:
|
||||
# reformat the result, so it is pretty-printed
|
||||
try:
|
||||
json.dump(json.loads(output), sys.stdout, indent=4, ensure_ascii=False)
|
||||
except json.decoder.JSONDecodeError as err:
|
||||
# Catch the error here, so that data can be debugged,
|
||||
# when people are developping custom result formatters.
|
||||
LOG.fatal("Parsing json failed: %s\nUnformatted output:\n%s", err, output)
|
||||
else:
|
||||
sys.stdout.write(output)
|
||||
sys.stdout.write('\n')
|
||||
|
||||
|
||||
class APISearch:
|
||||
"""\
|
||||
Execute a search query.
|
||||
@@ -135,18 +163,24 @@ class APISearch:
|
||||
help='Preferred area to find search results')
|
||||
group.add_argument('--bounded', action='store_true',
|
||||
help='Strictly restrict results to viewbox area')
|
||||
|
||||
group = parser.add_argument_group('Other arguments')
|
||||
group.add_argument('--no-dedupe', action='store_false', dest='dedupe',
|
||||
help='Do not remove duplicates from the result list')
|
||||
_add_list_format(parser)
|
||||
|
||||
|
||||
def run(self, args: NominatimArgs) -> int:
|
||||
formatter = napi.load_format_dispatcher('v1', args.project_dir)
|
||||
|
||||
if args.list_formats:
|
||||
return _list_formats(formatter, napi.SearchResults)
|
||||
|
||||
if args.format == 'debug':
|
||||
loglib.set_log_output('text')
|
||||
elif not formatter.supports_format(napi.SearchResults, args.format):
|
||||
raise UsageError(f"Unsupported format '{args.format}'. "
|
||||
'Use --list-formats to see supported formats.')
|
||||
|
||||
api = napi.NominatimAPI(args.project_dir)
|
||||
|
||||
params: Dict[str, Any] = {'max_results': args.limit + min(args.limit, 10),
|
||||
'address_details': True, # needed for display name
|
||||
'geometry_output': _get_geometry_output(args),
|
||||
@@ -177,19 +211,10 @@ class APISearch:
|
||||
print(loglib.get_and_disable())
|
||||
return 0
|
||||
|
||||
output = api_output.format_result(
|
||||
results,
|
||||
args.format,
|
||||
{'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')
|
||||
|
||||
_print_output(formatter, results, args.format,
|
||||
{'extratags': args.extratags,
|
||||
'namedetails': args.namedetails,
|
||||
'addressdetails': args.addressdetails})
|
||||
return 0
|
||||
|
||||
|
||||
@@ -205,9 +230,9 @@ class APIReverse:
|
||||
|
||||
def add_args(self, parser: argparse.ArgumentParser) -> None:
|
||||
group = parser.add_argument_group('Query arguments')
|
||||
group.add_argument('--lat', type=float, required=True,
|
||||
group.add_argument('--lat', type=float,
|
||||
help='Latitude of coordinate to look up (in WGS84)')
|
||||
group.add_argument('--lon', type=float, required=True,
|
||||
group.add_argument('--lon', type=float,
|
||||
help='Longitude of coordinate to look up (in WGS84)')
|
||||
group.add_argument('--zoom', type=int,
|
||||
help='Level of detail required for the address')
|
||||
@@ -217,14 +242,25 @@ class APIReverse:
|
||||
help='OSM id to lookup in format <NRW><id> (may be repeated)')
|
||||
|
||||
_add_api_output_arguments(parser)
|
||||
_add_list_format(parser)
|
||||
|
||||
|
||||
def run(self, args: NominatimArgs) -> int:
|
||||
formatter = napi.load_format_dispatcher('v1', args.project_dir)
|
||||
|
||||
if args.list_formats:
|
||||
return _list_formats(formatter, napi.ReverseResults)
|
||||
|
||||
if args.format == 'debug':
|
||||
loglib.set_log_output('text')
|
||||
elif not formatter.supports_format(napi.ReverseResults, args.format):
|
||||
raise UsageError(f"Unsupported format '{args.format}'. "
|
||||
'Use --list-formats to see supported formats.')
|
||||
|
||||
if args.lat is None or args.lon is None:
|
||||
raise UsageError("lat' and 'lon' parameters are required.")
|
||||
|
||||
api = napi.NominatimAPI(args.project_dir)
|
||||
|
||||
result = api.reverse(napi.Point(args.lon, args.lat),
|
||||
max_rank=zoom_to_rank(args.zoom or 18),
|
||||
layers=_get_layers(args, napi.DataLayer.ADDRESS | napi.DataLayer.POI),
|
||||
@@ -238,18 +274,10 @@ class APIReverse:
|
||||
return 0
|
||||
|
||||
if result:
|
||||
output = api_output.format_result(
|
||||
napi.ReverseResults([result]),
|
||||
args.format,
|
||||
{'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')
|
||||
_print_output(formatter, napi.ReverseResults([result]), args.format,
|
||||
{'extratags': args.extratags,
|
||||
'namedetails': args.namedetails,
|
||||
'addressdetails': args.addressdetails})
|
||||
|
||||
return 0
|
||||
|
||||
@@ -271,43 +299,45 @@ class APILookup:
|
||||
def add_args(self, parser: argparse.ArgumentParser) -> None:
|
||||
group = parser.add_argument_group('Query arguments')
|
||||
group.add_argument('--id', metavar='OSMID',
|
||||
action='append', required=True, dest='ids',
|
||||
action='append', dest='ids',
|
||||
help='OSM id to lookup in format <NRW><id> (may be repeated)')
|
||||
|
||||
_add_api_output_arguments(parser)
|
||||
_add_list_format(parser)
|
||||
|
||||
|
||||
def run(self, args: NominatimArgs) -> int:
|
||||
formatter = napi.load_format_dispatcher('v1', args.project_dir)
|
||||
|
||||
if args.list_formats:
|
||||
return _list_formats(formatter, napi.ReverseResults)
|
||||
|
||||
if args.format == 'debug':
|
||||
loglib.set_log_output('text')
|
||||
elif not formatter.supports_format(napi.ReverseResults, args.format):
|
||||
raise UsageError(f"Unsupported format '{args.format}'. "
|
||||
'Use --list-formats to see supported formats.')
|
||||
|
||||
api = napi.NominatimAPI(args.project_dir)
|
||||
|
||||
if args.format == 'debug':
|
||||
print(loglib.get_and_disable())
|
||||
return 0
|
||||
if args.ids is None:
|
||||
raise UsageError("'id' parameter required.")
|
||||
|
||||
places = [napi.OsmID(o[0], int(o[1:])) for o in args.ids]
|
||||
|
||||
api = napi.NominatimAPI(args.project_dir)
|
||||
results = api.lookup(places,
|
||||
address_details=True, # needed for display name
|
||||
geometry_output=_get_geometry_output(args),
|
||||
geometry_simplification=args.polygon_threshold or 0.0,
|
||||
locales=_get_locales(args, api.config.DEFAULT_LANGUAGE))
|
||||
|
||||
output = api_output.format_result(
|
||||
results,
|
||||
args.format,
|
||||
{'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')
|
||||
if args.format == 'debug':
|
||||
print(loglib.get_and_disable())
|
||||
return 0
|
||||
|
||||
_print_output(formatter, results, args.format,
|
||||
{'extratags': args.extratags,
|
||||
'namedetails': args.namedetails,
|
||||
'addressdetails': args.addressdetails})
|
||||
return 0
|
||||
|
||||
|
||||
@@ -323,20 +353,21 @@ class APIDetails:
|
||||
|
||||
def add_args(self, parser: argparse.ArgumentParser) -> None:
|
||||
group = parser.add_argument_group('Query arguments')
|
||||
objs = group.add_mutually_exclusive_group(required=True)
|
||||
objs.add_argument('--node', '-n', type=int,
|
||||
help="Look up the OSM node with the given ID.")
|
||||
objs.add_argument('--way', '-w', type=int,
|
||||
help="Look up the OSM way with the given ID.")
|
||||
objs.add_argument('--relation', '-r', type=int,
|
||||
help="Look up the OSM relation with the given ID.")
|
||||
objs.add_argument('--place_id', '-p', type=int,
|
||||
help='Database internal identifier of the OSM object to look up')
|
||||
group.add_argument('--node', '-n', type=int,
|
||||
help="Look up the OSM node with the given ID.")
|
||||
group.add_argument('--way', '-w', type=int,
|
||||
help="Look up the OSM way with the given ID.")
|
||||
group.add_argument('--relation', '-r', type=int,
|
||||
help="Look up the OSM relation with the given ID.")
|
||||
group.add_argument('--place_id', '-p', type=int,
|
||||
help='Database internal identifier of the OSM object to look up')
|
||||
group.add_argument('--class', dest='object_class',
|
||||
help=("Class type to disambiguated multiple entries "
|
||||
"of the same object."))
|
||||
|
||||
group = parser.add_argument_group('Output arguments')
|
||||
group.add_argument('--format', type=str, default='json',
|
||||
help='Format of result (use --list-formats to see supported formats)')
|
||||
group.add_argument('--addressdetails', action='store_true',
|
||||
help='Include a breakdown of the address into elements')
|
||||
group.add_argument('--keywords', action='store_true',
|
||||
@@ -351,9 +382,21 @@ class APIDetails:
|
||||
help='Include geometry of result')
|
||||
group.add_argument('--lang', '--accept-language', metavar='LANGS',
|
||||
help='Preferred language order for presenting search results')
|
||||
_add_list_format(parser)
|
||||
|
||||
|
||||
def run(self, args: NominatimArgs) -> int:
|
||||
formatter = napi.load_format_dispatcher('v1', args.project_dir)
|
||||
|
||||
if args.list_formats:
|
||||
return _list_formats(formatter, napi.DetailedResult)
|
||||
|
||||
if args.format == 'debug':
|
||||
loglib.set_log_output('text')
|
||||
elif not formatter.supports_format(napi.DetailedResult, args.format):
|
||||
raise UsageError(f"Unsupported format '{args.format}'. "
|
||||
'Use --list-formats to see supported formats.')
|
||||
|
||||
place: napi.PlaceRef
|
||||
if args.node:
|
||||
place = napi.OsmID('N', args.node, args.object_class)
|
||||
@@ -361,12 +404,13 @@ class APIDetails:
|
||||
place = napi.OsmID('W', args.way, args.object_class)
|
||||
elif args.relation:
|
||||
place = napi.OsmID('R', args.relation, args.object_class)
|
||||
else:
|
||||
assert args.place_id is not None
|
||||
elif args.place_id is not None:
|
||||
place = napi.PlaceID(args.place_id)
|
||||
else:
|
||||
raise UsageError('One of the arguments --node/-n --way/-w '
|
||||
'--relation/-r --place_id/-p is required/')
|
||||
|
||||
api = napi.NominatimAPI(args.project_dir)
|
||||
|
||||
locales = _get_locales(args, api.config.DEFAULT_LANGUAGE)
|
||||
result = api.details(place,
|
||||
address_details=args.addressdetails,
|
||||
@@ -378,17 +422,14 @@ class APIDetails:
|
||||
else napi.GeometryFormat.NONE,
|
||||
locales=locales)
|
||||
|
||||
if args.format == 'debug':
|
||||
print(loglib.get_and_disable())
|
||||
return 0
|
||||
|
||||
if result:
|
||||
output = api_output.format_result(
|
||||
result,
|
||||
'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, ensure_ascii=False)
|
||||
sys.stdout.write('\n')
|
||||
|
||||
_print_output(formatter, result, args.format or 'json',
|
||||
{'locales': locales,
|
||||
'group_hierarchy': args.group_hierarchy})
|
||||
return 0
|
||||
|
||||
LOG.error("Object not found in database.")
|
||||
@@ -406,13 +447,30 @@ class APIStatus:
|
||||
"""
|
||||
|
||||
def add_args(self, parser: argparse.ArgumentParser) -> None:
|
||||
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')
|
||||
group.add_argument('--format', type=str, default='text',
|
||||
help='Format of result (use --list-formats to see supported formats)')
|
||||
_add_list_format(parser)
|
||||
|
||||
|
||||
def run(self, args: NominatimArgs) -> int:
|
||||
formatter = napi.load_format_dispatcher('v1', args.project_dir)
|
||||
|
||||
if args.list_formats:
|
||||
return _list_formats(formatter, napi.StatusResult)
|
||||
|
||||
if args.format == 'debug':
|
||||
loglib.set_log_output('text')
|
||||
elif not formatter.supports_format(napi.StatusResult, args.format):
|
||||
raise UsageError(f"Unsupported format '{args.format}'. "
|
||||
'Use --list-formats to see supported formats.')
|
||||
|
||||
status = napi.NominatimAPI(args.project_dir).status()
|
||||
print(api_output.format_result(status, args.format, {}))
|
||||
|
||||
if args.format == 'debug':
|
||||
print(loglib.get_and_disable())
|
||||
return 0
|
||||
|
||||
_print_output(formatter, status, args.format, {})
|
||||
|
||||
return 0
|
||||
|
||||
@@ -137,6 +137,7 @@ class NominatimArgs:
|
||||
|
||||
# Arguments to all query functions
|
||||
format: str
|
||||
list_formats: bool
|
||||
addressdetails: bool
|
||||
extratags: bool
|
||||
namedetails: bool
|
||||
|
||||
@@ -15,7 +15,7 @@ import json
|
||||
|
||||
import pytest
|
||||
|
||||
import nominatim_api.v1 as api_impl
|
||||
from nominatim_api.v1.format import dispatch as v1_format
|
||||
import nominatim_api as napi
|
||||
|
||||
STATUS_FORMATS = {'text', 'json'}
|
||||
@@ -23,30 +23,30 @@ STATUS_FORMATS = {'text', 'json'}
|
||||
# StatusResult
|
||||
|
||||
def test_status_format_list():
|
||||
assert set(api_impl.list_formats(napi.StatusResult)) == STATUS_FORMATS
|
||||
assert set(v1_format.list_formats(napi.StatusResult)) == STATUS_FORMATS
|
||||
|
||||
|
||||
@pytest.mark.parametrize('fmt', list(STATUS_FORMATS))
|
||||
def test_status_supported(fmt):
|
||||
assert api_impl.supports_format(napi.StatusResult, fmt)
|
||||
assert v1_format.supports_format(napi.StatusResult, fmt)
|
||||
|
||||
|
||||
def test_status_unsupported():
|
||||
assert not api_impl.supports_format(napi.StatusResult, 'gagaga')
|
||||
assert not v1_format.supports_format(napi.StatusResult, 'gagaga')
|
||||
|
||||
|
||||
def test_status_format_text():
|
||||
assert api_impl.format_result(napi.StatusResult(0, 'message here'), 'text', {}) == 'OK'
|
||||
assert v1_format.format_result(napi.StatusResult(0, 'message here'), 'text', {}) == 'OK'
|
||||
|
||||
|
||||
def test_status_format_text():
|
||||
assert api_impl.format_result(napi.StatusResult(500, 'message here'), 'text', {}) == 'ERROR: message here'
|
||||
assert v1_format.format_result(napi.StatusResult(500, 'message here'), 'text', {}) == 'ERROR: message here'
|
||||
|
||||
|
||||
def test_status_format_json_minimal():
|
||||
status = napi.StatusResult(700, 'Bad format.')
|
||||
|
||||
result = api_impl.format_result(status, 'json', {})
|
||||
result = v1_format.format_result(status, 'json', {})
|
||||
|
||||
assert result == \
|
||||
f'{{"status":700,"message":"Bad format.","software_version":"{napi.__version__}"}}'
|
||||
@@ -57,7 +57,7 @@ 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 = v1_format.format_result(status, 'json', {})
|
||||
|
||||
assert result == \
|
||||
f'{{"status":0,"message":"OK","data_updated":"2010-02-07T20:20:03+00:00","software_version":"{napi.__version__}","database_version":"5.6"}}'
|
||||
@@ -70,7 +70,7 @@ def test_search_details_minimal():
|
||||
('place', 'thing'),
|
||||
napi.Point(1.0, 2.0))
|
||||
|
||||
result = api_impl.format_result(search, 'json', {})
|
||||
result = v1_format.format_result(search, 'json', {})
|
||||
|
||||
assert json.loads(result) == \
|
||||
{'category': 'place',
|
||||
@@ -114,7 +114,7 @@ def test_search_details_full():
|
||||
)
|
||||
search.localize(napi.Locales())
|
||||
|
||||
result = api_impl.format_result(search, 'json', {})
|
||||
result = v1_format.format_result(search, 'json', {})
|
||||
|
||||
assert json.loads(result) == \
|
||||
{'place_id': 37563,
|
||||
@@ -153,7 +153,7 @@ def test_search_details_no_geometry(gtype, isarea):
|
||||
napi.Point(1.0, 2.0),
|
||||
geometry={'type': gtype})
|
||||
|
||||
result = api_impl.format_result(search, 'json', {})
|
||||
result = v1_format.format_result(search, 'json', {})
|
||||
js = json.loads(result)
|
||||
|
||||
assert js['geometry'] == {'type': 'Point', 'coordinates': [1.0, 2.0]}
|
||||
@@ -166,7 +166,7 @@ def test_search_details_with_geometry():
|
||||
napi.Point(1.0, 2.0),
|
||||
geometry={'geojson': '{"type":"Point","coordinates":[56.947,-87.44]}'})
|
||||
|
||||
result = api_impl.format_result(search, 'json', {})
|
||||
result = v1_format.format_result(search, 'json', {})
|
||||
js = json.loads(result)
|
||||
|
||||
assert js['geometry'] == {'type': 'Point', 'coordinates': [56.947, -87.44]}
|
||||
@@ -178,7 +178,7 @@ def test_search_details_with_icon_available():
|
||||
('amenity', 'restaurant'),
|
||||
napi.Point(1.0, 2.0))
|
||||
|
||||
result = api_impl.format_result(search, 'json', {'icon_base_url': 'foo'})
|
||||
result = v1_format.format_result(search, 'json', {'icon_base_url': 'foo'})
|
||||
js = json.loads(result)
|
||||
|
||||
assert js['icon'] == 'foo/food_restaurant.p.20.png'
|
||||
@@ -189,7 +189,7 @@ def test_search_details_with_icon_not_available():
|
||||
('amenity', 'tree'),
|
||||
napi.Point(1.0, 2.0))
|
||||
|
||||
result = api_impl.format_result(search, 'json', {'icon_base_url': 'foo'})
|
||||
result = v1_format.format_result(search, 'json', {'icon_base_url': 'foo'})
|
||||
js = json.loads(result)
|
||||
|
||||
assert 'icon' not in js
|
||||
@@ -212,7 +212,7 @@ def test_search_details_with_address_minimal():
|
||||
distance=0.0)
|
||||
])
|
||||
|
||||
result = api_impl.format_result(search, 'json', {})
|
||||
result = v1_format.format_result(search, 'json', {})
|
||||
js = json.loads(result)
|
||||
|
||||
assert js['address'] == [{'localname': '',
|
||||
@@ -245,7 +245,7 @@ def test_search_details_with_further_infos(field, outfield):
|
||||
distance=0.034)
|
||||
])
|
||||
|
||||
result = api_impl.format_result(search, 'json', {})
|
||||
result = v1_format.format_result(search, 'json', {})
|
||||
js = json.loads(result)
|
||||
|
||||
assert js[outfield] == [{'localname': 'Trespass',
|
||||
@@ -279,7 +279,7 @@ def test_search_details_grouped_hierarchy():
|
||||
distance=0.034)
|
||||
])
|
||||
|
||||
result = api_impl.format_result(search, 'json', {'group_hierarchy': True})
|
||||
result = v1_format.format_result(search, 'json', {'group_hierarchy': True})
|
||||
js = json.loads(result)
|
||||
|
||||
assert js['hierarchy'] == {'note': [{'localname': 'Trespass',
|
||||
@@ -303,7 +303,7 @@ def test_search_details_keywords_name():
|
||||
napi.WordInfo(23, 'foo', 'mefoo'),
|
||||
napi.WordInfo(24, 'foo', 'bafoo')])
|
||||
|
||||
result = api_impl.format_result(search, 'json', {'keywords': True})
|
||||
result = v1_format.format_result(search, 'json', {'keywords': True})
|
||||
js = json.loads(result)
|
||||
|
||||
assert js['keywords'] == {'name': [{'id': 23, 'token': 'foo'},
|
||||
@@ -319,7 +319,7 @@ def test_search_details_keywords_address():
|
||||
napi.WordInfo(23, 'foo', 'mefoo'),
|
||||
napi.WordInfo(24, 'foo', 'bafoo')])
|
||||
|
||||
result = api_impl.format_result(search, 'json', {'keywords': True})
|
||||
result = v1_format.format_result(search, 'json', {'keywords': True})
|
||||
js = json.loads(result)
|
||||
|
||||
assert js['keywords'] == {'address': [{'id': 23, 'token': 'foo'},
|
||||
|
||||
@@ -15,7 +15,7 @@ import xml.etree.ElementTree as ET
|
||||
|
||||
import pytest
|
||||
|
||||
import nominatim_api.v1 as api_impl
|
||||
from nominatim_api.v1.format import dispatch as v1_format
|
||||
import nominatim_api as napi
|
||||
|
||||
FORMATS = ['json', 'jsonv2', 'geojson', 'geocodejson', 'xml']
|
||||
@@ -26,7 +26,7 @@ def test_format_reverse_minimal(fmt):
|
||||
('amenity', 'post_box'),
|
||||
napi.Point(0.3, -8.9))
|
||||
|
||||
raw = api_impl.format_result(napi.ReverseResults([reverse]), fmt, {})
|
||||
raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt, {})
|
||||
|
||||
if fmt == 'xml':
|
||||
root = ET.fromstring(raw)
|
||||
@@ -38,7 +38,7 @@ def test_format_reverse_minimal(fmt):
|
||||
|
||||
@pytest.mark.parametrize('fmt', FORMATS)
|
||||
def test_format_reverse_no_result(fmt):
|
||||
raw = api_impl.format_result(napi.ReverseResults(), fmt, {})
|
||||
raw = v1_format.format_result(napi.ReverseResults(), fmt, {})
|
||||
|
||||
if fmt == 'xml':
|
||||
root = ET.fromstring(raw)
|
||||
@@ -55,7 +55,7 @@ def test_format_reverse_with_osm_id(fmt):
|
||||
place_id=5564,
|
||||
osm_object=('N', 23))
|
||||
|
||||
raw = api_impl.format_result(napi.ReverseResults([reverse]), fmt, {})
|
||||
raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt, {})
|
||||
|
||||
if fmt == 'xml':
|
||||
root = ET.fromstring(raw).find('result')
|
||||
@@ -103,7 +103,7 @@ def test_format_reverse_with_address(fmt):
|
||||
]))
|
||||
reverse.localize(napi.Locales())
|
||||
|
||||
raw = api_impl.format_result(napi.ReverseResults([reverse]), fmt,
|
||||
raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
|
||||
{'addressdetails': True})
|
||||
|
||||
|
||||
@@ -167,7 +167,7 @@ def test_format_reverse_geocodejson_special_parts():
|
||||
|
||||
reverse.localize(napi.Locales())
|
||||
|
||||
raw = api_impl.format_result(napi.ReverseResults([reverse]), 'geocodejson',
|
||||
raw = v1_format.format_result(napi.ReverseResults([reverse]), 'geocodejson',
|
||||
{'addressdetails': True})
|
||||
|
||||
props = json.loads(raw)['features'][0]['properties']['geocoding']
|
||||
@@ -183,7 +183,7 @@ def test_format_reverse_with_address_none(fmt):
|
||||
napi.Point(1.0, 2.0),
|
||||
address_rows=napi.AddressLines())
|
||||
|
||||
raw = api_impl.format_result(napi.ReverseResults([reverse]), fmt,
|
||||
raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
|
||||
{'addressdetails': True})
|
||||
|
||||
|
||||
@@ -213,7 +213,7 @@ def test_format_reverse_with_extratags(fmt):
|
||||
napi.Point(1.0, 2.0),
|
||||
extratags={'one': 'A', 'two':'B'})
|
||||
|
||||
raw = api_impl.format_result(napi.ReverseResults([reverse]), fmt,
|
||||
raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
|
||||
{'extratags': True})
|
||||
|
||||
if fmt == 'xml':
|
||||
@@ -235,7 +235,7 @@ def test_format_reverse_with_extratags_none(fmt):
|
||||
('place', 'thing'),
|
||||
napi.Point(1.0, 2.0))
|
||||
|
||||
raw = api_impl.format_result(napi.ReverseResults([reverse]), fmt,
|
||||
raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
|
||||
{'extratags': True})
|
||||
|
||||
if fmt == 'xml':
|
||||
@@ -258,7 +258,7 @@ def test_format_reverse_with_namedetails_with_name(fmt):
|
||||
napi.Point(1.0, 2.0),
|
||||
names={'name': 'A', 'ref':'1'})
|
||||
|
||||
raw = api_impl.format_result(napi.ReverseResults([reverse]), fmt,
|
||||
raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
|
||||
{'namedetails': True})
|
||||
|
||||
if fmt == 'xml':
|
||||
@@ -280,7 +280,7 @@ def test_format_reverse_with_namedetails_without_name(fmt):
|
||||
('place', 'thing'),
|
||||
napi.Point(1.0, 2.0))
|
||||
|
||||
raw = api_impl.format_result(napi.ReverseResults([reverse]), fmt,
|
||||
raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
|
||||
{'namedetails': True})
|
||||
|
||||
if fmt == 'xml':
|
||||
@@ -302,7 +302,7 @@ def test_search_details_with_icon_available(fmt):
|
||||
('amenity', 'restaurant'),
|
||||
napi.Point(1.0, 2.0))
|
||||
|
||||
result = api_impl.format_result(napi.ReverseResults([reverse]), fmt,
|
||||
result = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
|
||||
{'icon_base_url': 'foo'})
|
||||
|
||||
js = json.loads(result)
|
||||
@@ -316,7 +316,7 @@ def test_search_details_with_icon_not_available(fmt):
|
||||
('amenity', 'tree'),
|
||||
napi.Point(1.0, 2.0))
|
||||
|
||||
result = api_impl.format_result(napi.ReverseResults([reverse]), fmt,
|
||||
result = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
|
||||
{'icon_base_url': 'foo'})
|
||||
|
||||
assert 'icon' not in json.loads(result)
|
||||
|
||||
@@ -13,6 +13,15 @@ import pytest
|
||||
import nominatim_db.clicmd.api
|
||||
import nominatim_api as napi
|
||||
|
||||
@pytest.mark.parametrize('call', ['search', 'reverse', 'lookup', 'details', 'status'])
|
||||
def test_list_format(cli_call, call):
|
||||
assert 0 == cli_call(call, '--list-formats')
|
||||
|
||||
|
||||
@pytest.mark.parametrize('call', ['search', 'reverse', 'lookup', 'details', 'status'])
|
||||
def test_bad_format(cli_call, call):
|
||||
assert 1 == cli_call(call, '--format', 'rsdfsdfsdfsaefsdfsd')
|
||||
|
||||
|
||||
class TestCliStatusCall:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user