]> git.openstreetmap.org Git - nominatim.git/blob - src/nominatim_api/result_formatting.py
572cd3cd3df7ec37c4a9ecba22b5cd9f5e4317fd
[nominatim.git] / src / nominatim_api / result_formatting.py
1 # SPDX-License-Identifier: GPL-3.0-or-later
2 #
3 # This file is part of Nominatim. (https://nominatim.org)
4 #
5 # Copyright (C) 2024 by the Nominatim developer community.
6 # For a full list of authors see the git log.
7 """
8 Helper classes and functions for formatting results into API responses.
9 """
10 from typing import Type, TypeVar, Dict, List, Callable, Any, Mapping, Optional, cast
11 from collections import defaultdict
12 from pathlib import Path
13 import importlib
14
15 T = TypeVar('T') # pylint: disable=invalid-name
16 FormatFunc = Callable[[T, Mapping[str, Any]], str]
17
18
19 class FormatDispatcher:
20     """ Helper class to conveniently create formatting functions in
21         a module using decorators.
22     """
23
24     def __init__(self) -> None:
25         self.format_functions: Dict[Type[Any], Dict[str, FormatFunc[Any]]] = defaultdict(dict)
26
27
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
31             selected format.
32         """
33         def decorator(func: FormatFunc[T]) -> FormatFunc[T]:
34             self.format_functions[result_class][fmt] = func
35             return func
36
37         return decorator
38
39
40     def list_formats(self, result_type: Type[Any]) -> List[str]:
41         """ Return a list of formats supported by this formatter.
42         """
43         return list(self.format_functions[result_type].keys())
44
45
46     def supports_format(self, result_type: Type[Any], fmt: str) -> bool:
47         """ Check if the given format is supported by this formatter.
48         """
49         return fmt in self.format_functions[result_type]
50
51
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.
54
55             The format is expected to be in the list returned by
56             `list_formats()`.
57         """
58         return self.format_functions[type(result)][fmt](result, options)
59
60
61 def load_format_dispatcher(api_name: str, project_dir: Optional[Path]) -> FormatDispatcher:
62     """ Load the dispatcher for the given API.
63
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
66         `dispatcher`.
67
68         If the function does not exist, the default formatter is loaded.
69     """
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',
74                                                           str(priv_module))
75             if spec:
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)
81
82                 return cast(FormatDispatcher, module.dispatch)
83
84     return cast(FormatDispatcher,
85                 importlib.import_module(f'nominatim_api.{api_name}.format').dispatch)