Skip to content

Commit 4d811c3

Browse files
committed
Speed improvements
1 parent 1cb5297 commit 4d811c3

File tree

2 files changed

+82
-104
lines changed

2 files changed

+82
-104
lines changed

pynetbox/core/app.py

+12-2
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ def __init__(self, api, name):
4141
self.api = api
4242
self.name = name
4343
self._setmodel()
44+
self._cached_endpoints = {}
4445

4546
models = {
4647
"dcim": dcim,
@@ -63,7 +64,11 @@ def __setstate__(self, d):
6364
self._setmodel()
6465

6566
def __getattr__(self, name):
66-
return Endpoint(self.api, self, name, model=self.model)
67+
if name not in self._cached_endpoints:
68+
self._cached_endpoints[name] = Endpoint(
69+
self.api, self, name, model=self.model
70+
)
71+
return self._cached_endpoints[name]
6772

6873
def config(self):
6974
"""Returns config response from app
@@ -103,6 +108,7 @@ class PluginsApp:
103108

104109
def __init__(self, api):
105110
self.api = api
111+
self._cached_apps = {}
106112

107113
def __getstate__(self):
108114
return self.__dict__
@@ -111,7 +117,11 @@ def __setstate__(self, d):
111117
self.__dict__.update(d)
112118

113119
def __getattr__(self, name):
114-
return App(self.api, "plugins/{}".format(name.replace("_", "-")))
120+
if name not in self._cached_apps:
121+
self._cached_apps[name] = App(
122+
self.api, "plugins/{}".format(name.replace("_", "-"))
123+
)
124+
return self._cached_apps[name]
115125

116126
def installed_plugins(self):
117127
"""Returns raw response with installed plugins

pynetbox/core/response.py

+70-102
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,8 @@
1414
limitations under the License.
1515
"""
1616

17-
import copy
17+
import marshal
1818
from collections import OrderedDict
19-
from urllib.parse import urlsplit
2019

2120
import pynetbox.core.app
2221
from pynetbox.core.query import Request
@@ -26,35 +25,20 @@
2625
LIST_AS_SET = ("tags", "tagged_vlans")
2726

2827

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.
4131
"""
32+
return_fields = ["id", "value"]
4233

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
5640
else:
57-
return lookup
41+
return str(record)
5842

5943

6044
def flatten_custom(custom_dict):
@@ -277,7 +261,6 @@ class Record:
277261

278262
def __init__(self, values, api, endpoint):
279263
self.has_details = False
280-
self._full_cache = []
281264
self._init_cache = []
282265
self.api = api
283266
self.default_ret = Record
@@ -308,16 +291,16 @@ def __getattr__(self, k):
308291
raise AttributeError('object has no attribute "{}"'.format(k))
309292

310293
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)
313296
if isinstance(cur_attr, Record):
314-
yield i, dict(cur_attr)
297+
yield k, dict(cur_attr)
315298
elif isinstance(cur_attr, list) and all(
316299
isinstance(i, Record) for i in cur_attr
317300
):
318-
yield i, [dict(x) for x in cur_attr]
301+
yield k, [dict(x) for x in cur_attr]
319302
else:
320-
yield i, cur_attr
303+
yield k, cur_attr
321304

322305
def __getitem__(self, k):
323306
return dict(self)[k]
@@ -353,92 +336,77 @@ def __eq__(self, other):
353336
return self.__key__() == other.__key__()
354337
return NotImplemented
355338

356-
def _add_cache(self, item):
357-
key, value = item
358-
self._init_cache.append((key, get_return(value)))
359-
360339
def _parse_values(self, values):
361340
"""Parses values init arg.
362341
363342
Parses values dict at init and sets object attributes with the
364343
values within.
365344
"""
366345

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):
368358
from pynetbox.models.mapper import CONTENT_TYPE_MAPPER
369359

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)
381363
return list_item
382364

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, []
393368

394-
return list_item
369+
if key_name in ["constraints"]:
370+
return value, marshal.loads(marshal.dumps(value))
395371

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]
407376
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()]
427400

428401
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, "")
435403
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":
440405
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)
442410

443411
def full_details(self):
444412
"""Queries the hyperlinked endpoint if 'url' is defined.

0 commit comments

Comments
 (0)