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