Skip to content

Commit d6c3168

Browse files
pybind/mgr: add short guide to the object_format.py docstring
Add a short-ish guide to the use of the object_format module's Responder and other types. Signed-off-by: John Mulligan <[email protected]>
1 parent 55ee8a3 commit d6c3168

File tree

1 file changed

+135
-0
lines changed

1 file changed

+135
-0
lines changed

src/pybind/mgr/object_format.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,140 @@
11
# object_format.py provides types and functions for working with
22
# 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+
"""
3138

4139
import enum
5140
import errno

0 commit comments

Comments
 (0)