diff options
author | Mike Crute <mike@crute.us> | 2017-10-30 02:20:47 +0000 |
---|---|---|
committer | Mike Crute <mike@crute.us> | 2017-10-30 02:27:08 +0000 |
commit | 1b20d353dabb63f2c68ee7f5563b5d3f22833a17 (patch) | |
tree | 3faa05192fed69a5467c2403a010b0a1765fe80f | |
parent | 8d8d3f1418eefe5e0ec179ddd768bb96866ecc8b (diff) | |
download | pydora-1b20d353dabb63f2c68ee7f5563b5d3f22833a17.tar.bz2 pydora-1b20d353dabb63f2c68ee7f5563b5d3f22833a17.tar.xz pydora-1b20d353dabb63f2c68ee7f5563b5d3f22833a17.zip |
Add documentation
The complex models don't make sense without a data sample and I always
end up looking at the tests. Instead add some docs and a data sample.
-rw-r--r-- | pandora/models/__init__.py | 113 |
1 files changed, 112 insertions, 1 deletions
diff --git a/pandora/models/__init__.py b/pandora/models/__init__.py index 6a14f45..f799308 100644 --- a/pandora/models/__init__.py +++ b/pandora/models/__init__.py | |||
@@ -7,13 +7,34 @@ def with_metaclass(meta, *bases): | |||
7 | 7 | ||
8 | 8 | ||
9 | class Field(namedtuple("Field", ["field", "default", "formatter"])): | 9 | class Field(namedtuple("Field", ["field", "default", "formatter"])): |
10 | """Model Field | ||
11 | |||
12 | Model fields represent JSON key/value pairs. When added to a PandoraModel | ||
13 | the describe the unpacking logic for the API JSON and will be replaced at | ||
14 | runtime with the values from the parsed JSON or their defaults. | ||
15 | |||
16 | field | ||
17 | name of the field from the incoming JSON | ||
18 | default | ||
19 | default value if key does not exist in the incoming JSON, None if not | ||
20 | provided | ||
21 | formatter | ||
22 | formatter function accepting an API client and the value of the field | ||
23 | as arguments, will be called on the value of the data for the field key | ||
24 | in the incoming JSON. The return value of this function is used as the | ||
25 | value of the field on the model object. | ||
26 | model | ||
27 | the model class that the value of this field should be constructed into | ||
28 | the model construction logic will handle building a list or single | ||
29 | model based on the type of data in the JSON | ||
30 | """ | ||
10 | 31 | ||
11 | def __new__(cls, field, default=None, formatter=None): | 32 | def __new__(cls, field, default=None, formatter=None): |
12 | return super(Field, cls).__new__(cls, field, default, formatter) | 33 | return super(Field, cls).__new__(cls, field, default, formatter) |
13 | 34 | ||
14 | 35 | ||
15 | class SyntheticField(namedtuple("SyntheticField", ["field"])): | 36 | class SyntheticField(namedtuple("SyntheticField", ["field"])): |
16 | """Field That Requires Synthesis | 37 | """Field Requiring Synthesis |
17 | 38 | ||
18 | Synthetic fields may exist in the data but generally do not and require | 39 | Synthetic fields may exist in the data but generally do not and require |
19 | additional synthesis to arrive ate a sane value. Subclasses must define | 40 | additional synthesis to arrive ate a sane value. Subclasses must define |
@@ -25,6 +46,17 @@ class SyntheticField(namedtuple("SyntheticField", ["field"])): | |||
25 | 46 | ||
26 | @staticmethod | 47 | @staticmethod |
27 | def formatter(api_client, field, data): # pragma: no cover | 48 | def formatter(api_client, field, data): # pragma: no cover |
49 | """Format Value for Model | ||
50 | |||
51 | The return value of this method is used as a value for the field in the | ||
52 | model of which this field is a member | ||
53 | |||
54 | api_client | ||
55 | instance of a Pandora API client | ||
56 | data | ||
57 | complete JSON data blob for the parent model of which this field is | ||
58 | a member | ||
59 | """ | ||
28 | raise NotImplementedError | 60 | raise NotImplementedError |
29 | 61 | ||
30 | 62 | ||
@@ -46,6 +78,14 @@ class ModelMetaClass(type): | |||
46 | 78 | ||
47 | 79 | ||
48 | class PandoraModel(with_metaclass(ModelMetaClass, object)): | 80 | class PandoraModel(with_metaclass(ModelMetaClass, object)): |
81 | """Pandora API Model | ||
82 | |||
83 | A single object representing a Pandora data object. Subclasses are | ||
84 | specified declaratively and contain Field objects as well as optionally | ||
85 | other methods. The end result object after loading from JSON will be a | ||
86 | normal python object with all fields declared in the schema populated and | ||
87 | consumers of these instances can ignore all of the details of this class. | ||
88 | """ | ||
49 | 89 | ||
50 | @staticmethod | 90 | @staticmethod |
51 | def json_to_date(api_client, data): | 91 | def json_to_date(api_client, data): |
@@ -53,6 +93,8 @@ class PandoraModel(with_metaclass(ModelMetaClass, object)): | |||
53 | 93 | ||
54 | @classmethod | 94 | @classmethod |
55 | def from_json_list(cls, api_client, data): | 95 | def from_json_list(cls, api_client, data): |
96 | """Convert a list of JSON values to a list of models | ||
97 | """ | ||
56 | return [cls.from_json(api_client, item) for item in data] | 98 | return [cls.from_json(api_client, item) for item in data] |
57 | 99 | ||
58 | def __init__(self, api_client): | 100 | def __init__(self, api_client): |
@@ -70,6 +112,13 @@ class PandoraModel(with_metaclass(ModelMetaClass, object)): | |||
70 | 112 | ||
71 | @staticmethod | 113 | @staticmethod |
72 | def populate_fields(api_client, instance, data): | 114 | def populate_fields(api_client, instance, data): |
115 | """Populate all fields of a model with data | ||
116 | |||
117 | Given a model with a PandoraModel superclass will enumerate all | ||
118 | declared fields on that model and populate the values of their Field | ||
119 | and SyntheticField classes. All declared fields will have a value after | ||
120 | this function runs even if they are missing from the incoming JSON. | ||
121 | """ | ||
73 | for key, value in instance.__class__._fields.items(): | 122 | for key, value in instance.__class__._fields.items(): |
74 | newval = data.get(value.field, value.default) | 123 | newval = data.get(value.field, value.default) |
75 | 124 | ||
@@ -85,11 +134,15 @@ class PandoraModel(with_metaclass(ModelMetaClass, object)): | |||
85 | 134 | ||
86 | @classmethod | 135 | @classmethod |
87 | def from_json(cls, api_client, data): | 136 | def from_json(cls, api_client, data): |
137 | """Convert one JSON value to a model object | ||
138 | """ | ||
88 | self = cls(api_client) | 139 | self = cls(api_client) |
89 | PandoraModel.populate_fields(api_client, self, data) | 140 | PandoraModel.populate_fields(api_client, self, data) |
90 | return self | 141 | return self |
91 | 142 | ||
92 | def _base_repr(self, and_also=None): | 143 | def _base_repr(self, and_also=None): |
144 | """Common repr logic for subclasses to hook | ||
145 | """ | ||
93 | items = [ | 146 | items = [ |
94 | "=".join((key, repr(getattr(self, key)))) | 147 | "=".join((key, repr(getattr(self, key)))) |
95 | for key in sorted(self._fields.keys())] | 148 | for key in sorted(self._fields.keys())] |
@@ -110,6 +163,31 @@ class PandoraModel(with_metaclass(ModelMetaClass, object)): | |||
110 | 163 | ||
111 | 164 | ||
112 | class PandoraListModel(PandoraModel, list): | 165 | class PandoraListModel(PandoraModel, list): |
166 | """Dict-like List of Pandora Models | ||
167 | |||
168 | Processes a JSON map, expecting a key that contains a list of maps. Will | ||
169 | process each item in the list, creating models for each one and a secondary | ||
170 | index based on the value in each item. This object behaves like a list and | ||
171 | like a dict. | ||
172 | |||
173 | Example JSON: | ||
174 | |||
175 | { | ||
176 | "__list_key__": [ | ||
177 | { "__index_key__": "key", "other": "fields" }, | ||
178 | { "__index_key__": "key", "other": "fields" } | ||
179 | ], | ||
180 | "other": "fields" | ||
181 | } | ||
182 | |||
183 | __list_key__ | ||
184 | they key within the parent map containing a list | ||
185 | __list_model__ | ||
186 | model class to use when constructing models for list contents | ||
187 | __index_key__ | ||
188 | key from each object in the model list that will be used as an index | ||
189 | within this object | ||
190 | """ | ||
113 | 191 | ||
114 | __list_key__ = None | 192 | __list_key__ = None |
115 | __list_model__ = None | 193 | __list_model__ = None |
@@ -159,6 +237,39 @@ class PandoraListModel(PandoraModel, list): | |||
159 | 237 | ||
160 | 238 | ||
161 | class PandoraDictListModel(PandoraModel, dict): | 239 | class PandoraDictListModel(PandoraModel, dict): |
240 | """Dict of Models | ||
241 | |||
242 | Processes a JSON map, expecting a key that contains a list of maps, each of | ||
243 | which contain a key and a list of values which are the final models. Will | ||
244 | process each item in the list, creating models for each one and storing the | ||
245 | constructed models in a map indexed by the dict key. Duplicated sub-maps | ||
246 | will be merged into one key for this model. | ||
247 | |||
248 | Example JSON: | ||
249 | |||
250 | { | ||
251 | "__dict_list_key__": [ | ||
252 | { | ||
253 | "__dict_key__": "key for this model", | ||
254 | "__list_key__": [ | ||
255 | { "model": "fields" }, | ||
256 | { "model": "fields" } | ||
257 | ] | ||
258 | } | ||
259 | ], | ||
260 | "other": "fields" | ||
261 | } | ||
262 | |||
263 | __dict_list_key__ | ||
264 | the key within the parent map that contains the maps that contain | ||
265 | lists of models | ||
266 | __dict_key__ | ||
267 | the key within the nested map that contains the key for this object | ||
268 | __list_key__ | ||
269 | they key within the nested map that contains the list of models | ||
270 | __list_model__ | ||
271 | model class to use when constructing models for list contents | ||
272 | """ | ||
162 | 273 | ||
163 | __dict_list_key__ = None | 274 | __dict_list_key__ = None |
164 | __dict_key__ = None | 275 | __dict_key__ = None |