-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathpreparers.py
More file actions
190 lines (166 loc) · 6.47 KB
/
preparers.py
File metadata and controls
190 lines (166 loc) · 6.47 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
import abc
class Preparer:
"""
A plain preparation object which just passes through data.
It also is relevant as the protocol subclasses should implement to work with
Restless.
"""
@abc.abstractmethod
def prepare(self, data):
"""
Handles actually transforming the data.
"""
class FieldsPreparer(Preparer):
"""
A more complex preparation object, this will return a given set of fields.
This takes a ``fields`` parameter, which should be a dictionary of
keys (fieldnames to expose to the user) & values (a dotted lookup path to
the desired attribute/key on the object).
Example::
preparer = FieldsPreparer(fields={
# ``user`` is the key the client will see.
# ``author.pk`` is the dotted path lookup ``FieldsPreparer``
# will traverse on the data to return a value.
'user': 'author.pk',
})
"""
def __init__(self, fields=None):
self.fields = fields or {}
def prepare(self, data):
"""
Handles transforming the provided data into the fielded data that should
be exposed to the end user.
Uses the ``lookup_data`` method to traverse dotted paths.
Returns a dictionary of data as the response.
"""
result = {}
if not self.fields:
# No fields specified. Serialize everything.
return data
for fieldname, lookup in self.fields.items():
if isinstance(lookup, SubPreparer):
result[fieldname] = lookup.prepare(data)
else:
result[fieldname] = self.lookup_data(lookup, data)
return result
def lookup_data(self, lookup, data):
"""
Given a lookup string, attempts to descend through nested data looking for
the value.
Can work with either dictionary-alikes or objects (or any combination of
those).
Lookups should be a string. If it is a dotted path, it will be split on
``.`` & it will traverse through to find the final value. If not, it will
simply attempt to find either a key or attribute of that name & return it.
Example::
>>> data = {
... 'type': 'message',
... 'greeting': {
... 'en': 'hello',
... 'fr': 'bonjour',
... 'es': 'hola',
... },
... 'person': Person(
... name='daniel'
... )
... }
>>> lookup_data('type', data)
'message'
>>> lookup_data('greeting.en', data)
'hello'
>>> lookup_data('person.name', data)
'daniel'
"""
value = data
parts = lookup.split('.')
if not parts or not parts[0]:
return value
part = parts[0]
remaining_lookup = '.'.join(parts[1:])
if callable(getattr(data, 'keys', None)) and hasattr(data, '__getitem__'):
# Dictionary enough for us.
value = data[part]
elif data is not None:
# Assume it's an object.
value = getattr(data, part)
# Call if it's callable except if it's a Django DB manager instance
# We check if is a manager by checking the db_manager (duck typing)
if callable(value) and not hasattr(value, 'db_manager'):
value = value()
if not remaining_lookup:
return value
# There's more to lookup, so dive in recursively.
return self.lookup_data(remaining_lookup, value)
class SubPreparer(FieldsPreparer):
"""
A preparation class designed to be used within other preparers.
This is primary to enable deeply-nested structures, allowing you
to compose/share definitions as well. Typical usage consists of creating
a configured instance of a FieldsPreparer, then use a `SubPreparer` to
pull it in.
Example::
# First, define the nested fields you'd like to expose.
author_preparer = FieldsPreparer(fields={
'id': 'pk',
'username': 'username',
'name': 'get_full_name',
})
# Then, in the main preparer, pull them in using `SubPreparer`.
preparer = FieldsPreparer(fields={
'author': SubPreparer('user', author_preparer),
# Other fields can come before/follow as normal.
'content': 'post',
'created': 'created_at',
})
"""
def __init__(self, lookup, preparer):
self.lookup = lookup
self.preparer = preparer
def get_inner_data(self, data):
"""
Used internally so that the correct data is extracted out of the
broader dataset, allowing the preparer being called to deal with just
the expected subset.
"""
return self.lookup_data(self.lookup, data)
def prepare(self, data):
"""
Handles passing the data to the configured preparer.
Uses the ``get_inner_data`` method to provide the correct subset of
the data.
Returns a dictionary of data as the response.
"""
return self.preparer.prepare(self.get_inner_data(data))
class CollectionSubPreparer(SubPreparer):
"""
A preparation class designed to handle collections of data.
This is useful in the case where you have a 1-to-many or many-to-many
relationship of data to expose as part of the parent data.
Example::
# First, set up a preparer that handles the data for each thing in
# the broader collection.
comment_preparer = FieldsPreparer(fields={
'comment': 'comment_text',
'created': 'created',
})
# Then use it with the ``CollectionSubPreparer`` to create a list
# of prepared sub items.
preparer = FieldsPreparer(fields={
# A normal blog post field.
'post': 'post_text',
# All the comments on the post.
'comments': CollectionSubPreparer('comments.all', comment_preparer),
})
"""
def prepare(self, data):
"""
Handles passing each item in the collection data to the configured
subpreparer.
Uses a loop and the ``get_inner_data`` method to provide the correct
item of the data.
Returns a list of data as the response.
"""
result = []
for item in self.get_inner_data(data):
result.append(self.preparer.prepare(item))
return result