1 # SPDX-License-Identifier: GPL-3.0-or-later
3 # This file is part of Nominatim. (https://nominatim.org)
5 # Copyright (C) 2024 by the Nominatim developer community.
6 # For a full list of authors see the git log.
8 Helper classes and functions for formatting results into API responses.
10 from typing import Type, TypeVar, Dict, List, Callable, Any, Mapping, Optional, cast
11 from collections import defaultdict
12 from pathlib import Path
15 T = TypeVar('T') # pylint: disable=invalid-name
16 FormatFunc = Callable[[T, Mapping[str, Any]], str]
19 class FormatDispatcher:
20 """ Helper class to conveniently create formatting functions in
21 a module using decorators.
24 def __init__(self) -> None:
25 self.format_functions: Dict[Type[Any], Dict[str, FormatFunc[Any]]] = defaultdict(dict)
28 def format_func(self, result_class: Type[T],
29 fmt: str) -> Callable[[FormatFunc[T]], FormatFunc[T]]:
30 """ Decorator for a function that formats a given type of result into the
33 def decorator(func: FormatFunc[T]) -> FormatFunc[T]:
34 self.format_functions[result_class][fmt] = func
40 def list_formats(self, result_type: Type[Any]) -> List[str]:
41 """ Return a list of formats supported by this formatter.
43 return list(self.format_functions[result_type].keys())
46 def supports_format(self, result_type: Type[Any], fmt: str) -> bool:
47 """ Check if the given format is supported by this formatter.
49 return fmt in self.format_functions[result_type]
52 def format_result(self, result: Any, fmt: str, options: Mapping[str, Any]) -> str:
53 """ Convert the given result into a string using the given format.
55 The format is expected to be in the list returned by
58 return self.format_functions[type(result)][fmt](result, options)
61 def load_format_dispatcher(api_name: str, project_dir: Optional[Path]) -> FormatDispatcher:
62 """ Load the dispatcher for the given API.
64 The function first tries to find a module api/<api_name>/format.py
65 in the project directory. This file must export a single variable
68 If the function does not exist, the default formatter is loaded.
70 if project_dir is not None:
71 priv_module = project_dir / 'api' / api_name / 'format.py'
72 if priv_module.is_file():
73 spec = importlib.util.spec_from_file_location(f'api.{api_name},format',
76 module = importlib.util.module_from_spec(spec)
77 # Do not add to global modules because there is no standard
78 # module name that Python can resolve.
79 assert spec.loader is not None
80 spec.loader.exec_module(module)
82 return cast(FormatDispatcher, module.dispatch)
84 return cast(FormatDispatcher,
85 importlib.import_module(f'nominatim_api.{api_name}.format').dispatch)