|
14 | 14 | limitations under the License.
|
15 | 15 | """
|
16 | 16 |
|
17 |
| -import copy |
| 17 | +import marshal |
18 | 18 | from collections import OrderedDict
|
19 |
| -from urllib.parse import urlsplit |
20 | 19 |
|
21 | 20 | import pynetbox.core.app
|
22 | 21 | from pynetbox.core.query import Request
|
|
26 | 25 | LIST_AS_SET = ("tags", "tagged_vlans")
|
27 | 26 |
|
28 | 27 |
|
29 |
| -def get_return(lookup, return_fields=None): |
30 |
| - """Returns simple representations for items passed to lookup. |
31 |
| -
|
32 |
| - Used to return a "simple" representation of objects and collections |
33 |
| - sent to it via lookup. Otherwise, we look to see if |
34 |
| - lookup is a "choices" field (dict with only 'id' and 'value') |
35 |
| - or a nested_return. Finally, we check if it's a Record, if |
36 |
| - so simply return a string. Order is important due to nested_return |
37 |
| - being self-referential. |
38 |
| -
|
39 |
| - :arg list,optional return_fields: A list of fields to reference when |
40 |
| - calling values on lookup. |
| 28 | +def get_return(record): |
| 29 | + """ |
| 30 | + Used to return a "simple" representation of objects and collections. |
41 | 31 | """
|
| 32 | + return_fields = ["id", "value"] |
42 | 33 |
|
43 |
| - for i in return_fields or ["id", "value", "nested_return"]: |
44 |
| - if isinstance(lookup, dict) and lookup.get(i): |
45 |
| - return lookup[i] |
46 |
| - else: |
47 |
| - if hasattr(lookup, i): |
48 |
| - # check if this is a "choices" field record |
49 |
| - # from a NetBox 2.7 server. |
50 |
| - if sorted(dict(lookup)) == sorted(["id", "value", "label"]): |
51 |
| - return getattr(lookup, "value") |
52 |
| - return getattr(lookup, i) |
53 |
| - |
54 |
| - if isinstance(lookup, Record): |
55 |
| - return str(lookup) |
| 34 | + if not isinstance(record, Record): |
| 35 | + raise ValueError |
| 36 | + |
| 37 | + for i in return_fields: |
| 38 | + if value := getattr(record, i, None): |
| 39 | + return value |
56 | 40 | else:
|
57 |
| - return lookup |
| 41 | + return str(record) |
58 | 42 |
|
59 | 43 |
|
60 | 44 | def flatten_custom(custom_dict):
|
@@ -277,7 +261,6 @@ class Record:
|
277 | 261 |
|
278 | 262 | def __init__(self, values, api, endpoint):
|
279 | 263 | self.has_details = False
|
280 |
| - self._full_cache = [] |
281 | 264 | self._init_cache = []
|
282 | 265 | self.api = api
|
283 | 266 | self.default_ret = Record
|
@@ -308,16 +291,16 @@ def __getattr__(self, k):
|
308 | 291 | raise AttributeError('object has no attribute "{}"'.format(k))
|
309 | 292 |
|
310 | 293 | def __iter__(self):
|
311 |
| - for i in dict(self._init_cache): |
312 |
| - cur_attr = getattr(self, i) |
| 294 | + for k, _ in self._init_cache: |
| 295 | + cur_attr = getattr(self, k) |
313 | 296 | if isinstance(cur_attr, Record):
|
314 |
| - yield i, dict(cur_attr) |
| 297 | + yield k, dict(cur_attr) |
315 | 298 | elif isinstance(cur_attr, list) and all(
|
316 | 299 | isinstance(i, Record) for i in cur_attr
|
317 | 300 | ):
|
318 |
| - yield i, [dict(x) for x in cur_attr] |
| 301 | + yield k, [dict(x) for x in cur_attr] |
319 | 302 | else:
|
320 |
| - yield i, cur_attr |
| 303 | + yield k, cur_attr |
321 | 304 |
|
322 | 305 | def __getitem__(self, k):
|
323 | 306 | return dict(self)[k]
|
@@ -353,92 +336,77 @@ def __eq__(self, other):
|
353 | 336 | return self.__key__() == other.__key__()
|
354 | 337 | return NotImplemented
|
355 | 338 |
|
356 |
| - def _add_cache(self, item): |
357 |
| - key, value = item |
358 |
| - self._init_cache.append((key, get_return(value))) |
359 |
| - |
360 | 339 | def _parse_values(self, values):
|
361 | 340 | """Parses values init arg.
|
362 | 341 |
|
363 | 342 | Parses values dict at init and sets object attributes with the
|
364 | 343 | values within.
|
365 | 344 | """
|
366 | 345 |
|
367 |
| - def generic_list_parser(key_name, list_item): |
| 346 | + def dict_parser(key_name, value): |
| 347 | + # We keep must keep some specific fields as dictionaries |
| 348 | + if key_name not in ["custom_fields", "local_context_data"]: |
| 349 | + lookup = getattr(self.__class__, key_name, None) |
| 350 | + if lookup is None or not issubclass(lookup, JsonField): |
| 351 | + # If we have a custom model field, use it, otherwise use a default Record model |
| 352 | + args = [value, self.api, self.endpoint] |
| 353 | + value = lookup(*args) if lookup else self.default_ret(*args) |
| 354 | + return value, get_return(value) |
| 355 | + return value, marshal.loads(marshal.dumps(value)) |
| 356 | + |
| 357 | + def list_item_parser(list_item): |
368 | 358 | from pynetbox.models.mapper import CONTENT_TYPE_MAPPER
|
369 | 359 |
|
370 |
| - if ( |
371 |
| - isinstance(list_item, dict) |
372 |
| - and "object_type" in list_item |
373 |
| - and "object" in list_item |
374 |
| - ): |
375 |
| - lookup = list_item["object_type"] |
376 |
| - model = None |
377 |
| - model = CONTENT_TYPE_MAPPER.get(lookup) |
378 |
| - if model: |
379 |
| - return model(list_item["object"], self.api, self.endpoint) |
380 |
| - |
| 360 | + lookup = list_item["object_type"] |
| 361 | + if model := CONTENT_TYPE_MAPPER.get(lookup, None): |
| 362 | + return model(list_item["object"], self.api, self.endpoint) |
381 | 363 | return list_item
|
382 | 364 |
|
383 |
| - def list_parser(key_name, list_item): |
384 |
| - if isinstance(list_item, dict): |
385 |
| - lookup = getattr(self.__class__, key_name, None) |
386 |
| - if not isinstance(lookup, list): |
387 |
| - # This is *list_parser*, so if the custom model field is not |
388 |
| - # a list (or is not defined), just return the default model |
389 |
| - return self.default_ret(list_item, self.api, self.endpoint) |
390 |
| - else: |
391 |
| - model = lookup[0] |
392 |
| - return model(list_item, self.api, self.endpoint) |
| 365 | + def list_parser(key_name, value): |
| 366 | + if not value: |
| 367 | + return value, [] |
393 | 368 |
|
394 |
| - return list_item |
| 369 | + if key_name in ["constraints"]: |
| 370 | + return value, marshal.loads(marshal.dumps(value)) |
395 | 371 |
|
396 |
| - for k, v in values.items(): |
397 |
| - if isinstance(v, dict): |
398 |
| - lookup = getattr(self.__class__, k, None) |
399 |
| - if k in ["custom_fields", "local_context_data"] or hasattr( |
400 |
| - lookup, "_json_field" |
401 |
| - ): |
402 |
| - self._add_cache((k, copy.deepcopy(v))) |
403 |
| - setattr(self, k, v) |
404 |
| - continue |
405 |
| - if lookup: |
406 |
| - v = lookup(v, self.api, self.endpoint) |
| 372 | + sample_item = value[0] |
| 373 | + if isinstance(sample_item, dict): |
| 374 | + if "object_type" in sample_item and "object" in sample_item: |
| 375 | + value = [list_item_parser(item) for item in value] |
407 | 376 | else:
|
408 |
| - v = self.default_ret(v, self.api, self.endpoint) |
409 |
| - self._add_cache((k, v)) |
410 |
| - |
411 |
| - elif isinstance(v, list): |
412 |
| - # check if GFK |
413 |
| - if len(v) and isinstance(v[0], dict) and "object_type" in v[0]: |
414 |
| - v = [generic_list_parser(k, i) for i in v] |
415 |
| - to_cache = list(v) |
416 |
| - elif k == "constraints": |
417 |
| - # Permissions constraints can be either dict or list |
418 |
| - to_cache = copy.deepcopy(v) |
419 |
| - else: |
420 |
| - v = [list_parser(k, i) for i in v] |
421 |
| - to_cache = list(v) |
422 |
| - self._add_cache((k, to_cache)) |
423 |
| - |
424 |
| - else: |
425 |
| - self._add_cache((k, v)) |
426 |
| - setattr(self, k, v) |
| 377 | + lookup = getattr(self.__class__, key_name, None) |
| 378 | + if not isinstance(lookup, list): |
| 379 | + # This is *list_parser*, so if the custom model field is not |
| 380 | + # a list (or is not defined), just return the default model |
| 381 | + value = [ |
| 382 | + self.default_ret(i, self.api, self.endpoint) for i in value |
| 383 | + ] |
| 384 | + else: |
| 385 | + model = lookup[0] |
| 386 | + value = [model(i, self.api, self.endpoint) for i in value] |
| 387 | + return value, [*value] |
| 388 | + |
| 389 | + def parse_value(key_name, value): |
| 390 | + if not isinstance(value, (dict, list)): |
| 391 | + to_cache = value |
| 392 | + elif isinstance(value, dict): |
| 393 | + value, to_cache = dict_parser(key_name, value) |
| 394 | + elif isinstance(value, list): |
| 395 | + value, to_cache = list_parser(key_name, value) |
| 396 | + setattr(self, key_name, value) |
| 397 | + return to_cache |
| 398 | + |
| 399 | + self._init_cache = [(k, parse_value(k, v)) for k, v in values.items()] |
427 | 400 |
|
428 | 401 | def _endpoint_from_url(self, url):
|
429 |
| - url_path = urlsplit(url).path |
430 |
| - base_url_path_parts = urlsplit(self.api.base_url).path.split("/") |
431 |
| - if len(base_url_path_parts) > 2: |
432 |
| - # There are some extra directories in the path, remove them from url |
433 |
| - extra_path = "/".join(base_url_path_parts[:-1]) |
434 |
| - url_path = url_path[len(extra_path) :] |
| 402 | + url_path = url.replace(self.api.base_url, "") |
435 | 403 | split_url_path = url_path.split("/")
|
436 |
| - if split_url_path[2] == "plugins": |
437 |
| - app = "plugins/{}".format(split_url_path[3]) |
438 |
| - name = split_url_path[4] |
439 |
| - else: |
| 404 | + if split_url_path[1] == "plugins": |
440 | 405 | app, name = split_url_path[2:4]
|
441 |
| - return getattr(pynetbox.core.app.App(self.api, app), name) |
| 406 | + return getattr(getattr(getattr(self.api, "plugins"), app), name) |
| 407 | + else: |
| 408 | + app, name = split_url_path[1:3] |
| 409 | + return getattr(getattr(self.api, app), name) |
442 | 410 |
|
443 | 411 | def full_details(self):
|
444 | 412 | """Queries the hyperlinked endpoint if 'url' is defined.
|
|
0 commit comments