Skip to content

Commit

Permalink
Merge pull request #10 from digitalocean/serializer-fix
Browse files Browse the repository at this point in the history
Interface Serializer fix
  • Loading branch information
Zach Moody authored Sep 7, 2017
2 parents 3bc6791 + 39f9aa7 commit 55f15d2
Show file tree
Hide file tree
Showing 7 changed files with 186 additions and 110 deletions.
14 changes: 11 additions & 3 deletions pynetbox/dcim.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
See the License for the specific language governing permissions and
limitations under the License.
'''
from pynetbox.lib.response import Record, BoolRecord
from pynetbox.lib.response import Record
from pynetbox.ipam import IpAddresses


Expand All @@ -38,8 +38,6 @@ class Devices(Record):
return an initialized DeviceType object
"""
has_details = True
status = BoolRecord
face = BoolRecord
device_type = DeviceTypes
primary_ip = IpAddresses
primary_ip4 = IpAddresses
Expand All @@ -52,6 +50,16 @@ def __str__(self):
return self.interface_a.name


class InterfaceConnection(Record):

def __str__(self):
return self.interface.name


class Interfaces(Record):
interface_connection = InterfaceConnection


class RackReservations(Record):

def __str__(self):
Expand Down
4 changes: 1 addition & 3 deletions pynetbox/ipam.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,11 @@
See the License for the specific language governing permissions and
limitations under the License.
'''
from pynetbox.lib.response import IPRecord, BoolRecord
from pynetbox.lib.response import IPRecord


class IpAddresses(IPRecord):

status = BoolRecord

def __str__(self):
return str(self.address)

Expand Down
2 changes: 1 addition & 1 deletion pynetbox/lib/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from pynetbox.lib.endpoint import Endpoint
from pynetbox.lib.response import Record, IPRecord, BoolRecord
from pynetbox.lib.response import Record, IPRecord
from pynetbox.lib.query import Request, RequestError
165 changes: 77 additions & 88 deletions pynetbox/lib/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,20 @@
from pynetbox.lib.query import Request


def _get_return(lookup, return_fields=['id', 'value', 'nested_return']):

for i in return_fields:
if isinstance(lookup, dict) and lookup.get(i):
return lookup[i]
else:
if hasattr(lookup, i):
return getattr(lookup, i)
if isinstance(lookup, Record):
return str(lookup)
else:
return lookup


class Record(object):
"""Create python objects from netbox API responses.
Expand All @@ -34,11 +48,13 @@ class attribute.
:arg dict api_kwargs: Contains the arguments passed to Api()
when it was instantiated.
"""

url = None
has_details = False

