diff options
-rw-r--r-- | pandora/models/__init__.py | 309 | ||||
-rw-r--r-- | pandora/models/_base.py | 309 | ||||
-rw-r--r-- | pandora/models/pandora.py | 4 | ||||
-rw-r--r-- | tests/test_pandora/test_models.py | 2 |
4 files changed, 312 insertions, 312 deletions
diff --git a/pandora/models/__init__.py b/pandora/models/__init__.py index 34ee2f7..e69de29 100644 --- a/pandora/models/__init__.py +++ b/pandora/models/__init__.py | |||
@@ -1,309 +0,0 @@ | |||
1 | from datetime import datetime | ||
2 | from collections import namedtuple | ||
3 | |||
4 | |||
5 | class Field(namedtuple("Field", ["field", "default", "formatter", "model"])): | ||
6 | """Model Field | ||
7 | |||
8 | Model fields represent JSON key/value pairs. When added to a PandoraModel | ||
9 | the describe the unpacking logic for the API JSON and will be replaced at | ||
10 | runtime with the values from the parsed JSON or their defaults. | ||
11 | |||
12 | field | ||
13 | name of the field from the incoming JSON | ||
14 | default | ||
15 | default value if key does not exist in the incoming JSON, None if not | ||
16 | provided | ||
17 | formatter | ||
18 | formatter function accepting an API client and the value of the field | ||
19 | as arguments, will be called on the value of the data for the field key | ||
20 | in the incoming JSON. The return value of this function is used as the | ||
21 | value of the field on the model object. | ||
22 | model | ||
23 | the model class that the value of this field should be constructed into | ||
24 | the model construction logic will handle building a list or single | ||
25 | model based on the type of data in the JSON | ||
26 | """ | ||
27 | |||
28 | def __new__(cls, field, default=None, formatter=None, model=None): | ||
29 | return super(Field, cls).__new__(cls, field, default, formatter, model) | ||
30 | |||
31 | |||
32 | class SyntheticField(namedtuple("SyntheticField", ["field"])): | ||
33 | """Field Requiring Synthesis | ||
34 | |||
35 | Synthetic fields may exist in the data but generally do not and require | ||
36 | additional synthesis to arrive ate a sane value. Subclasses must define | ||
37 | a formatter method that receives an API client, field name, and full data | ||
38 | payload. | ||
39 | """ | ||
40 | |||
41 | def formatter(self, api_client, data, newval): # pragma: no cover | ||
42 | """Format Value for Model | ||
43 | |||
44 | The return value of this method is used as a value for the field in the | ||
45 | model of which this field is a member | ||
46 | |||
47 | api_client | ||
48 | instance of a Pandora API client | ||
49 | data | ||
50 | complete JSON data blob for the parent model of which this field is | ||
51 | a member | ||
52 | newval | ||
53 | the value of this field as retrieved from the JSON data after | ||
54 | having resolved default value logic | ||
55 | """ | ||
56 | raise NotImplementedError | ||
57 | |||
58 | |||
59 | class DateField(SyntheticField): | ||
60 | """Date Field | ||
61 | |||
62 | Handles a JSON map that contains a time field which is the timestamp with | ||
63 | nanosecond precision. | ||
64 | """ | ||
65 | |||
66 | def formatter(self, api_client, data, value): | ||
67 | if not value: | ||
68 | return None | ||
69 | |||
70 | return datetime.utcfromtimestamp(value["time"] / 1000) | ||
71 | |||
72 | |||
73 | class ModelMetaClass(type): | ||
74 | |||
75 | def __new__(cls, name, parents, dct): | ||
76 | dct["_fields"] = fields = {} | ||
77 | new_dct = dct.copy() | ||
78 | |||
79 | for key, val in dct.items(): | ||
80 | if key.startswith("__"): | ||
81 | continue | ||
82 | |||
83 | if isinstance(val, Field) or isinstance(val, SyntheticField): | ||
84 | fields[key] = val | ||
85 | del new_dct[key] | ||
86 | |||
87 | return super(ModelMetaClass, cls).__new__(cls, name, parents, new_dct) | ||
88 | |||
89 | |||
90 | class PandoraModel(object, metaclass=ModelMetaClass): | ||
91 | """Pandora API Model | ||
92 | |||
93 | A single object representing a Pandora data object. Subclasses are | ||
94 | specified declaratively and contain Field objects as well as optionally | ||
95 | other methods. The end result object after loading from JSON will be a | ||
96 | normal python object with all fields declared in the schema populated and | ||
97 | consumers of these instances can ignore all of the details of this class. | ||
98 | """ | ||
99 | |||
100 | @classmethod | ||
101 | def from_json_list(cls, api_client, data): | ||
102 | """Convert a list of JSON values to a list of models | ||
103 | """ | ||
104 | return [cls.from_json(api_client, item) for item in data] | ||
105 | |||
106 | def __init__(self, api_client): | ||
107 | self._api_client = api_client | ||
108 | |||
109 | safe_types = (type(None), str, bytes, int, bool) | ||
110 | |||
111 | for key, value in self._fields.items(): | ||
112 | default = getattr(value, "default", None) | ||
113 | |||
114 | if not isinstance(default, safe_types): | ||
115 | default = type(default)() | ||
116 | |||
117 | setattr(self, key, default) | ||
118 | |||
119 | @staticmethod | ||
120 | def populate_fields(api_client, instance, data): | ||
121 | """Populate all fields of a model with data | ||
122 | |||
123 | Given a model with a PandoraModel superclass will enumerate all | ||
124 | declared fields on that model and populate the values of their Field | ||
125 | and SyntheticField classes. All declared fields will have a value after | ||
126 | this function runs even if they are missing from the incoming JSON. | ||
127 | """ | ||
128 | for key, value in instance.__class__._fields.items(): | ||
129 | default = getattr(value, "default", None) | ||
130 | newval = data.get(value.field, default) | ||
131 | |||
132 | if isinstance(value, SyntheticField): | ||
133 | newval = value.formatter(api_client, data, newval) | ||
134 | setattr(instance, key, newval) | ||
135 | continue | ||
136 | |||
137 | model_class = getattr(value, "model", None) | ||
138 | if newval and model_class: | ||
139 | if isinstance(newval, list): | ||
140 | newval = model_class.from_json_list(api_client, newval) | ||
141 | else: | ||
142 | newval = model_class.from_json(api_client, newval) | ||
143 | |||
144 | if newval and value.formatter: | ||
145 | newval = value.formatter(api_client, newval) | ||
146 | |||
147 | setattr(instance, key, newval) | ||
148 | |||
149 | @classmethod | ||
150 | def from_json(cls, api_client, data): | ||
151 | """Convert one JSON value to a model object | ||
152 | """ | ||
153 | self = cls(api_client) | ||
154 | PandoraModel.populate_fields(api_client, self, data) | ||
155 | return self | ||
156 | |||
157 | def _base_repr(self, and_also=None): | ||
158 | """Common repr logic for subclasses to hook | ||
159 | """ | ||
160 | items = [ | ||
161 | "=".join((key, repr(getattr(self, key)))) | ||
162 | for key in sorted(self._fields.keys())] | ||
163 | |||
164 | if items: | ||
165 | output = ", ".join(items) | ||
166 | else: | ||
167 | output = None | ||
168 | |||
169 | if and_also: | ||
170 | return "{}({}, {})".format(self.__class__.__name__, | ||
171 | output, and_also) | ||
172 | else: | ||
173 | return "{}({})".format(self.__class__.__name__, output) | ||
174 | |||
175 | def __repr__(self): | ||
176 | return self._base_repr() | ||
177 | |||
178 | |||
179 | class PandoraListModel(PandoraModel, list): | ||
180 | """Dict-like List of Pandora Models | ||
181 | |||
182 | Processes a JSON map, expecting a key that contains a list of maps. Will | ||
183 | process each item in the list, creating models for each one and a secondary | ||
184 | index based on the value in each item. This object behaves like a list and | ||
185 | like a dict. | ||
186 | |||
187 | Example JSON: | ||
188 | |||
189 | { | ||
190 | "__list_key__": [ | ||
191 | { "__index_key__": "key", "other": "fields" }, | ||
192 | { "__index_key__": "key", "other": "fields" } | ||
193 | ], | ||
194 | "other": "fields" | ||
195 | } | ||
196 | |||
197 | __list_key__ | ||
198 | they key within the parent map containing a list | ||
199 | __list_model__ | ||
200 | model class to use when constructing models for list contents | ||
201 | __index_key__ | ||
202 | key from each object in the model list that will be used as an index | ||
203 | within this object | ||
204 | """ | ||
205 | |||
206 | __list_key__ = None | ||
207 | __list_model__ = None | ||
208 | __index_key__ = None | ||
209 | |||
210 | def __init__(self, *args, **kwargs): | ||
211 | super(PandoraListModel, self).__init__(*args, **kwargs) | ||
212 | self._index = {} | ||
213 | |||
214 | @classmethod | ||
215 | def from_json(cls, api_client, data): | ||
216 | self = cls(api_client) | ||
217 | PandoraModel.populate_fields(api_client, self, data) | ||
218 | |||
219 | for item in data[cls.__list_key__]: | ||
220 | model = cls.__list_model__.from_json(api_client, item) | ||
221 | |||
222 | if self.__index_key__: | ||
223 | value = getattr(model, self.__index_key__) | ||
224 | self._index[value] = model | ||
225 | |||
226 | self.append(model) | ||
227 | |||
228 | return self | ||
229 | |||
230 | def __getitem__(self, key): | ||
231 | item = self._index.get(key, None) | ||
232 | if item: | ||
233 | return item | ||
234 | else: | ||
235 | return list.__getitem__(self, key) | ||
236 | |||
237 | def __contains__(self, key): | ||
238 | if key in self._index: | ||
239 | return True | ||
240 | else: | ||
241 | return list.__contains__(self, key) | ||
242 | |||
243 | def keys(self): | ||
244 | return self._index.keys() | ||
245 | |||
246 | def items(self): | ||
247 | return self._index.items() | ||
248 | |||
249 | def __repr__(self): | ||
250 | return self._base_repr(and_also=list.__repr__(self)) | ||
251 | |||
252 | |||
253 | class PandoraDictListModel(PandoraModel, dict): | ||
254 | """Dict of Models | ||
255 | |||
256 | Processes a JSON map, expecting a key that contains a list of maps, each of | ||
257 | which contain a key and a list of values which are the final models. Will | ||
258 | process each item in the list, creating models for each one and storing the | ||
259 | constructed models in a map indexed by the dict key. Duplicated sub-maps | ||
260 | will be merged into one key for this model. | ||
261 | |||
262 | Example JSON: | ||
263 | |||
264 | { | ||
265 | "__dict_list_key__": [ | ||
266 | { | ||
267 | "__dict_key__": "key for this model", | ||
268 | "__list_key__": [ | ||
269 | { "model": "fields" }, | ||
270 | { "model": "fields" } | ||
271 | ] | ||
272 | } | ||
273 | ], | ||
274 | "other": "fields" | ||
275 | } | ||
276 | |||
277 | __dict_list_key__ | ||
278 | the key within the parent map that contains the maps that contain | ||
279 | lists of models | ||
280 | __dict_key__ | ||
281 | the key within the nested map that contains the key for this object | ||
282 | __list_key__ | ||
283 | they key within the nested map that contains the list of models | ||
284 | __list_model__ | ||
285 | model class to use when constructing models for list contents | ||
286 | """ | ||
287 | |||
288 | __dict_list_key__ = None | ||
289 | __dict_key__ = None | ||
290 | __list_key__ = None | ||
291 | __list_model__ = None | ||
292 | |||
293 | @classmethod | ||
294 | def from_json(cls, api_client, data): | ||
295 | self = cls(api_client) | ||
296 | PandoraModel.populate_fields(api_client, self, data) | ||
297 | |||
298 | for item in data[self.__dict_list_key__]: | ||
299 | key = item[self.__dict_key__] | ||
300 | self[key] = [] | ||
301 | |||
302 | for part in item[self.__list_key__]: | ||
303 | self[key].append( | ||
304 | cls.__list_model__.from_json(api_client, part)) | ||
305 | |||
306 | return self | ||
307 | |||
308 | def __repr__(self): | ||
309 | return self._base_repr(and_also=dict.__repr__(self)) | ||
diff --git a/pandora/models/_base.py b/pandora/models/_base.py new file mode 100644 index 0000000..34ee2f7 --- /dev/null +++ b/pandora/models/_base.py | |||
@@ -0,0 +1,309 @@ | |||
1 | from datetime import datetime | ||
2 | from collections import namedtuple | ||
3 | |||
4 | |||
5 | class Field(namedtuple("Field", ["field", "default", "formatter", "model"])): | ||
6 | """Model Field | ||
7 | |||
8 | Model fields represent JSON key/value pairs. When added to a PandoraModel | ||
9 | the describe the unpacking logic for the API JSON and will be replaced at | ||
10 | runtime with the values from the parsed JSON or their defaults. | ||
11 | |||
12 | field | ||
13 | name of the field from the incoming JSON | ||
14 | default | ||
15 | default value if key does not exist in the incoming JSON, None if not | ||
16 | provided | ||
17 | formatter | ||
18 | formatter function accepting an API client and the value of the field | ||
19 | as arguments, will be called on the value of the data for the field key | ||
20 | in the incoming JSON. The return value of this function is used as the | ||
21 | value of the field on the model object. | ||
22 | model | ||
23 | the model class that the value of this field should be constructed into | ||
24 | the model construction logic will handle building a list or single | ||
25 | model based on the type of data in the JSON | ||
26 | """ | ||
27 | |||
28 | def __new__(cls, field, default=None, formatter=None, model=None): | ||
29 | return super(Field, cls).__new__(cls, field, default, formatter, model) | ||
30 | |||
31 | |||
32 | class SyntheticField(namedtuple("SyntheticField", ["field"])): | ||
33 | """Field Requiring Synthesis | ||
34 | |||
35 | Synthetic fields may exist in the data but generally do not and require | ||
36 | additional synthesis to arrive ate a sane value. Subclasses must define | ||
37 | a formatter method that receives an API client, field name, and full data | ||
38 | payload. | ||
39 | """ | ||
40 | |||
41 | def formatter(self, api_client, data, newval): # pragma: no cover | ||
42 | """Format Value for Model | ||
43 | |||
44 | The return value of this method is used as a value for the field in the | ||
45 | model of which this field is a member | ||
46 | |||
47 | api_client | ||
48 | instance of a Pandora API client | ||
49 | data | ||
50 | complete JSON data blob for the parent model of which this field is | ||
51 | a member | ||
52 | newval | ||
53 | the value of this field as retrieved from the JSON data after | ||
54 | having resolved default value logic | ||
55 | """ | ||
56 | raise NotImplementedError | ||
57 | |||
58 | |||
59 | class DateField(SyntheticField): | ||
60 | """Date Field | ||
61 | |||
62 | Handles a JSON map that contains a time field which is the timestamp with | ||
63 | nanosecond precision. | ||
64 | """ | ||
65 | |||
66 | def formatter(self, api_client, data, value): | ||
67 | if not value: | ||
68 | return None | ||
69 | |||
70 | return datetime.utcfromtimestamp(value["time"] / 1000) | ||
71 | |||
72 | |||
73 | class ModelMetaClass(type): | ||
74 | |||
75 | def __new__(cls, name, parents, dct): | ||
76 | dct["_fields"] = fields = {} | ||
77 | new_dct = dct.copy() | ||
78 | |||
79 | for key, val in dct.items(): | ||
80 | if key.startswith("__"): | ||
81 | continue | ||
82 | |||
83 | if isinstance(val, Field) or isinstance(val, SyntheticField): | ||
84 | fields[key] = val | ||
85 | del new_dct[key] | ||
86 | |||
87 | return super(ModelMetaClass, cls).__new__(cls, name, parents, new_dct) | ||
88 | |||
89 | |||
90 | class PandoraModel(object, metaclass=ModelMetaClass): | ||
91 | """Pandora API Model | ||
92 | |||
93 | A single object representing a Pandora data object. Subclasses are | ||
94 | specified declaratively and contain Field objects as well as optionally | ||
95 | other methods. The end result object after loading from JSON will be a | ||
96 | normal python object with all fields declared in the schema populated and | ||
97 | consumers of these instances can ignore all of the details of this class. | ||
98 | """ | ||
99 | |||
100 | @classmethod | ||
101 | def from_json_list(cls, api_client, data): | ||
102 | """Convert a list of JSON values to a list of models | ||
103 | """ | ||
104 | return [cls.from_json(api_client, item) for item in data] | ||
105 | |||
106 | def __init__(self, api_client): | ||
107 | self._api_client = api_client | ||
108 | |||
109 | safe_types = (type(None), str, bytes, int, bool) | ||
110 | |||
111 | for key, value in self._fields.items(): | ||
112 | default = getattr(value, "default", None) | ||
113 | |||
114 | if not isinstance(default, safe_types): | ||
115 | default = type(default)() | ||
116 | |||
117 | setattr(self, key, default) | ||
118 | |||
119 | @staticmethod | ||
120 | def populate_fields(api_client, instance, data): | ||
121 | """Populate all fields of a model with data | ||
122 | |||
123 | Given a model with a PandoraModel superclass will enumerate all | ||
124 | declared fields on that model and populate the values of their Field | ||
125 | and SyntheticField classes. All declared fields will have a value after | ||
126 | this function runs even if they are missing from the incoming JSON. | ||
127 | """ | ||
128 | for key, value in instance.__class__._fields.items(): | ||
129 | default = getattr(value, "default", None) | ||
130 | newval = data.get(value.field, default) | ||
131 | |||
132 | if isinstance(value, SyntheticField): | ||
133 | newval = value.formatter(api_client, data, newval) | ||
134 | setattr(instance, key, newval) | ||
135 | continue | ||
136 | |||
137 | model_class = getattr(value, "model", None) | ||
138 | if newval and model_class: | ||
139 | if isinstance(newval, list): | ||
140 | newval = model_class.from_json_list(api_client, newval) | ||
141 | else: | ||
142 | newval = model_class.from_json(api_client, newval) | ||
143 | |||
144 | if newval and value.formatter: | ||
145 | newval = value.formatter(api_client, newval) | ||
146 | |||
147 | setattr(instance, key, newval) | ||
148 | |||
149 | @classmethod | ||
150 | def from_json(cls, api_client, data): | ||
151 | """Convert one JSON value to a model object | ||
152 | """ | ||
153 | self = cls(api_client) | ||
154 | PandoraModel.populate_fields(api_client, self, data) | ||
155 | return self | ||
156 | |||
157 | def _base_repr(self, and_also=None): | ||
158 | """Common repr logic for subclasses to hook | ||
159 | """ | ||
160 | items = [ | ||
161 | "=".join((key, repr(getattr(self, key)))) | ||
162 | for key in sorted(self._fields.keys())] | ||
163 | |||
164 | if items: | ||
165 | output = ", ".join(items) | ||
166 | else: | ||
167 | output = None | ||
168 | |||
169 | if and_also: | ||
170 | return "{}({}, {})".format(self.__class__.__name__, | ||
171 | output, and_also) | ||
172 | else: | ||
173 | return "{}({})".format(self.__class__.__name__, output) | ||
174 | |||
175 | def __repr__(self): | ||
176 | return self._base_repr() | ||
177 | |||
178 | |||
179 | class PandoraListModel(PandoraModel, list): | ||
180 | """Dict-like List of Pandora Models | ||
181 | |||
182 | Processes a JSON map, expecting a key that contains a list of maps. Will | ||
183 | process each item in the list, creating models for each one and a secondary | ||
184 | index based on the value in each item. This object behaves like a list and | ||
185 | like a dict. | ||
186 | |||
187 | Example JSON: | ||
188 | |||
189 | { | ||
190 | "__list_key__": [ | ||
191 | { "__index_key__": "key", "other": "fields" }, | ||
192 | { "__index_key__": "key", "other": "fields" } | ||
193 | ], | ||
194 | "other": "fields" | ||
195 | } | ||
196 | |||
197 | __list_key__ | ||
198 | they key within the parent map containing a list | ||
199 | __list_model__ | ||
200 | model class to use when constructing models for list contents | ||
201 | __index_key__ | ||
202 | key from each object in the model list that will be used as an index | ||
203 | within this object | ||
204 | """ | ||
205 | |||
206 | __list_key__ = None | ||
207 | __list_model__ = None | ||
208 | __index_key__ = None | ||
209 | |||
210 | def __init__(self, *args, **kwargs): | ||
211 | super(PandoraListModel, self).__init__(*args, **kwargs) | ||
212 | self._index = {} | ||
213 | |||
214 | @classmethod | ||
215 | def from_json(cls, api_client, data): | ||
216 | self = cls(api_client) | ||
217 | PandoraModel.populate_fields(api_client, self, data) | ||
218 | |||
219 | for item in data[cls.__list_key__]: | ||
220 | model = cls.__list_model__.from_json(api_client, item) | ||
221 | |||
222 | if self.__index_key__: | ||
223 | value = getattr(model, self.__index_key__) | ||
224 | self._index[value] = model | ||
225 | |||
226 | self.append(model) | ||
227 | |||
228 | return self | ||
229 | |||
230 | def __getitem__(self, key): | ||
231 | item = self._index.get(key, None) | ||
232 | if item: | ||
233 | return item | ||
234 | else: | ||
235 | return list.__getitem__(self, key) | ||
236 | |||
237 | def __contains__(self, key): | ||
238 | if key in self._index: | ||
239 | return True | ||
240 | else: | ||
241 | return list.__contains__(self, key) | ||
242 | |||
243 | def keys(self): | ||
244 | return self._index.keys() | ||
245 | |||
246 | def items(self): | ||
247 | return self._index.items() | ||
248 | |||
249 | def __repr__(self): | ||
250 | return self._base_repr(and_also=list.__repr__(self)) | ||
251 | |||
252 | |||
253 | class PandoraDictListModel(PandoraModel, dict): | ||
254 | """Dict of Models | ||
255 | |||
256 | Processes a JSON map, expecting a key that contains a list of maps, each of | ||
257 | which contain a key and a list of values which are the final models. Will | ||
258 | process each item in the list, creating models for each one and storing the | ||
259 | constructed models in a map indexed by the dict key. Duplicated sub-maps | ||
260 | will be merged into one key for this model. | ||
261 | |||
262 | Example JSON: | ||
263 | |||
264 | { | ||
265 | "__dict_list_key__": [ | ||
266 | { | ||
267 | "__dict_key__": "key for this model", | ||
268 | "__list_key__": [ | ||
269 | { "model": "fields" }, | ||
270 | { "model": "fields" } | ||
271 | ] | ||
272 | } | ||
273 | ], | ||
274 | "other": "fields" | ||
275 | } | ||
276 | |||
277 | __dict_list_key__ | ||
278 | the key within the parent map that contains the maps that contain | ||
279 | lists of models | ||
280 | __dict_key__ | ||
281 | the key within the nested map that contains the key for this object | ||
282 | __list_key__ | ||
283 | they key within the nested map that contains the list of models | ||
284 | __list_model__ | ||
285 | model class to use when constructing models for list contents | ||
286 | """ | ||
287 | |||
288 | __dict_list_key__ = None | ||
289 | __dict_key__ = None | ||
290 | __list_key__ = None | ||
291 | __list_model__ = None | ||
292 | |||
293 | @classmethod | ||
294 | def from_json(cls, api_client, data): | ||
295 | self = cls(api_client) | ||
296 | PandoraModel.populate_fields(api_client, self, data) | ||
297 | |||
298 | for item in data[self.__dict_list_key__]: | ||
299 | key = item[self.__dict_key__] | ||
300 | self[key] = [] | ||
301 | |||
302 | for part in item[self.__list_key__]: | ||
303 | self[key].append( | ||
304 | cls.__list_model__.from_json(api_client, part)) | ||
305 | |||
306 | return self | ||
307 | |||
308 | def __repr__(self): | ||
309 | return self._base_repr(and_also=dict.__repr__(self)) | ||
diff --git a/pandora/models/pandora.py b/pandora/models/pandora.py index ecc1225..341b0be 100644 --- a/pandora/models/pandora.py +++ b/pandora/models/pandora.py | |||
@@ -2,8 +2,8 @@ from enum import Enum | |||
2 | 2 | ||
3 | from ..client import BaseAPIClient | 3 | from ..client import BaseAPIClient |
4 | from ..errors import ParameterMissing | 4 | from ..errors import ParameterMissing |
5 | from . import Field, DateField, SyntheticField | 5 | from ._base import Field, DateField, SyntheticField |
6 | from . import PandoraModel, PandoraListModel, PandoraDictListModel | 6 | from ._base import PandoraModel, PandoraListModel, PandoraDictListModel |
7 | 7 | ||
8 | 8 | ||
9 | class AdditionalAudioUrl(Enum): | 9 | class AdditionalAudioUrl(Enum): |
diff --git a/tests/test_pandora/test_models.py b/tests/test_pandora/test_models.py index 13ca366..8e0e0b5 100644 --- a/tests/test_pandora/test_models.py +++ b/tests/test_pandora/test_models.py | |||
@@ -5,7 +5,7 @@ from unittest.mock import Mock, patch | |||
5 | from pandora.client import APIClient | 5 | from pandora.client import APIClient |
6 | from pandora.errors import ParameterMissing | 6 | from pandora.errors import ParameterMissing |
7 | 7 | ||
8 | import pandora.models as m | 8 | import pandora.models._base as m |
9 | import pandora.models.pandora as pm | 9 | import pandora.models.pandora as pm |
10 | 10 | ||
11 | 11 | ||