From 93c55af38a563466c838bd85cfdaefe18a91a072 Mon Sep 17 00:00:00 2001 From: Zach Moody Date: Tue, 5 Sep 2017 21:55:53 -0500 Subject: [PATCH 1/2] Fixes #9 - interface_connection field handled in interface serializer now. --- pynetbox/dcim.py | 14 ++- pynetbox/ipam.py | 4 +- pynetbox/lib/__init__.py | 2 +- pynetbox/lib/response.py | 165 ++++++++++++++--------------- tests/fixtures/dcim/interface.json | 52 +++++++-- tests/test_dcim.py | 57 +++++++++- 6 files changed, 185 insertions(+), 109 deletions(-) diff --git a/pynetbox/dcim.py b/pynetbox/dcim.py index ebcc5f01..6dc368c5 100644 --- a/pynetbox/dcim.py +++ b/pynetbox/dcim.py @@ -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 @@ -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 @@ -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): diff --git a/pynetbox/ipam.py b/pynetbox/ipam.py index ed56f36e..e00b305f 100644 --- a/pynetbox/ipam.py +++ b/pynetbox/ipam.py @@ -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) diff --git a/pynetbox/lib/__init__.py b/pynetbox/lib/__init__.py index 4c450ddd..81718f10 100644 --- a/pynetbox/lib/__init__.py +++ b/pynetbox/lib/__init__.py @@ -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 diff --git a/pynetbox/lib/response.py b/pynetbox/lib/response.py index 7f801fc3..9aa49eda 100644 --- a/pynetbox/lib/response.py +++ b/pynetbox/lib/response.py @@ -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. @@ -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 @@ -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': @@ -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) @@ -88,6 +107,14 @@ 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. @@ -95,19 +122,17 @@ def _parse_values(self, values): 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. @@ -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 @@ -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 @@ -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. @@ -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) diff --git a/tests/fixtures/dcim/interface.json b/tests/fixtures/dcim/interface.json index 875a84a6..324fa7dc 100644 --- a/tests/fixtures/dcim/interface.json +++ b/tests/fixtures/dcim/interface.json @@ -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 } \ No newline at end of file diff --git a/tests/test_dcim.py b/tests/test_dcim.py index 9efbb2c9..0744ea24 100644 --- a/tests/test_dcim.py +++ b/tests/test_dcim.py @@ -114,6 +114,30 @@ def test_delete(self): headers=AUTH_HEADERS ) + def test_compare(self): + with patch( + 'pynetbox.lib.query.requests.get', + return_value=Response(fixture='{}/{}.json'.format( + self.app, + self.name[:-1] + )) + ): + ret = getattr(nb, self.name).get(1) + self.assertTrue(ret) + self.assertTrue(ret._compare()) + + def test_serialize(self): + with patch( + 'pynetbox.lib.query.requests.get', + return_value=Response(fixture='{}/{}.json'.format( + self.app, + self.name[:-1] + )) + ): + ret = getattr(nb, self.name).get(1) + self.assertTrue(ret) + self.assertTrue(ret.serialize()) + class DeviceTestCase(unittest.TestCase, GenericTest): name = 'devices' @@ -207,13 +231,12 @@ class SiteTestCase(unittest.TestCase, GenericTest): return_value=Response(fixture='dcim/site.json') ) def test_modify(self, mock): - '''Test modifying custom field. + '''Test modifying a custom field. ''' ret = getattr(nb, self.name).get(1) ret.custom_fields['test_custom'] = "Testing" - ret_serialized = ret.serialize() - self.assertTrue(ret_serialized) self.assertFalse(ret._compare()) + self.assertTrue(ret.serialize()) self.assertEqual( ret.custom_fields['test_custom'], 'Testing' ) @@ -236,6 +259,34 @@ def test_create(self, mock): class InterfaceTestCase(unittest.TestCase, GenericTest): name = 'interfaces' + @patch( + 'pynetbox.lib.query.requests.get', + return_value=Response(fixture='dcim/interface.json') + ) + def test_modify(self, mock): + ret = nb.interfaces.get(1) + ret.description = 'Testing' + ret_serialized = ret.serialize() + self.assertTrue(ret) + self.assertEqual(ret_serialized['description'], 'Testing') + self.assertEqual(ret_serialized['form_factor'], 1400) + + @patch( + 'pynetbox.lib.query.requests.get', + return_value=Response(fixture='dcim/interface.json') + ) + def test_modify_connected_iface(self, mock): + new_iface = dict( + status=True, + interface=2 + ) + ret = nb.interfaces.get(1) + ret.interface_connection = new_iface + self.assertEqual( + ret.serialize()['interface_connection'], + new_iface + ) + @patch( 'pynetbox.lib.query.requests.get', side_effect=[ From 39f9aa787a60e76d5faf8e6bf8910bfdb3320c73 Mon Sep 17 00:00:00 2001 From: Zach Moody Date: Wed, 6 Sep 2017 23:09:45 -0500 Subject: [PATCH 2/2] bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 7a7ffb35..c52ad40b 100644 --- a/setup.py +++ b/setup.py @@ -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',