def __init__(self, values, api_kwargs={}, endpoint_meta={}):
self._meta = []
self._full_cache = []
self._index_cache = []
self.api_kwargs = api_kwargs
self.endpoint_meta = endpoint_meta
self.default_ret = Record
Expand All @@ -54,7 +70,6 @@ def __getattr__(self, k):
In order to prevent non-explicit behavior,`k='keys'` is
excluded because casting to dict() calls this attr.
This was done in order to prevent non-explicit API calls.
"""
if self.url:
if self.has_details is False and k != 'keys':
Expand All @@ -66,18 +81,22 @@ def __getattr__(self, k):
raise AttributeError('object has no attribute "{}"'.format(k))

def __iter__(self):
for i in dict(self._meta).keys():
for i in dict(self._full_cache).keys():
cur_attr = getattr(self, i)
if isinstance(cur_attr, (int, str, unicode, type(None), list)):
yield i, cur_attr
else:
if isinstance(cur_attr, Record):
yield i, dict(cur_attr)
else:
yield i, cur_attr

def __getitem__(self, item):
return item

def __str__(self):
return self.name or ''
return (
getattr(self, 'name', None) or
getattr(self, 'label', None) or
''
)

def __repr__(self):
return str(self)
Expand All @@ -88,26 +107,32 @@ def __getstate__(self):
def __setstate__(self, d):
self.__dict__.update(d)

def _add_cache(self, item):
key, value = item
if isinstance(value, Record):
self._full_cache.append((key, dict(value)))
else:
self._full_cache.append((key, value))
self._index_cache.append((key, _get_return(value)))

def _parse_values(self, values):
""" Parses values init arg.
Parses values dict at init and sets object attributes with the
values within.
"""
for k, v in values.items():
self._meta.append((k, v))
if isinstance(v, dict) and k != 'custom_fields':
lookup = getattr(self.__class__, k, None)
if lookup:
setattr(self, k, lookup(v, api_kwargs=self.api_kwargs))
else:
setattr(
self,
k,
self.default_ret(v, api_kwargs=self.api_kwargs)
)
if k != 'custom_fields':
if isinstance(v, dict):
lookup = getattr(self.__class__, k, None)
if lookup:
v = lookup(v, api_kwargs=self.api_kwargs)
else:
v = self.default_ret(v, api_kwargs=self.api_kwargs)
self._add_cache((k, v))
else:
setattr(self, k, v)
self._add_cache((k, v.copy()))
setattr(self, k, v)

def _compare(self):
"""Compares current attributes to values at instantiation.
Expand All @@ -119,18 +144,15 @@ def _compare(self):
attributes as the ones passed to `values`.
"""
init_dict = {}
init_vals = dict(self._meta)
init_vals = dict(self._index_cache)
for i in dict(self):
current_val = init_vals.get(i)
if i != 'custom_fields':
current_val = init_vals.get(i)
if isinstance(current_val, dict):
current_val_id = current_val.get('id')
current_val_value = current_val.get('value')
init_dict.update(
{i: current_val_id or current_val_value}
)
init_dict.update({i: _get_return(current_val)})
else:
init_dict.update({i: current_val})
init_dict.update({i: _get_return(current_val)})
init_dict.update({i: current_val})
if init_dict == self.serialize():
return True
return False
Expand All @@ -155,24 +177,29 @@ def full_details(self):
return True
return False

def serialize(self):
def serialize(self, nested=False):
"""Serializes an object
Pulls all the attributes in an object and creates a dict that
can be turned into the json that netbox is expecting.
If an attribute's value is a ``Record`` or ``IPRecord`` type
it's replaced with the ``id`` field of that object.
:returns: dict of values the NetBox API is expecting.
"""
if nested:
return _get_return(self)

ret = {}
for i in dict(self):
current_val = getattr(self, i)
if i != 'custom_fields':
try:
current_val = current_val.id
except AttributeError:
type_filter = (int, str, unicode, type(None))
if not isinstance(current_val, type_filter):
current_val = current_val.value
if isinstance(current_val, Record):
current_val = getattr(current_val, 'serialize')(nested=True)

if isinstance(current_val, netaddr.ip.IPNetwork):
current_val = str(current_val)

ret.update({i: current_val})
return ret

Expand Down Expand Up @@ -243,15 +270,15 @@ def __init__(self, *args, **kwargs):
self.default_ret = IPRecord

def __iter__(self):
for i in dict(self._meta).keys():
for i in dict(self._full_cache).keys():
cur_attr = getattr(self, i)
if isinstance(cur_attr, (int, str, unicode, type(None))):
yield i, cur_attr
if isinstance(cur_attr, Record):
yield i, dict(cur_attr)
else:
if isinstance(cur_attr, netaddr.IPNetwork):
yield i, str(cur_attr)
else:
yield i, dict(cur_attr)
yield i, cur_attr

def _parse_values(self, values):
""" Parses values init arg. for responses with IPs fields.
Expand All @@ -260,57 +287,19 @@ def _parse_values(self, values):
trys converting them to IPNetwork objects.
"""
for k, v in values.items():
self._meta.append((k, v))
if isinstance(v, dict) and k != 'custom_fields':
lookup = getattr(self.__class__, k, None)
if lookup:
setattr(self, k, lookup(v, api_kwargs=self.api_kwargs))
else:
setattr(
self,
k,
self.default_ret(v, api_kwargs=self.api_kwargs)
)
else:
if k != 'custom_fields':
if isinstance(v, dict):
lookup = getattr(self.__class__, k, None)
if lookup:
v = lookup(v, api_kwargs=self.api_kwargs)
else:
v = self.default_ret(v, api_kwargs=self.api_kwargs)
if isinstance(v, (str, unicode)):
try:
v = netaddr.IPNetwork(v)
except netaddr.AddrFormatError:
pass
setattr(self, k, v)

def serialize(self):
"""Serializes an IPRecord object
Pulls all the attributes in an object and creates a dict that
can be turned into the json that netbox is expecting. Also
accounts for IPNetwork objects present in IPRecord objects.
:returns: dict of values the NetBox API is expecting.
"""
ret = {}
for i in dict(self):
current_val = getattr(self, i)
if i != 'custom_fields':
try:
current_val = current_val.id
except AttributeError:
type_filter = (int, str, unicode, type(None))
if not isinstance(current_val, type_filter):
if isinstance(current_val, netaddr.ip.IPNetwork):
current_val = str(current_val)
else:
current_val = current_val.value
ret.update({i: current_val})
return ret


class BoolRecord(Record):
"""Simple boolean record type to handle NetBox responses with fields
containing json objects that aren't a reference to another endpoint.
E.g. status field on device response.
"""

def __str__(self):
return self.label
self._add_cache((k, v))
else:
self._add_cache((k, v.copy()))
setattr(self, k, v)
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

setup(
name='pynetbox',
version='2.0.5',
version='2.1.0',
description='NetBox API client library',
url='https://github.com/digitalocean/pynetbox',
author='Zach Moody',
Expand Down
52 changes: 41 additions & 11 deletions tests/fixtures/dcim/interface.json
Original file line number Diff line number Diff line change
@@ -1,20 +1,50 @@
{
"id": 1,
"device": {
"id": 1,
"url": "http://localhost:8000/api/dcim/devices/1/",
"name": "test1-edge1",
"display_name": "test1-edge1"
"id": 2,
"url": "http://localhost:8000/api/dcim/devices/2/",
"name": "test1-core1",
"display_name": "test1-core1"
},
"name": "fxp0 (RE0)",
"name": "et-0/0/0",
"form_factor": {
"value": 1000,
"label": "1000BASE-T (1GE)"
"value": 1400,
"label": "QSFP+ (40GE)"
},
"lag": null,
"enabled": true,
"lag": {
"id": 223,
"url": "http://localhost:8000/api/dcim/interfaces/223/",
"name": "ae0"
},
"mtu": null,
"mac_address": null,
"mgmt_only": true,
"mgmt_only": false,
"description": "",
"connection": null,
"connected_interface": null
"is_connected": true,
"interface_connection": {
"interface": {
"id": 37,
"url": "http://localhost:8000/api/dcim/interfaces/37/",
"device": {
"id": 3,
"url": "http://localhost:8000/api/dcim/devices/3/",
"name": "test1-spine1",
"display_name": "test1-spine1"
},
"name": "et-0/1/0",
"form_factor": {
"value": 1400,
"label": "QSFP+ (40GE)"
},
"enabled": true,
"lag": null,
"mtu": null,
"mac_address": null,
"mgmt_only": false,
"description": ""
},
"status": true
},
"circuit_termination": null
}
Loading

0 comments on commit 55f15d2

Please sign in to comment.