]>
Commit | Line | Data |
---|---|---|
33c7a0ef TL |
1 | # object_format.py provides types and functions for working with |
2 | # requested output formats such as JSON, YAML, etc. | |
3 | """tools for writing formatting-friendly mgr module functions | |
4 | ||
5 | Currently, the ceph mgr code in python is most commonly written by adding mgr | |
6 | modules and corresponding classes and then adding methods to those classes that | |
7 | are decorated using `@CLICommand` from `mgr_module.py`. These methods (that | |
8 | will be called endpoints subsequently) then implement the logic that is | |
9 | executed when the mgr receives a command from a client. These endpoints are | |
10 | currently responsible for forming a response tuple of (int, str, str) where the | |
11 | int represents a return value (error code) and the first string the "body" of | |
12 | the response. The mgr supports a generic `format` parameter (`--format` on the | |
13 | ceph cli) that each endpoint must then explicitly handle. At the time of this | |
14 | writing, many endpoints do not handle alternate formats and are each | |
15 | implementing formatting/serialization of values in various different ways. | |
16 | ||
17 | The `object_format` module aims to make the process of writing endpoint | |
18 | functions easier, more consistent, and (hopefully) better documented. At the | |
19 | highest level, the module provides a new decorator `Responder` that must be | |
20 | placed below the `CLICommand` decorator (so that it decorates the endpoint | |
21 | before `CLICommand`). This decorator helps automatically convert Python objects | |
22 | to response tuples expected by the manager, while handling the `format` | |
23 | parameter automatically. | |
24 | ||
25 | In addition to the decorator the module provides a few other types and methods | |
26 | that intended to interoperate with the decorator and make small customizations | |
27 | and error handling easier. | |
28 | ||
29 | == Using Responder == | |
30 | ||
31 | The simple and intended way to use the decorator is as follows: | |
32 | @CLICommand("command name", perm="r") | |
33 | Responder() | |
34 | def create_something(self, name: str) -> Dict[str, str]: | |
35 | ... # implementation | |
36 | return {"name": name, "id": new_id} | |
37 | ||
38 | In this case the `create_something` method return a python dict, | |
39 | and does not return a response tuple directly. Instead, the | |
40 | dict is converted to either JSON or YAML depending on what the | |
41 | client requested. Assuming no exception is raised by the | |
42 | implementation then the response code is always zero (success). | |
43 | ||
44 | The object_format module provides an exception type `ErrorResponse` | |
45 | that assists in returning "clean" error conditions to the client. | |
46 | Extending the previous example to use this exception: | |
47 | @CLICommand("command name", perm="r") | |
48 | Responder() | |
49 | def create_something(self, name: str) -> Dict[str, str]: | |
50 | try: | |
51 | ... # implementation | |
52 | return {"name": name, "id": new_id} | |
53 | except KeyError as kerr: | |
54 | # explicitly set the return value to ENOENT for KeyError | |
55 | raise ErrorResponse.wrap(kerr, return_value=-errno.ENOENT) | |
56 | except (BusinessLogcError, OSError) as err: | |
57 | # return value is based on err when possible | |
58 | raise ErrorResponse.wrap(err) | |
59 | ||
60 | Most uses of ErrorResponse are expected to use the `wrap` classmethod, | |
61 | as it will aid in the handling of an existing exception but `ErrorResponse` | |
62 | can be used directly too. | |
63 | ||
64 | == Customizing Response Formatting == | |
65 | ||
66 | The `Responder` is built using two additional mid-layer types. The | |
67 | `ObjectFormatAdapter` and the `ReturnValueAdapter` by default. These types | |
68 | implement the `CommonFormatter` protocol and `ReturnValueProvider` protocols | |
69 | respectively. Most cases will not need to customize the `ReturnValueAdapter` as | |
70 | returning zero on success is expected. However, if there's a need to return a | |
71 | non-zero error code outside of an exception, you can add the `mgr_return_value` | |
72 | function to the returned type of the endpoint function - causing it to meet the | |
73 | `ReturnValueProvider` protocol. Whatever integer that function returns will | |
74 | then be used in the response tuple. | |
75 | ||
76 | The `ObjectFormatAdapter` can operate in two modes. By default, any type | |
77 | returned from the endpoint function will be checked for a `to_simplified` | |
78 | method (the type matches the SimpleDataProvider` protocol) and if it exists | |
79 | the method will be called and the result serialized. Example: | |
80 | class CoolStuff: | |
81 | def __init__(self, temperature: int, quantity: int) -> None: | |
82 | self.temperature = temperature | |
83 | self.quantity = quantity | |
84 | def to_simplified(self) -> Dict[str, int]: | |
85 | return {"temp": self.temperature, "qty": self.quantity} | |
86 | ||
87 | @CLICommand("command name", perm="r") | |
88 | Responder() | |
89 | def create_something_cool(self) -> CoolStuff: | |
90 | cool_stuff: CoolStuff = self._make_cool_stuff() # implementation | |
91 | return cool_stuff | |
92 | ||
93 | In order to serialize the result, the object returned from the wrapped | |
94 | function must provide the `to_simplified` method (or the compatibility methods, | |
95 | see below) or already be a "simplified type". Valid types include lists and | |
96 | dicts that contain other lists and dicts and ints, strs, bools -- basic objects | |
97 | that can be directly converted to json (via json.dumps) without any additional | |
98 | conversions. The `to_simplified` method must always return such types. | |
99 | ||
100 | To be compatible with many existing types in the ceph mgr codebase one can pass | |
101 | `compatible=True` to the `ObjectFormatAdapter`. If the type provides a | |
102 | `to_json` and/or `to_yaml` method that returns basic python types (dict, list, | |
103 | str, etc...) but *not* already serialized JSON or YAML this flag can be | |
104 | enabled. Note that Responder takes as an argument any callable that returns a | |
105 | `CommonFormatter`. In this example below we enable the flag using | |
106 | `functools.partial`: | |
107 | class MyExistingClass: | |
108 | def to_json(self) -> Dict[str, Any]: | |
109 | return {"name": self.name, "height": self.height} | |
110 | ||
111 | @CLICommand("command name", perm="r") | |
112 | Responder(functools.partial(ObjectFormatAdapter, compatible=True)) | |
113 | def create_an_item(self) -> MyExistingClass: | |
114 | item: MyExistingClass = self._new_item() # implementation | |
115 | return item | |
116 | ||
117 | ||
118 | For cases that need to return xml or plain text formatted responses one can | |
119 | create a new class that matches the `CommonFormatter` protocol (provides a | |
120 | valid_formats method) and one or more `format_x` method where x is the name of | |
121 | a format ("json", "yaml", "xml", "plain", etc...). | |
122 | class MyCustomFormatAdapter: | |
123 | def __init__(self, obj_to_format: Any) -> None: | |
124 | ... | |
125 | def valid_formats(self) -> Iterable[str]: | |
126 | ... | |
127 | def format_json(self) -> str: | |
128 | ... | |
129 | def format_xml(self) -> str: | |
130 | ... | |
131 | ||
132 | ||
133 | Of course, the Responder itself can be used as a base class and aspects of the | |
134 | Responder altered for specific use cases. Inheriting from `Responder` and | |
135 | customizing it is an exercise left for those brave enough to read the code in | |
136 | `object_format.py` :-). | |
137 | """ | |
138 | ||
139 | import enum | |
140 | import errno | |
141 | import json | |
142 | import sys | |
143 | ||
144 | from functools import wraps | |
145 | from typing import ( | |
146 | Any, | |
147 | Callable, | |
148 | Dict, | |
149 | Iterable, | |
150 | List, | |
151 | Optional, | |
152 | TYPE_CHECKING, | |
153 | Tuple, | |
154 | Type, | |
155 | TypeVar, | |
156 | Union, | |
157 | ) | |
158 | ||
159 | import yaml | |
160 | ||
161 | # this uses a version check as opposed to a try/except because this | |
162 | # form makes mypy happy and try/except doesn't. | |
163 | if sys.version_info >= (3, 8): | |
164 | from typing import Protocol | |
165 | elif TYPE_CHECKING: | |
166 | # typing_extensions will not be available for the real mgr server | |
167 | from typing_extensions import Protocol | |
168 | else: | |
169 | # fallback type that is acceptable to older python on prod. builds | |
170 | class Protocol: # type: ignore | |
171 | pass | |
172 | ||
173 | from mgr_module import HandlerFuncType | |
174 | ||
175 | ||
176 | DEFAULT_JSON_INDENT: int = 2 | |
177 | ||
178 | ||
179 | class Format(str, enum.Enum): | |
180 | plain = "plain" | |
181 | json = "json" | |
182 | json_pretty = "json-pretty" | |
183 | yaml = "yaml" | |
184 | xml_pretty = "xml-pretty" | |
185 | xml = "xml" | |
186 | ||
187 | ||
188 | # SimpleData is a type alias for Any unless we can determine the | |
189 | # exact set of subtypes we want to support. But it is explicit! | |
190 | SimpleData = Any | |
191 | ||
192 | ||
33c7a0ef TL |
193 | class SimpleDataProvider(Protocol): |
194 | def to_simplified(self) -> SimpleData: | |
195 | """Return a simplified representation of the current object. | |
196 | The simplified representation should be trivially serializable. | |
197 | """ | |
198 | ... # pragma: no cover | |
199 | ||
200 | ||
201 | class JSONDataProvider(Protocol): | |
202 | def to_json(self) -> Any: | |
203 | """Return a python object that can be serialized into JSON. | |
204 | This function does _not_ return a JSON string. | |
205 | """ | |
206 | ... # pragma: no cover | |
207 | ||
208 | ||
209 | class YAMLDataProvider(Protocol): | |
210 | def to_yaml(self) -> Any: | |
211 | """Return a python object that can be serialized into YAML. | |
212 | This function does _not_ return a string of YAML. | |
213 | """ | |
214 | ... # pragma: no cover | |
215 | ||
216 | ||
217 | class JSONFormatter(Protocol): | |
218 | def format_json(self) -> str: | |
219 | """Return a JSON formatted representation of an object.""" | |
220 | ... # pragma: no cover | |
221 | ||
222 | ||
223 | class YAMLFormatter(Protocol): | |
224 | def format_yaml(self) -> str: | |
225 | """Return a JSON formatted representation of an object.""" | |
226 | ... # pragma: no cover | |
227 | ||
228 | ||
229 | class ReturnValueProvider(Protocol): | |
230 | def mgr_return_value(self) -> int: | |
231 | """Return an integer value to provide the Ceph MGR with a error code | |
232 | for the MGR's response tuple. Zero means success. Return an negative | |
233 | errno otherwise. | |
234 | """ | |
235 | ... # pragma: no cover | |
236 | ||
237 | ||
238 | class CommonFormatter(Protocol): | |
239 | """A protocol that indicates the type is a formatter for multiple | |
240 | possible formats. | |
241 | """ | |
242 | ||
243 | def valid_formats(self) -> Iterable[str]: | |
244 | """Return the names of known valid formats.""" | |
245 | ... # pragma: no cover | |
246 | ||
247 | ||
248 | # The _is_name_of_protocol_type functions below are here because the production | |
249 | # builds of the ceph manager are lower than python 3.8 and do not have | |
250 | # typing_extensions available in the resulting images. This means that | |
251 | # runtime_checkable is not available and isinstance can not be used with a | |
252 | # protocol type. These could be replaced by isinstance in a later version of | |
253 | # python. Note that these functions *can not* be methods of the protocol types | |
254 | # for neatness - including methods on the protocl types makes mypy consider | |
255 | # those methods as part of the protcol & a required method. Using decorators | |
256 | # did not change that - I checked. | |
257 | ||
258 | ||
259 | def _is_simple_data_provider(obj: SimpleDataProvider) -> bool: | |
260 | """Return true if obj is usable as a SimpleDataProvider.""" | |
261 | return callable(getattr(obj, 'to_simplified', None)) | |
262 | ||
263 | ||
264 | def _is_json_data_provider(obj: JSONDataProvider) -> bool: | |
265 | """Return true if obj is usable as a JSONDataProvider.""" | |
266 | return callable(getattr(obj, 'to_json', None)) | |
267 | ||
268 | ||
269 | def _is_yaml_data_provider(obj: YAMLDataProvider) -> bool: | |
270 | """Return true if obj is usable as a YAMLDataProvider.""" | |
271 | return callable(getattr(obj, 'to_yaml', None)) | |
272 | ||
273 | ||
274 | def _is_return_value_provider(obj: ReturnValueProvider) -> bool: | |
275 | """Return true if obj is usable as a YAMLDataProvider.""" | |
276 | return callable(getattr(obj, 'mgr_return_value', None)) | |
277 | ||
278 | ||
279 | class ObjectFormatAdapter: | |
280 | """A format adapater for a single object. | |
281 | Given an input object, this type will adapt the object, or a simplified | |
282 | representation of the object, to either JSON or YAML when the format_json or | |
283 | format_yaml methods are used. | |
284 | ||
285 | If the compatible flag is true and the object provided to the adapter has | |
286 | methods such as `to_json` and/or `to_yaml` these methods will be called in | |
287 | order to get a JSON/YAML compatible simplified representation of the | |
288 | object. | |
289 | ||
290 | If the above case is not satisfied and the object provided to the adapter | |
291 | has a method `to_simplified`, this method will be called to acquire a | |
292 | simplified representation of the object. | |
293 | ||
294 | If none of the above cases is true, the object itself will be used for | |
295 | serialization. If the object can not be safely serialized an exception will | |
296 | be raised. | |
297 | ||
298 | NOTE: Some code may use methods named like `to_json` to return a JSON | |
299 | string. If that is the case, you should not use that method with the | |
300 | ObjectFormatAdapter. Do not set compatible=True for objects of this type. | |
301 | """ | |
302 | ||
303 | def __init__( | |
304 | self, | |
305 | obj: Any, | |
306 | json_indent: Optional[int] = DEFAULT_JSON_INDENT, | |
307 | compatible: bool = False, | |
308 | ) -> None: | |
309 | self.obj = obj | |
310 | self._compatible = compatible | |
311 | self.json_indent = json_indent | |
312 | ||
313 | def _fetch_json_data(self) -> Any: | |
314 | # if the data object provides a specific simplified representation for | |
315 | # JSON (and compatible mode is enabled) get the data via that method | |
316 | if self._compatible and _is_json_data_provider(self.obj): | |
317 | return self.obj.to_json() | |
318 | # otherwise we use our specific method `to_simplified` if it exists | |
319 | if _is_simple_data_provider(self.obj): | |
320 | return self.obj.to_simplified() | |
321 | # and fall back to the "raw" object | |
322 | return self.obj | |
323 | ||
324 | def format_json(self) -> str: | |
325 | """Return a JSON formatted string representing the input object.""" | |
326 | return json.dumps( | |
327 | self._fetch_json_data(), indent=self.json_indent, sort_keys=True | |
328 | ) | |
329 | ||
330 | def _fetch_yaml_data(self) -> Any: | |
331 | if self._compatible and _is_yaml_data_provider(self.obj): | |
332 | return self.obj.to_yaml() | |
333 | # nothing specific to YAML was found. use the simplified representation | |
334 | # for JSON, as all valid JSON is valid YAML. | |
335 | return self._fetch_json_data() | |
336 | ||
337 | def format_yaml(self) -> str: | |
338 | """Return a YAML formatted string representing the input object.""" | |
339 | return yaml.safe_dump(self._fetch_yaml_data()) | |
340 | ||
341 | format_json_pretty = format_json | |
342 | ||
343 | def valid_formats(self) -> Iterable[str]: | |
344 | """Return valid format names.""" | |
345 | return set(str(v) for v in Format.__members__) | |
346 | ||
347 | ||
348 | class ReturnValueAdapter: | |
349 | """A return-value adapter for an object. | |
350 | Given an input object, this type will attempt to get a mgr return value | |
351 | from the object if provides a `mgr_return_value` function. | |
352 | If not it returns a default return value, typically 0. | |
353 | """ | |
354 | ||
355 | def __init__( | |
356 | self, | |
357 | obj: Any, | |
358 | default: int = 0, | |
359 | ) -> None: | |
360 | self.obj = obj | |
361 | self.default_return_value = default | |
362 | ||
363 | def mgr_return_value(self) -> int: | |
364 | if _is_return_value_provider(self.obj): | |
365 | return int(self.obj.mgr_return_value()) | |
366 | return self.default_return_value | |
367 | ||
368 | ||
369 | class ErrorResponseBase(Exception): | |
370 | """An exception that can directly be converted to a mgr reponse.""" | |
371 | ||
372 | def format_response(self) -> Tuple[int, str, str]: | |
373 | raise NotImplementedError() | |
374 | ||
375 | ||
376 | class UnknownFormat(ErrorResponseBase): | |
377 | """Raised if the format name is unexpected. | |
378 | This can help distinguish typos from formats that are known but | |
379 | not implemented. | |
380 | """ | |
381 | ||
382 | def __init__(self, format_name: str) -> None: | |
383 | self.format_name = format_name | |
384 | ||
385 | def format_response(self) -> Tuple[int, str, str]: | |
386 | return -errno.EINVAL, "", f"Unknown format name: {self.format_name}" | |
387 | ||
388 | ||
389 | class UnsupportedFormat(ErrorResponseBase): | |
390 | """Raised if the format name does not correspond to any valid | |
391 | conversion functions. | |
392 | """ | |
393 | ||
394 | def __init__(self, format_name: str) -> None: | |
395 | self.format_name = format_name | |
396 | ||
397 | def format_response(self) -> Tuple[int, str, str]: | |
398 | return -errno.EINVAL, "", f"Unsupported format: {self.format_name}" | |
399 | ||
400 | ||
401 | class ErrorResponse(ErrorResponseBase): | |
402 | """General exception convertible to a mgr response.""" | |
403 | ||
404 | E = TypeVar("E", bound="ErrorResponse") | |
405 | ||
406 | def __init__(self, status: str, return_value: Optional[int] = None) -> None: | |
407 | self.return_value = ( | |
408 | return_value if return_value is not None else -errno.EINVAL | |
409 | ) | |
410 | self.status = status | |
411 | ||
412 | def format_response(self) -> Tuple[int, str, str]: | |
413 | return (self.return_value, "", self.status) | |
414 | ||
415 | def mgr_return_value(self) -> int: | |
416 | return self.return_value | |
417 | ||
418 | @property | |
419 | def errno(self) -> int: | |
420 | rv = self.return_value | |
421 | return -rv if rv < 0 else rv | |
422 | ||
423 | def __repr__(self) -> str: | |
424 | return f"ErrorResponse({self.status!r}, {self.return_value!r})" | |
425 | ||
426 | @classmethod | |
427 | def wrap( | |
428 | cls: Type[E], exc: Exception, return_value: Optional[int] = None | |
39ae355f TL |
429 | ) -> ErrorResponseBase: |
430 | if isinstance(exc, ErrorResponseBase): | |
431 | return exc | |
33c7a0ef TL |
432 | if return_value is None: |
433 | try: | |
39ae355f TL |
434 | return_value = int(getattr(exc, "errno")) |
435 | if return_value > 0: | |
436 | return_value = -return_value | |
33c7a0ef TL |
437 | except (AttributeError, ValueError): |
438 | pass | |
439 | err = cls(str(exc), return_value=return_value) | |
440 | setattr(err, "__cause__", exc) | |
441 | return err | |
442 | ||
443 | ||
39ae355f TL |
444 | ObjectResponseFuncType = Union[ |
445 | Callable[..., Dict[Any, Any]], | |
446 | Callable[..., List[Any]], | |
447 | Callable[..., SimpleDataProvider], | |
448 | Callable[..., JSONDataProvider], | |
449 | Callable[..., YAMLDataProvider], | |
450 | Callable[..., ReturnValueProvider], | |
451 | ] | |
452 | ||
453 | ||
33c7a0ef TL |
454 | def _get_requested_format(f: ObjectResponseFuncType, kw: Dict[str, Any]) -> str: |
455 | # todo: leave 'format' in kw dict iff its part of f's signature | |
456 | return kw.pop("format", None) | |
457 | ||
458 | ||
459 | class Responder: | |
460 | """A decorator type intended to assist in converting Python return types | |
461 | into valid responses for the Ceph MGR. | |
462 | ||
463 | A function that returns a Python object will have the object converted into | |
464 | a return value and formatted response body, based on the `format` argument | |
465 | passed to the mgr. When used from the ceph cli tool the `--format=[name]` | |
466 | argument is mapped to a `format` keyword argument. The decorated function | |
467 | may provide a `format` argument (type str). If the decorated function does | |
468 | not provide a `format` argument itself, the Responder decorator will | |
469 | implicitly add one to the MGR's "CLI arguments" handling stack. | |
470 | ||
471 | The Responder object is callable and is expected to be used as a decorator. | |
472 | """ | |
473 | ||
474 | def __init__( | |
475 | self, formatter: Optional[Callable[..., CommonFormatter]] = None | |
476 | ) -> None: | |
477 | self.formatter = formatter | |
478 | self.default_format = "json" | |
479 | ||
480 | def _formatter(self, obj: Any) -> CommonFormatter: | |
481 | """Return the formatter/format-adapter for the object.""" | |
482 | if self.formatter is not None: | |
483 | return self.formatter(obj) | |
484 | return ObjectFormatAdapter(obj) | |
485 | ||
486 | def _retval_provider(self, obj: Any) -> ReturnValueProvider: | |
487 | """Return a ReturnValueProvider for the given object.""" | |
488 | return ReturnValueAdapter(obj) | |
489 | ||
490 | def _get_format_func( | |
491 | self, obj: Any, format_req: Optional[str] = None | |
492 | ) -> Callable: | |
493 | formatter = self._formatter(obj) | |
494 | if format_req is None: | |
495 | format_req = self.default_format | |
496 | if format_req not in formatter.valid_formats(): | |
497 | raise UnknownFormat(format_req) | |
498 | req = str(format_req).replace("-", "_") | |
499 | ffunc = getattr(formatter, f"format_{req}", None) | |
500 | if ffunc is None: | |
501 | raise UnsupportedFormat(format_req) | |
502 | return ffunc | |
503 | ||
504 | def _dry_run(self, format_req: Optional[str] = None) -> None: | |
505 | """Raise an exception if the format_req is not supported.""" | |
506 | # call with an empty dict to see if format_req is valid and supported | |
507 | self._get_format_func({}, format_req) | |
508 | ||
509 | def _formatted(self, obj: Any, format_req: Optional[str] = None) -> str: | |
510 | """Return the object formatted/serialized.""" | |
511 | ffunc = self._get_format_func(obj, format_req) | |
512 | return ffunc() | |
513 | ||
514 | def _return_value(self, obj: Any) -> int: | |
515 | """Return a mgr return-value for the given object (usually zero).""" | |
516 | return self._retval_provider(obj).mgr_return_value() | |
517 | ||
518 | def __call__(self, f: ObjectResponseFuncType) -> HandlerFuncType: | |
519 | """Wrap a python function so that the original function's return value | |
520 | becomes the source for an automatically formatted mgr response. | |
521 | """ | |
522 | ||
523 | @wraps(f) | |
524 | def _format_response(*args: Any, **kwargs: Any) -> Tuple[int, str, str]: | |
525 | format_req = _get_requested_format(f, kwargs) | |
526 | try: | |
527 | self._dry_run(format_req) | |
528 | robj = f(*args, **kwargs) | |
529 | body = self._formatted(robj, format_req) | |
530 | retval = self._return_value(robj) | |
531 | except ErrorResponseBase as e: | |
532 | return e.format_response() | |
533 | return retval, body, "" | |
534 | ||
535 | # set the extra args on our wrapper function. this will be consumed by | |
536 | # the CLICommand decorator and added to the set of optional arguments | |
537 | # on the ceph cli/api | |
538 | setattr(_format_response, "extra_args", {"format": str}) | |
539 | return _format_response | |
39ae355f TL |
540 | |
541 | ||
542 | class ErrorResponseHandler: | |
543 | """ErrorResponseHandler is a very simple decorator that handles functions that | |
544 | raise exceptions inheriting from ErrorResponseBase. If such an exception | |
545 | is raised that exception can and will be converted to a mgr response tuple. | |
546 | This is similar to Responder but error handling is all this decorator does. | |
547 | """ | |
548 | ||
549 | def __call__(self, f: Callable[..., Tuple[int, str, str]]) -> HandlerFuncType: | |
550 | """Wrap a python function so that if the function raises an exception inheriting | |
551 | ErrorResponderBase the error is correctly converted to a mgr response. | |
552 | """ | |
553 | ||
554 | @wraps(f) | |
555 | def _format_response(*args: Any, **kwargs: Any) -> Tuple[int, str, str]: | |
556 | try: | |
557 | retval, body, sts = f(*args, **kwargs) | |
558 | except ErrorResponseBase as e: | |
559 | return e.format_response() | |
560 | return retval, body, sts | |
561 | ||
562 | return _format_response | |
563 | ||
564 | ||
565 | class ConstantResponderBase: | |
566 | """The constant responder base assumes that a wrapped function should not | |
567 | be passing data back to the manager. It only responds with the default | |
568 | (constant) values provided. The process_response function allows a subclass | |
569 | to handle/log/validate any values that were returned from the wrapped | |
570 | function. | |
571 | ||
572 | This class can be used a building block for special decorators that | |
573 | do not normally emit response data. | |
574 | """ | |
575 | ||
576 | def mgr_return_value(self) -> int: | |
577 | return 0 | |
578 | ||
579 | def mgr_body_value(self) -> str: | |
580 | return "" | |
581 | ||
582 | def mgr_status_value(self) -> str: | |
583 | return "" | |
584 | ||
585 | def process_response(self, result: Any) -> None: | |
586 | return None | |
587 | ||
588 | def __call__(self, f: Callable) -> HandlerFuncType: | |
589 | """Wrap a python function so that if the function raises an exception | |
590 | inheriting ErrorResponderBase the error is correctly converted to a mgr | |
591 | response. Otherwise, it returns a default set of constant values. | |
592 | """ | |
593 | ||
594 | @wraps(f) | |
595 | def _format_response(*args: Any, **kwargs: Any) -> Tuple[int, str, str]: | |
596 | try: | |
597 | self.process_response(f(*args, **kwargs)) | |
598 | except ErrorResponseBase as e: | |
599 | return e.format_response() | |
600 | return self.mgr_return_value(), self.mgr_body_value(), self.mgr_status_value() | |
601 | return _format_response | |
602 | ||
603 | ||
604 | class EmptyResponder(ConstantResponderBase): | |
605 | """Always respond with an empty (string) body. Checks that the wrapped function | |
606 | returned None in order to ensure it is not being used on functions that | |
607 | return data objects. | |
608 | """ | |
609 | ||
610 | def process_response(self, result: Any) -> None: | |
611 | if result is not None: | |
612 | raise ValueError("EmptyResponder expects None from wrapped functions") |