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 from .server.content_types import CONTENT_JSON
18 FormatFunc = Callable[[T, Mapping[str, Any]], str]
19 ErrorFormatFunc = Callable[[str, str, int], str]
22 class FormatDispatcher:
23 """ Container for formatting functions for results.
24 Functions can conveniently be added by using decorated functions.
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] = {}
31 self.content_types.update(content_types)
32 self.format_functions: Dict[Type[Any], Dict[str, FormatFunc[Any]]] = defaultdict(dict)
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
39 def decorator(func: FormatFunc[T]) -> FormatFunc[T]:
40 self.format_functions[result_class][fmt] = func
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.
50 self.error_handler = func
53 def list_formats(self, result_type: Type[Any]) -> List[str]:
54 """ Return a list of formats supported by this formatter.
56 return list(self.format_functions[result_type].keys())
58 def supports_format(self, result_type: Type[Any], fmt: str) -> bool:
59 """ Check if the given format is supported by this formatter.
61 return fmt in self.format_functions[result_type]
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.
66 The format is expected to be in the list returned by
69 return self.format_functions[type(result)][fmt](result, options)
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.
75 Change the format using the error_format_func decorator.
77 return self.error_handler(content_type, msg, status)
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.
84 self.content_types[fmt] = content_type
86 def get_content_type(self, fmt: str) -> str:
87 """ Return the content type for the given format.
89 If no explicit content type has been defined, then
90 JSON format is assumed.
92 return self.content_types.get(fmt, CONTENT_JSON)
95 def load_format_dispatcher(api_name: str, project_dir: Optional[Path]) -> FormatDispatcher:
96 """ Load the dispatcher for the given API.
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
102 If the function does not exist, the default formatter is loaded.
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',
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)
116 return cast(FormatDispatcher, module.dispatch)
118 return cast(FormatDispatcher,
119 importlib.import_module(f'nominatim_api.{api_name}.format').dispatch)