]> git.openstreetmap.org Git - nominatim.git/blob - src/nominatim_api/result_formatting.py
8eb500dbbd157ee42f95961ce23c04f3634f6049
[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 from .server.content_types import CONTENT_JSON
16
17 T = TypeVar('T') # pylint: disable=invalid-name
18 FormatFunc = Callable[[T, Mapping[str, Any]], str]
19 ErrorFormatFunc = Callable[[str, str, int], str]
20
21
22 class FormatDispatcher:
23     """ Helper class to conveniently create formatting functions in
24         a module using decorators.
25     """
26
27     def __init__(self, content_types: Optional[Mapping[str, str]] = None) -> None:
28         self.error_handler: ErrorFormatFunc = lambda ct, msg, status: f"ERROR {status}: {msg}"
29         self.content_types: Dict[str, str] = {}
30         if content_types:
31             self.content_types.update(content_types)
32         self.format_functions: Dict[Type[Any], Dict[str, FormatFunc[Any]]] = defaultdict(dict)
33
34
35     def format_func(self, result_class: Type[T],
36                     fmt: str) -> Callable[[FormatFunc[T]], FormatFunc[T]]:
37         """ Decorator for a function that formats a given type of result into the
38             selected format.
39         """
40         def decorator(func: FormatFunc[T]) -> FormatFunc[T]:
41             self.format_functions[result_class][fmt] = func
42             return func
43
44         return decorator
45
46
47     def error_format_func(self, func: ErrorFormatFunc) -> ErrorFormatFunc:
48         """ Decorator for a function that formats error messges.
49             There is only one error formatter per dispatcher. Using
50             the decorator repeatedly will overwrite previous functions.
51         """
52         self.error_handler = func
53         return func
54
55
56     def list_formats(self, result_type: Type[Any]) -> List[str]:
57         """ Return a list of formats supported by this formatter.
58         """
59         return list(self.format_functions[result_type].keys())
60
61
62     def supports_format(self, result_type: Type[Any], fmt: str) -> bool:
63         """ Check if the given format is supported by this formatter.
64         """
65         return fmt in self.format_functions[result_type]
66
67
68     def format_result(self, result: Any, fmt: str, options: Mapping[str, Any]) -> str:
69         """ Convert the given result into a string using the given format.
70
71             The format is expected to be in the list returned by
72             `list_formats()`.
73         """
74         return self.format_functions[type(result)][fmt](result, options)
75
76
77     def format_error(self, content_type: str, msg: str, status: int) -> str:
78         """ Convert the given error message into a response string
79             taking the requested content_type into account.
80
81             Change the format using the error_format_func decorator.
82         """
83         return self.error_handler(content_type, msg, status)
84
85
86     def set_content_type(self, fmt: str, content_type: str) -> None:
87         """ Set the content type for the given format. This is the string
88             that will be returned in the Content-Type header of the HTML
89             response, when the given format is choosen.
90         """
91         self.content_types[fmt] = content_type
92
93
94     def get_content_type(self, fmt: str) -> str:
95         """ Return the content type for the given format.
96
97             If no explicit content type has been defined, then
98             JSON format is assumed.
99         """
100         return self.content_types.get(fmt, CONTENT_JSON)
101
102
103 def load_format_dispatcher(api_name: str, project_dir: Optional[Path]) -> FormatDispatcher:
104     """ Load the dispatcher for the given API.
105
106         The function first tries to find a module api/<api_name>/format.py
107         in the project directory. This file must export a single variable
108         `dispatcher`.
109
110         If the function does not exist, the default formatter is loaded.
111     """
112     if project_dir is not None:
113         priv_module = project_dir / 'api' / api_name / 'format.py'
114         if priv_module.is_file():
115             spec = importlib.util.spec_from_file_location(f'api.{api_name},format',
116                                                           str(priv_module))
117             if spec:
118                 module = importlib.util.module_from_spec(spec)
119                 # Do not add to global modules because there is no standard
120                 # module name that Python can resolve.
121                 assert spec.loader is not None
122                 spec.loader.exec_module(module)
123
124                 return cast(FormatDispatcher, module.dispatch)
125
126     return cast(FormatDispatcher,
127                 importlib.import_module(f'nominatim_api.{api_name}.format').dispatch)