Repository: digitalocean/pynetbox
Pull request #121 (2018-12-04 22:38:58)

4.0 Release


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..2c94d2e
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,22 @@
+---
+
+v4.0.0 (2018-12-04)
+
+## Enhancements
+
+* [#109](https://github.com/digitalocean/pynetbox/issues/109) - Clean up tests a bit.
+* Added cable endpoint support coming in NetBox 2.5.
+* Added some detail to the ValueError raised when `.get()` returns more than one object.
+* Added reserved kwargs to `.get()` and `.filter()` ("id", "pk", "limit", "offset").
+* Made RequestError more verbose when the NetBox API returns a json response. Also added more details when a 404 is returned (e.g. misspelled endpoints). 
+
+## API Changes
+
+* [#22](https://github.com/digitalocean/pynetbox/issues/22) - Switch to using PATCH instead of PUTs for updates. Also added an `.update()` method to Response objects that takes a dictionary to update multiple values on the object.
+* [#24](https://github.com/digitalocean/pynetbox/issues/24) - Add basic support for the `_choices` endpoint on each app by adding a `choices()` method to the App object.
+* [#108](https://github.com/digitalocean/pynetbox/issues/108) - Return `Record` objects from `.create()`.
+
+## Bug Fixes
+
+* [#88](https://github.com/digitalocean/pynetbox/issues/88) - Raise an exception now if `.filter()` is passed no kwargs.
+* [#90](https://github.com/digitalocean/pynetbox/issues/90) - Fixes some miscellaneous issues related to converting certain Record object's fields into netaddr.IPNetwork objects. That feature has been removed and it simply return strings now.
diff --git a/MANIFEST.in b/MANIFEST.in
deleted file mode 100644
index a52a403..0000000
--- a/MANIFEST.in
+++ /dev/null
@@ -1,1 +0,0 @@
-include pynetbox/VERSION
\ No newline at end of file
diff --git a/docs/IPAM.rst b/docs/IPAM.rst
index 352c427..0106add 100644
--- a/docs/IPAM.rst
+++ b/docs/IPAM.rst
@@ -1,5 +1,5 @@
 IPAM
 ========
 
-.. autoclass:: pynetbox.ipam.Prefixes
+.. autoclass:: pynetbox.models.ipam.Prefixes
   :members:
\ No newline at end of file
diff --git a/docs/conf.py b/docs/conf.py
index 76ba515..c1d2718 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -18,8 +18,8 @@
 #
 import os
 import sys
+from pkg_resources import get_distribution
 sys.path.insert(0, os.path.abspath('../'))
-import pynetbox
 
 
 # -- General configuration ------------------------------------------------
@@ -53,11 +53,11 @@ author = u'Zach Moody'
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
 # built documents.
+# The full version, including alpha/beta/rc tags.
+release = get_distribution('pynetbox').version
 #
 # The short X.Y version.
-version = pynetbox.__version__
-# The full version, including alpha/beta/rc tags.
-release = pynetbox.__version__
+version = '.'.join(release.split('.')[:2])
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
diff --git a/docs/endpoint.rst b/docs/endpoint.rst
index bbc0e5d..7b7df4a 100644
--- a/docs/endpoint.rst
+++ b/docs/endpoint.rst
@@ -1,8 +1,8 @@
 Endpoint
 ========
 
-.. autoclass:: pynetbox.lib.endpoint.Endpoint
+.. autoclass:: pynetbox.core.endpoint.Endpoint
   :members:
 
-.. autoclass:: pynetbox.lib.endpoint.DetailEndpoint
+.. autoclass:: pynetbox.core.endpoint.DetailEndpoint
   :members:
\ No newline at end of file
diff --git a/docs/index.rst b/docs/index.rst
index 2c72286..3b3f6bf 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -7,6 +7,9 @@
    request
    IPAM
 
+TL;DR
+=====
+Instantiate the :py:class:`.Api`. Use the methods available on :py:class:`.Endpoint` to return :py:class:`.Record` objects. 
 
 API
 ===
@@ -18,7 +21,7 @@ App
 ===
 
 .. autoclass:: pynetbox.api.App
-
+  :members:
 
 Indices and tables
 ==================
diff --git a/docs/request.rst b/docs/request.rst
index 9140d75..3364d6a 100644
--- a/docs/request.rst
+++ b/docs/request.rst
@@ -1,5 +1,5 @@
 Request
 ========
 
-.. autoclass:: pynetbox.lib.query.RequestError
+.. autoclass:: pynetbox.core.query.RequestError
   :members:
\ No newline at end of file
diff --git a/docs/response.rst b/docs/response.rst
index cc91a50..6ac5c0a 100644
--- a/docs/response.rst
+++ b/docs/response.rst
@@ -1,8 +1,5 @@
 Response
 ========
 
-.. autoclass:: pynetbox.lib.response.Record
-  :members:
-
-.. autoclass:: pynetbox.lib.response.IPRecord
+.. autoclass:: pynetbox.core.response.Record
   :members:
\ No newline at end of file
diff --git a/pynetbox/__init__.py b/pynetbox/__init__.py
index 16a8448..ab70552 100644
--- a/pynetbox/__init__.py
+++ b/pynetbox/__init__.py
@@ -1,7 +1,7 @@
 from pkg_resources import get_distribution, DistributionNotFound
 
+from pynetbox.core.query import RequestError
 from pynetbox.api import Api as api
-from pynetbox.lib import RequestError
 
 try:
     __version__ = get_distribution(__name__).version
diff --git a/pynetbox/api.py b/pynetbox/api.py
index 44d554e..314188b 100644
--- a/pynetbox/api.py
+++ b/pynetbox/api.py
@@ -1,4 +1,4 @@
-'''
+"""
 (c) 2017 DigitalOcean
 
 Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,9 +12,10 @@ distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
-'''
-from pynetbox.lib import Endpoint, Request
-from pynetbox import dcim, ipam, virtualization, circuits
+"""
+from pynetbox.core.endpoint import Endpoint
+from pynetbox.core.query import Request
+from pynetbox.models import dcim, ipam, virtualization, circuits
 
 
 class App(object):
@@ -26,16 +27,30 @@ class App(object):
     :raises: :py:class:`.RequestError`
         if requested endpoint doesn't exist.
     """
-    def __init__(self, app, api_kwargs=None):
-        self.app = app
-        self.api_kwargs = api_kwargs
+
+    def __init__(self, api, name, model=None):
+        self.api = api
+        self.name = name
+        self.model = model
+        self._choices = None
 
     def __getattr__(self, name):
-        return Endpoint(
-            name,
-            api_kwargs=self.api_kwargs,
-            app=self.app
-        )
+        return Endpoint(self.api, self, name, model=self.model)
+
+    def choices(self):
+        """ Returns _choices response from App
+
+        :Returns: Raw response from NetBox's _choices endpoint.
+        """
+        if self._choices:
+            return self._choices
+
+        self._choices = Request(
+            base="{}/{}/_choices/".format(self.api.base_url, self.name),
+            ssl_verify=self.api.ssl_verify,
+        ).get()
+
+        return self._choices
 
 
 class Api(object):
@@ -83,44 +98,44 @@ class Api(object):
     .. _requests: http://docs.python-requests.org/en/master/user/advanced/#ssl-cert-verification
     """  # noqa
 
-    def __init__(self, url, token=None, private_key=None,
-                 private_key_file=None, version=None, ssl_verify=True):
+    def __init__(
+        self,
+        url,
+        token=None,
+        private_key=None,
+        private_key_file=None,
+        ssl_verify=True,
+    ):
         if private_key and private_key_file:
             raise ValueError(
                 '"private_key" and "private_key_file" cannot be used together.'
             )
-        base_url = "{}/api".format(url if url[-1] != '/' else url[:-1])
-
-        self.api_kwargs = {
-            "token": token,
-            "private_key": private_key,
-            "private_key_file": private_key_file,
-            "base_url": base_url,
-            "ssl_verify": ssl_verify,
-        }
-
-        if self.api_kwargs.get('private_key_file'):
-            with open(self.api_kwargs.get('private_key_file'), 'r') as kf:
+        base_url = "{}/api".format(url if url[-1] != "/" else url[:-1])
+        self.token = token
+        self.private_key = private_key
+        self.private_key_file = private_key_file
+        self.base_url = base_url
+        self.ssl_verify = ssl_verify
+        self.session_key = None
+
+        if self.private_key_file:
+            with open(self.private_key_file, "r") as kf:
                 private_key = kf.read()
-                self.api_kwargs.update(private_key=private_key)
-        if not version:
-            version = Request(base=base_url,
-                              ssl_verify=ssl_verify).get_version()
-        self.api_kwargs.update(version=version)
+                self.private_key = private_key
 
         req = Request(
             base=base_url,
             token=token,
             private_key=private_key,
-            ssl_verify=ssl_verify
+            ssl_verify=ssl_verify,
         )
-        if token and private_key:
-            self.api_kwargs.update(session_key=req.get_session_key())
-
-        self.dcim = App(dcim, api_kwargs=self.api_kwargs)
-        self.ipam = App(ipam, api_kwargs=self.api_kwargs)
-        self.circuits = App(circuits, api_kwargs=self.api_kwargs)
-        self.secrets = App('secrets', api_kwargs=self.api_kwargs)
-        self.tenancy = App('tenancy', api_kwargs=self.api_kwargs)
-        self.extras = App('extras', api_kwargs=self.api_kwargs)
-        self.virtualization = App(virtualization, api_kwargs=self.api_kwargs)
+        if self.token and self.private_key:
+            self.session_key = req.get_session_key()
+
+        self.dcim = App(self, "dcim", model=dcim)
+        self.ipam = App(self, "ipam", model=ipam)
+        self.circuits = App(self, "circuits", model=circuits)
+        self.secrets = App(self, "secrets", model=None)
+        self.tenancy = App(self, "tenancy", model=None)
+        self.extras = App(self, "extras", model=None)
+        self.virtualization = App(self, "virtualization", model=virtualization)
diff --git a/pynetbox/core/__init__.py b/pynetbox/core/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/pynetbox/lib/endpoint.py b/pynetbox/core/endpoint.py
similarity index 67%
rename from pynetbox/lib/endpoint.py
rename to pynetbox/core/endpoint.py
index 2b40406..fd9049b 100644
--- a/pynetbox/lib/endpoint.py
+++ b/pynetbox/core/endpoint.py
@@ -1,4 +1,4 @@
-'''
+"""
 (c) 2017 DigitalOcean
 
 Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,11 +12,11 @@ distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
-'''
-from collections import defaultdict
+"""
+from pynetbox.core.query import Request, url_param_builder
+from pynetbox.core.response import Record
 
-from pynetbox.lib.query import Request, url_param_builder
-from pynetbox.lib.response import Record, IPRecord
+RESERVED_KWARGS = ("id", "pk", "limit", "offset")
 
 
 class Endpoint(object):
@@ -26,11 +26,10 @@ class Endpoint(object):
     url to make queries to and the proper Response object to return
     results in.
 
+    :arg obj api: Takes :py:class:`.Api` created at instantiation.
+    :arg obj app: Takes :py:class:`.App`.
     :arg str name: Name of endpoint passed to App().
-    :arg obj app: Application object containing any endpoint-specific
-        classes.
-    :arg dict api_kwargs: Vars passed from Api() that contain
-        variables that are set when Api is instantiated.
+    :arg obj,optional model: Custom model for given app.
 
     .. note::
 
@@ -41,61 +40,47 @@ class Endpoint(object):
 
     """
 
-    def __init__(self, name, app, api_kwargs={}):
-        self.app = app
-        app_module = app
-        if isinstance(app, str):
-            app_name = app_module
-        else:
-            app_name = app_module.__name__.split('.')[-1]
-        self.return_obj = self._lookup_ret_obj(name, app_module, app_name)
-        self.api_kwargs = api_kwargs
-        self.base_url = api_kwargs.get('base_url')
-        self.token = api_kwargs.get('token')
-        self.version = api_kwargs.get('version')
-        self.session_key = api_kwargs.get('session_key')
-        self.ssl_verify = api_kwargs.get('ssl_verify')
-        self.url = '{base_url}/{app}/{endpoint}'.format(
+    def __init__(self, api, app, name, model=None):
+        self.return_obj = self._lookup_ret_obj(name, model)
+        self.api = api
+        self.base_url = api.base_url
+        self.token = api.token
+        self.session_key = api.session_key
+        self.ssl_verify = api.ssl_verify
+        self.url = "{base_url}/{app}/{endpoint}".format(
             base_url=self.base_url,
-            app=app_name,
-            endpoint=name.replace('_', '-'),
+            app=app.name,
+            endpoint=name.replace("_", "-"),
         )
-        self.endpoint_name = name
-        self.meta = dict(url=self.url)
 
-    def _lookup_ret_obj(self, name, app_module, app_name):
+    def _lookup_ret_obj(self, name, model):
         """Loads unique Response objects.
 
         This method loads a unique response object for an endpoint if
         it exists. Otherwise return a generic `Record` object.
 
         :arg str name: Endpoint name.
-        :arg str/obj app_module: The application module that
+        :arg obj model: The application model that
             contains unique Record objects.
-        :arg str app_name: App name.
 
         :Returns: Record (obj)
         """
-        app_return_override = {
-            'ipam': IPRecord,
-        }
-        if app_module:
-            obj_name = name.title().replace('_', '')
-            ret = getattr(
-                app_module,
-                obj_name,
-                app_return_override.get(app_name, Record)
-            )
+        if model:
+            name = name.title().replace("_", "")
+            ret = getattr(model, name, Record)
         else:
             ret = Record
         return ret
 
+    def _response_loader(self, values):
+        return self.return_obj(values, self.api, self)
+
     def all(self):
         """Queries the 'ListView' of a given endpoint.
 
         Returns all objects from an endpoint.
 
-        :Returns: List of instantiated objects.
+        :Returns: List of :py:class:`.Record` objects.
 
         :Examples:
 
@@ -104,20 +89,13 @@ class Endpoint(object):
         >>>
         """
         req = Request(
-            base='{}/'.format(self.url),
+            base="{}/".format(self.url),
             token=self.token,
             session_key=self.session_key,
-            version=self.version,
             ssl_verify=self.ssl_verify,
         )
-        ret_kwargs = dict(
-            api_kwargs=self.api_kwargs,
-            endpoint_meta=self.meta,
-        )
-        return [
-            self.return_obj(i, **ret_kwargs)
-            for i in req.get()
-        ]
+
+        return [self._response_loader(i) for i in req.get()]
 
     def get(self, *args, **kwargs):
         r"""Queries the DetailsView of a given endpoint.
@@ -129,7 +107,7 @@ class Endpoint(object):
             filter(). Any search argument the endpoint accepts can
             be added as a keyword arg.
 
-        :returns: A single instantiated objects.
+        :returns: A single :py:class:`.Record` object.
 
         :raises ValueError: if kwarg search return more than one value.
 
@@ -147,33 +125,34 @@ class Endpoint(object):
         test1-edge1
         >>>
         """
+
         try:
             key = args[0]
         except IndexError:
             key = None
+
         if not key:
             filter_lookup = self.filter(**kwargs)
-            if len(filter_lookup) == 1:
-                return filter_lookup[0]
-            if len(filter_lookup) == 0:
-                return None
-            else:
-                raise ValueError('get() returned more than one result.')
+            if filter_lookup:
+                if len(filter_lookup) > 1:
+                    raise ValueError(
+                        "get() returned more than one result. "
+                        "Check that the kwarg(s) passed are valid for this "
+                        "endpoint or use filter() or all() instead."
+                    )
+                else:
+                    return filter_lookup[0]
+            return filter_lookup
 
         req = Request(
             key=key,
             base=self.url,
             token=self.token,
             session_key=self.session_key,
-            version=self.version,
             ssl_verify=self.ssl_verify,
         )
-        ret_kwargs = dict(
-            api_kwargs=self.api_kwargs,
-            endpoint_meta=self.meta,
-        )
 
-        return self.return_obj(req.get(), **ret_kwargs)
+        return self._response_loader(req.get())
 
     def filter(self, *args, **kwargs):
         r"""Queries the 'ListView' of a given endpoint.
@@ -187,7 +166,7 @@ class Endpoint(object):
         :arg str,optional \**kwargs: Any search argument the
             endpoint accepts can be added as a keyword arg.
 
-        :Returns: A list of instantiated objects.
+        :Returns: A list of :py:class:`.Record` objects.
 
         :Examples:
 
@@ -217,25 +196,28 @@ class Endpoint(object):
         >>>
         """
 
-        if len(args) > 0:
-            kwargs.update({'q': args[0]})
+        if args:
+            kwargs.update({"q": args[0]})
+
+        if not kwargs:
+            raise ValueError(
+                "filter must be passed kwargs. Perhaps use all() instead."
+            )
+        if any(i in RESERVED_KWARGS for i in kwargs):
+            raise ValueError(
+                "A reserved {} kwarg was passed. Please remove it "
+                "try again.".format(RESERVED_KWARGS)
+            )
 
         req = Request(
             filters=kwargs,
             base=self.url,
             token=self.token,
             session_key=self.session_key,
-            version=self.version,
             ssl_verify=self.ssl_verify,
         )
-        ret_kwargs = dict(
-            api_kwargs=self.api_kwargs,
-            endpoint_meta=self.meta,
-        )
-        ret = [
-            self.return_obj(i, **ret_kwargs)
-            for i in req.get()
-        ]
+
+        ret = [self._response_loader(i) for i in req.get()]
         return ret
 
     def create(self, *args, **kwargs):
@@ -255,8 +237,8 @@ class Endpoint(object):
         :arg str \**kwargs: key/value strings representing
             properties on a json object.
 
-        :returns: A response from NetBox as a dictionary or list of
-            dictionaries.
+        :returns: A list or single :py:class:`.Record` object depending
+            on whether a bulk creation was requested.
 
         :Examples:
 
@@ -289,36 +271,37 @@ class Endpoint(object):
         ... ])
         """
 
-        return Request(
+        req = Request(
             base=self.url,
             token=self.token,
             session_key=self.session_key,
-            version=self.version,
             ssl_verify=self.ssl_verify,
-        ).post(args[0] if len(args) > 0 else kwargs)
+        ).post(args[0] if args else kwargs)
+
+        if isinstance(req, list):
+            return [self._response_loader(i) for i in req]
+
+        return self._response_loader(req)
 
 
 class DetailEndpoint(object):
-    '''Enables read/write Operations on detail endpoints.
+    """Enables read/write Operations on detail endpoints.
 
     Endpoints like ``available-ips`` that are detail routes off
     traditional endpoints are handled with this class.
-    '''
+    """
 
     def __init__(self, parent_obj, name, custom_return=None):
         self.parent_obj = parent_obj
         self.custom_return = custom_return
         self.url = "{}/{}/{}/".format(
-            parent_obj.endpoint_meta.get('url'),
-            parent_obj.id,
-            name
+            parent_obj.endpoint.url, parent_obj.id, name
         )
         self.request_kwargs = dict(
             base=self.url,
-            token=parent_obj.api_kwargs.get('token'),
-            session_key=parent_obj.api_kwargs.get('session_key'),
-            version=parent_obj.api_kwargs.get('version'),
-            ssl_verify=parent_obj.api_kwargs.get('ssl_verify'),
+            token=parent_obj.api.token,
+            session_key=parent_obj.api.session_key,
+            ssl_verify=parent_obj.api.ssl_verify,
         )
 
     def list(self, **kwargs):
@@ -335,9 +318,8 @@ class DetailEndpoint(object):
             NetBox.
         """
         if kwargs:
-            self.request_kwargs['base'] = '{}{}'.format(
-                self.url,
-                url_param_builder(kwargs)
+            self.request_kwargs["base"] = "{}{}".format(
+                self.url, url_param_builder(kwargs)
             )
         req = Request(**self.request_kwargs).get()
 
@@ -345,20 +327,16 @@ class DetailEndpoint(object):
             if isinstance(req, list):
                 return [
                     self.custom_return(
-                        i,
-                        api_kwargs=self.parent_obj.api_kwargs,
-                        endpoint_meta=self.parent_obj.endpoint_meta
+                        i, self.parent_obj.api, self.parent_obj.endpoint
                     )
                     for i in req
                 ]
             return self.custom_return(
-                req,
-                api_kwargs=self.parent_obj.api_kwargs,
-                endpoint_meta=self.parent_obj.endpoint_meta
+                req, self.parent_obj.api, self.parent_obj.endpoint
             )
         return req
 
-    def create(self, data={}):
+    def create(self, data):
         """The write operation for a detail endpoint.
 
         Creates objects on a detail endpoint in NetBox.
@@ -375,8 +353,7 @@ class DetailEndpoint(object):
 
 
 class RODetailEndpoint(DetailEndpoint):
-
-    def create(self, data={}):
+    def create(self, data):
         raise NotImplementedError(
-            'Writes are not supported for this endpoint.'
+            "Writes are not supported for this endpoint."
         )
diff --git a/pynetbox/lib/query.py b/pynetbox/core/query.py
similarity index 60%
rename from pynetbox/lib/query.py
rename to pynetbox/core/query.py
index f6b5ccc..b4ad5c5 100644
--- a/pynetbox/lib/query.py
+++ b/pynetbox/core/query.py
@@ -1,4 +1,4 @@
-'''
+"""
 (c) 2017 DigitalOcean
 
 Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,7 +12,7 @@ distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
-'''
+"""
 import json
 from six.moves.urllib.parse import urlencode
 
@@ -20,39 +20,52 @@ import requests
 
 
 def url_param_builder(param_dict):
-    '''Builds url parameters
+    """Builds url parameters
 
     Creates URL paramters (e.g. '.../?xyz=r21&abc=123') from a dict
     passed in param_dict
-    '''
-    return '?{}'.format(urlencode(param_dict))
+    """
+    return "?{}".format(urlencode(param_dict))
 
 
 class RequestError(Exception):
     """Basic Request Exception
 
-    More detailed exception that returns the requests object. Along
-    with some attributes with specific details from the requests
-    object.
+    More detailed exception that returns the original requests object
+    for inspection. Along with some attributes with specific details
+    from the requests object. If return is json we decode and add it
+    to the message.
 
     :Example:
 
     >>> try:
-    ...   nb.dcim.devices.create({'name': 'destined-for-failure'})
+    ...   nb.dcim.devices.create(name="destined-for-failure")
     ... except pynetbox.RequestError as e:
     ...   print(e.error)
-    ...
-    {"device_role":["This field is required."]}
-
-
 
     """
+
     def __init__(self, message):
         req = message
-        message = "The request failed with code {} {}".format(
-            req.status_code,
-            req.reason
-        )
+
+        if req.status_code == 404:
+            message = "The requested url: {} could not be found.".format(
+                req.url
+            )
+        else:
+            try:
+                message = "The request failed with code {} {}: {}".format(
+                    req.status_code, req.reason, req.json()
+                )
+            except ValueError:
+                message = (
+                    "The request failed with code {} {} but more specific "
+                    "details were not returned in json. Check the NetBox Logs "
+                    "or investigate this exception's error attribute.".format(
+                        req.status_code, req.reason
+                    )
+                )
+
         super(RequestError, self).__init__(message)
         self.req = req
         self.request_body = req.request.body
@@ -75,9 +88,16 @@ class Request(object):
         string.
     """
 
-    def __init__(self, base=None, filters=None, key=None, token=None,
-                 private_key=None, version=None, session_key=None,
-                 ssl_verify=True):
+    def __init__(
+        self,
+        base=None,
+        filters=None,
+        key=None,
+        token=None,
+        private_key=None,
+        session_key=None,
+        ssl_verify=True,
+    ):
         """
         Instantiates a new Request object
 
@@ -96,24 +116,9 @@ class Request(object):
         self.key = key
         self.token = token
         self.private_key = private_key
-        self.version = version
         self.session_key = session_key
         self.ssl_verify = ssl_verify
 
-    def get_version(self):
-        """Query the netbox API for its API-Version.
-
-        Queries the API root for the *API-Verion* header.
-
-        :returns: String containing version.
-        """
-        ret = requests.get(
-            "{}/".format(self.base),
-            verify=self.ssl_verify
-        ).headers.get('API-Version', '1.0')
-        setattr(self, 'version', ret)
-        return ret
-
     def get_session_key(self):
         """Requests session key
 
@@ -123,19 +128,17 @@ class Request(object):
         :Returns: String containing session key.
         """
         req = requests.post(
-            '{}/secrets/get-session-key/?preserve_key=True'.format(self.base),
+            "{}/secrets/get-session-key/?preserve_key=True".format(self.base),
             headers={
-                'accept': 'application/json',
-                'authorization': 'Token {}'.format(self.token),
-                'Content-Type': 'application/x-www-form-urlencoded',
+                "accept": "application/json",
+                "authorization": "Token {}".format(self.token),
+                "Content-Type": "application/x-www-form-urlencoded",
             },
-            data=urlencode({
-                'private_key': self.private_key.strip('\n')
-            }),
-            verify=self.ssl_verify
+            data=urlencode({"private_key": self.private_key.strip("\n")}),
+            verify=self.ssl_verify,
         )
         if req.ok:
-            return req.json()['session_key']
+            return req.json()["session_key"]
         else:
             raise RequestError(req)
 
@@ -147,8 +150,9 @@ class Request(object):
 
         :Returns: String of URL.
         """
-        def construct_url(input):
-            for k, v in input.items():
+
+        def construct_url(params):
+            for k, v in params.items():
                 if isinstance(v, list):
                     for i in v:
                         yield urlencode({k: i})
@@ -156,25 +160,22 @@ class Request(object):
                     yield urlencode({k: v})
 
         if self.key:
-            return '{}/{key}/'.format(
-                self.base,
-                key=self.key
-            )
+            return "{}/{key}/".format(self.base, key=self.key)
         if self.filters:
-            if self.base[-1] == '/':
-                return '{}?{query}'.format(
+            if self.base[-1] == "/":
+                return "{}?{query}".format(
                     self.base,
-                    query="&".join(list(construct_url(self.filters)))
+                    query="&".join(list(construct_url(self.filters))),
                 )
-            elif '/?' in self.base:
-                return '{}&{query}'.format(
+            elif "/?" in self.base:
+                return "{}&{query}".format(
                     self.base,
-                    query="&".join(list(construct_url(self.filters)))
+                    query="&".join(list(construct_url(self.filters))),
                 )
             else:
-                return '{}/?{query}'.format(
+                return "{}/?{query}".format(
                     self.base,
-                    query="&".join(list(construct_url(self.filters)))
+                    query="&".join(list(construct_url(self.filters))),
                 )
         else:
             return self.base
@@ -182,7 +183,7 @@ class Request(object):
     def normalize_url(self, url):
         """ Builds a url for POST actions.
         """
-        if url[-1] != '/':
+        if url[-1] != "/":
             return "{}/".format(url)
 
         return url
@@ -198,17 +199,11 @@ class Request(object):
         :Returns: List of `Response` objects returned from the
             endpoint.
         """
-        headers = {
-            'accept': 'application/json; version={};'.format(
-                self.version or self.get_version()
-            )
-        }
+        headers = {"accept": "application/json;"}
         if self.token:
-            headers.update(authorization='Token {}'.format(self.token))
+            headers.update(authorization="Token {}".format(self.token))
         if self.session_key:
-            headers.update(
-                {'X-Session-Key': self.session_key}
-            )
+            headers.update({"X-Session-Key": self.session_key})
 
         def make_request(url):
 
@@ -220,19 +215,23 @@ class Request(object):
 
         def req_all(url):
             req = make_request(url)
-            if isinstance(req, dict) and req.get('results') is not None:
-                ret = req['results']
+            if isinstance(req, dict) and req.get("results") is not None:
+                ret = req["results"]
                 first_run = True
-                while req['next']:
-                    next_url = "{}{}limit={}&offset={}".format(
-                        self.url,
-                        '&' if self.url[-1] != '/' else '?',
-                        req['count'],
-                        len(req['results'])
-                    ) if first_run else req['next']
+                while req["next"]:
+                    next_url = (
+                        "{}{}limit={}&offset={}".format(
+                            self.url,
+                            "&" if self.url[-1] != "/" else "?",
+                            req["count"],
+                            len(req["results"]),
+                        )
+                        if first_run
+                        else req["next"]
+                    )
                     req = make_request(next_url)
                     first_run = False
-                    ret.extend(req['results'])
+                    ret.extend(req["results"])
                 return ret
             else:
                 return req
@@ -251,19 +250,17 @@ class Request(object):
         :returns: Dict containing the response from NetBox's API.
         """
         headers = {
-            'Content-Type': 'application/json; version={};'.format(
-                self.version or self.get_version()
-            ),
-            'authorization': 'Token {}'.format(self.token),
+            "Content-Type": "application/json;",
+            "authorization": "Token {}".format(self.token),
         }
         if self.session_key:
-            headers.update(
-                {'X-Session-Key': self.session_key}
-            )
-        req = requests.put(self.url,
-                           headers=headers,
-                           data=json.dumps(data),
-                           verify=self.ssl_verify)
+            headers.update({"X-Session-Key": self.session_key})
+        req = requests.put(
+            self.url,
+            headers=headers,
+            data=json.dumps(data),
+            verify=self.ssl_verify,
+        )
         if req.ok:
             return req.json()
         else:
@@ -281,20 +278,16 @@ class Request(object):
         :Returns: Dict containing the response from NetBox's API.
         """
         headers = {
-            'Content-Type': 'application/json; version={};'.format(
-                self.version or self.get_version()
-            ),
-            'authorization': 'Token {}'.format(self.token),
+            "Content-Type": "application/json;",
+            "authorization": "Token {}".format(self.token),
         }
         if self.session_key:
-            headers.update(
-                {'X-Session-Key': self.session_key}
-            )
+            headers.update({"X-Session-Key": self.session_key})
         req = requests.post(
             self.normalize_url(self.url),
             headers=headers,
             data=json.dumps(data),
-            verify=self.ssl_verify
+            verify=self.ssl_verify,
         )
         if req.ok:
             return req.json()
@@ -304,8 +297,7 @@ class Request(object):
     def delete(self):
         """Makes DELETE request.
 
-        Makes a DELETE request to NetBox's API. Adds the session key to
-        headers if the `private_key` attribute was populated.
+        Makes a DELETE request to NetBox's API.
 
         Returns:
             True if successful.
@@ -314,17 +306,40 @@ class Request(object):
             RequestError if req.ok doesn't return True.
         """
         headers = {
-            'accept': 'application/json; version={};'.format(
-                self.version or self.get_version()
-            ),
-            'authorization': 'Token {}'.format(self.token),
+            "accept": "application/json;",
+            "authorization": "Token {}".format(self.token),
         }
         req = requests.delete(
-            "{}".format(self.url),
-            headers=headers,
-            verify=self.ssl_verify
+            "{}".format(self.url), headers=headers, verify=self.ssl_verify
         )
         if req.ok:
             return True
         else:
             raise RequestError(req)
+
+    def patch(self, data):
+        """Makes PATCH request.
+
+        Makes a PATCH request to NetBox's API.
+
+        :param data: (dict) Contains a dict that will be turned into a
+            json object and sent to the API.
+        :raises: RequestError if req.ok returns false.
+        :returns: Dict containing the response from NetBox's API.
+        """
+        headers = {
+            "Content-Type": "application/json;",
+            "authorization": "Token {}".format(self.token),
+        }
+        if self.session_key:
+            headers.update({"X-Session-Key": self.session_key})
+        req = requests.patch(
+            self.url,
+            headers=headers,
+            data=json.dumps(data),
+            verify=self.ssl_verify,
+        )
+        if req.ok:
+            return req.json()
+        else:
+            raise RequestError(req)
diff --git a/pynetbox/lib/response.py b/pynetbox/core/response.py
similarity index 51%
rename from pynetbox/lib/response.py
rename to pynetbox/core/response.py
index 895e4e0..2e48422 100644
--- a/pynetbox/lib/response.py
+++ b/pynetbox/core/response.py
@@ -1,4 +1,4 @@
-'''
+"""
 (c) 2017 DigitalOcean
 
 Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,19 +12,17 @@ distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
-'''
-import netaddr
-import six
-
-from pynetbox.lib.query import Request
+"""
+from pynetbox.core.query import Request
+from pynetbox.core.util import Hashabledict
 
 # List of fields that contain a dict but are not to be converted into
 # Record objects.
-JSON_FIELDS = ('custom_fields', 'data', 'config_context')
+JSON_FIELDS = ("custom_fields", "data", "config_context")
 
 
 def get_return(lookup, return_fields=None):
-    '''Returns simple representations for items passed to lookup.
+    """Returns simple representations for items passed to lookup.
 
     Used to return a "simple" representation of objects and collections
     sent to it via lookup. If lookup is an IPNetwork object immediately
@@ -36,12 +34,9 @@ def get_return(lookup, return_fields=None):
 
     :arg list,optional return_fields: A list of fields to reference when
         calling values on lookup.
-    '''
-
-    if isinstance(lookup, netaddr.IPNetwork):
-        return str(lookup)
+    """
 
-    for i in return_fields or ['id', 'value', 'nested_return']:
+    for i in return_fields or ["id", "value", "nested_return"]:
         if isinstance(lookup, dict) and lookup.get(i):
             return lookup[i]
         else:
@@ -56,7 +51,7 @@ def get_return(lookup, return_fields=None):
 
 def flatten_custom(custom_dict):
     return {
-        k: v if not isinstance(v, dict) else v['value']
+        k: v if not isinstance(v, dict) else v["value"]
         for k, v in custom_dict.items()
     }
 
@@ -64,28 +59,102 @@ def flatten_custom(custom_dict):
 class Record(object):
     """Create python objects from netbox API responses.
 
-        Iterates over `values` and checks if the `key` is defined as a
-        a class attribute point to another Record object. If so, it
-        then takes `value` and instantiates the referenced object
-        creating a nested object of whatever type is requested in the
-        class attribute.
-
-        If `key` doesn't match a class attribute an instance attribute
-        is set containing `value`.
+    Creates an object from a NetBox response passed as `values`.
+    Nested dicts that represent other endpoints are also turned
+    into Record objects. All fields are then assigned to the
+    object's attributes. If a missing attr is requested
+    (e.g. requesting a field that's only present on a full response on
+    a Record made from a nested response) the pynetbox will make a
+    request for the full object and return the requsted value.
+
+    :examples:
+
+    Default representation of the object is usually its name
+
+    >>> x = nb.dcim.devices.get(1)
+    >>> x
+    test1-switch1
+    >>>
+
+    Querying a string field.
+
+    >>> x = nb.dcim.devices.get(1)
+    >>> x.serial
+    'ABC123'
+    >>>
+
+    Querying a field on a nested object.
+
+    >>> x = nb.dcim.devices.get(1)
+    >>> x.device_type.model
+    'QFX5100-24Q'
+    >>>
+
+    Casting the object as a dictionary.
+
+    >>> from pprint import pprint
+    >>> pprint(dict(x))
+    {'asset_tag': None,
+     'cluster': None,
+     'comments': '',
+     'config_context': {},
+     'created': '2018-04-01',
+     'custom_fields': {},
+     'device_role': {'id': 1,
+                     'name': 'Test Switch',
+                     'slug': 'test-switch',
+                     'url': 'http://localhost:8000/api/dcim/device-roles/1/'},
+     'device_type': {...},
+     'display_name': 'test1-switch1',
+     'face': {'label': 'Rear', 'value': 1},
+     'id': 1,
+     'name': 'test1-switch1',
+     'parent_device': None,
+     'platform': {...},
+     'position': 1,
+     'primary_ip': {'address': '192.0.2.1/24',
+                    'family': 4,
+                    'id': 1,
+                    'url': 'http://localhost:8000/api/ipam/ip-addresses/1/'},
+     'primary_ip4': {...},
+     'primary_ip6': None,
+     'rack': {'display_name': 'Test Rack',
+              'id': 1,
+              'name': 'Test Rack',
+              'url': 'http://localhost:8000/api/dcim/racks/1/'},
+     'serial': 'ABC123',
+     'site': {'id': 1,
+              'name': 'TEST',
+              'slug': 'TEST',
+              'url': 'http://localhost:8000/api/dcim/sites/1/'},
+     'status': {'label': 'Active', 'value': 1},
+     'tags': [],
+     'tenant': None,
+     'vc_position': None,
+     'vc_priority': None,
+     'virtual_chassis': None}
+     >>>
+
+     Iterating over a Record object.
+
+    >>> for i in x:
+    ...  print(i)
+    ...
+    ('id', 1)
+    ('name', 'test1-switch1')
+    ('display_name', 'test1-switch1')
+    >>>
 
-        :arg dict values: The response from the netbox api.
-        :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={}):
+    def __init__(self, values, api, endpoint):
+        self.has_details = False
         self._full_cache = []
-        self._index_cache = []
-        self.api_kwargs = api_kwargs
-        self.endpoint_meta = endpoint_meta
+        self._init_cache = []
+        self.api = api
+        self.endpoint = endpoint
         self.default_ret = Record
 
         if values:
@@ -101,7 +170,7 @@ class Record(object):
         excluded because casting to dict() calls this attr.
         """
         if self.url:
-            if self.has_details is False and k != 'keys':
+            if self.has_details is False and k != "keys":
                 if self.full_details():
                     ret = getattr(self, k, None)
                     if ret or hasattr(self, k):
@@ -110,7 +179,7 @@ class Record(object):
         raise AttributeError('object has no attribute "{}"'.format(k))
 
     def __iter__(self):
-        for i in dict(self._full_cache).keys():
+        for i in dict(self._init_cache):
             cur_attr = getattr(self, i)
             if isinstance(cur_attr, Record):
                 yield i, dict(cur_attr)
@@ -122,9 +191,7 @@ class Record(object):
 
     def __str__(self):
         return (
-            getattr(self, 'name', None) or
-            getattr(self, 'label', None) or
-            ''
+            getattr(self, "name", None) or getattr(self, "label", None) or ""
         )
 
     def __repr__(self):
@@ -138,11 +205,7 @@ class Record(object):
 
     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)))
+        self._init_cache.append((key, get_return(value)))
 
     def _parse_values(self, values):
         """ Parses values init arg.
@@ -153,7 +216,7 @@ class Record(object):
 
         def list_parser(list_item):
             if isinstance(list_item, dict):
-                return self.default_ret(list_item, api_kwargs=self.api_kwargs)
+                return self.default_ret(list_item, self.api, self.endpoint)
             return list_item
 
         for k, v in values.items():
@@ -162,9 +225,9 @@ class Record(object):
                 if isinstance(v, dict):
                     lookup = getattr(self.__class__, k, None)
                     if lookup:
-                        v = lookup(v, api_kwargs=self.api_kwargs)
+                        v = lookup(v, self.api, self.endpoint)
                     else:
-                        v = self.default_ret(v, api_kwargs=self.api_kwargs)
+                        v = self.default_ret(v, self.api, self.endpoint)
                     self._add_cache((k, v))
 
                 elif isinstance(v, list):
@@ -187,16 +250,8 @@ class Record(object):
             Boolean value, True indicates current instance has the same
             attributes as the ones passed to `values`.
         """
-        init_dict = {}
-        init_vals = dict(self._index_cache)
-        for i in dict(self):
-            current_val = init_vals.get(i)
-            if i == 'custom_fields':
-                init_dict[i] = flatten_custom(current_val)
-            else:
-                init_dict[i] = get_return(current_val)
 
-        if init_dict == self.serialize():
+        if self.serialize(init=True) == self.serialize():
             return True
         return False
 
@@ -213,46 +268,50 @@ class Record(object):
         if self.url:
             req = Request(
                 base=self.url,
-                token=self.api_kwargs.get('token'),
-                session_key=self.api_kwargs.get('session_key'),
-                version=self.api_kwargs.get('version'),
-                ssl_verify=self.api_kwargs.get('ssl_verify')
+                token=self.api.token,
+                session_key=self.api.session_key,
+                ssl_verify=self.api.ssl_verify,
             )
             self._parse_values(req.get())
             self.has_details = True
             return True
         return False
 
-    def serialize(self, nested=False):
+    def serialize(self, nested=False, init=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.
+        If an attribute's value is a ``Record`` type it's replaced with
+        the ``id`` field of that object.
+
+        .. note::
 
-        :returns: dict of values the NetBox API is expecting.
+            Using this to get a dictionary representation of the record
+            is discouraged. It's probably better to cast to dict()
+            instead. See Record docstring for example.
+
+        :returns: dict.
         """
         if nested:
             return get_return(self)
 
+        if init:
+            init_vals = dict(self._init_cache)
+
         ret = {}
         for i in dict(self):
-            current_val = getattr(self, i)
-            if i == 'custom_fields':
+            current_val = getattr(self, i) if not init else init_vals.get(i)
+            if i == "custom_fields":
                 ret[i] = flatten_custom(current_val)
-            elif i == 'tags':
+            elif i == "tags":
                 ret[i] = list(set(current_val))
             else:
                 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)
+                    current_val = getattr(current_val, "serialize")(
+                        nested=True
+                    )
 
                 if isinstance(current_val, list):
                     current_val = [
@@ -262,13 +321,29 @@ class Record(object):
                 ret[i] = current_val
         return ret
 
+    def _diff(self):
+        def fmt_dict(k, v):
+            if isinstance(v, dict):
+                return k, Hashabledict(v)
+            if isinstance(v, list):
+                return k, "".join(v)
+            return k, v
+
+        current = Hashabledict(
+            {fmt_dict(k, v) for k, v in self.serialize().items()}
+        )
+        init = Hashabledict(
+            {fmt_dict(k, v) for k, v in self.serialize(init=True).items()}
+        )
+        return set([i[0] for i in set(current.items()) ^ set(init.items())])
+
     def save(self):
         """Saves changes to an existing object.
 
         Runs self.serialize() and checks that it doesn't match
         self._compare(). If not create a Request object and run .put()
 
-        :returns: True if PUT request was successful.
+        :returns: True if PATCH request was successful.
         :example:
 
         >>> x = nb.dcim.devices.get(name='test1-a3-tor1b')
@@ -280,19 +355,45 @@ class Record(object):
         >>>
         """
         if self.id:
-            if not self._compare():
+            diff = self._diff()
+            if diff:
+                serialized = self.serialize()
                 req = Request(
                     key=self.id,
-                    base=self.endpoint_meta.get('url'),
-                    token=self.api_kwargs.get('token'),
-                    session_key=self.api_kwargs.get('session_key'),
-                    version=self.api_kwargs.get('version'),
-                    ssl_verify=self.api_kwargs.get('ssl_verify')
+                    base=self.endpoint.url,
+                    token=self.api.token,
+                    session_key=self.api.session_key,
+                    ssl_verify=self.api.ssl_verify,
                 )
-                if req.put(self.serialize()):
+                if req.patch({i: serialized[i] for i in diff}):
                     return True
-            else:
-                return False
+
+        return False
+
+    def update(self, data):
+        """Update an object with a dictionary.
+
+        Accepts a dict and uses it to update the record and call save().
+        For nested and choice fields you'd pass an int the same as
+        if you were modifying the attribute and calling save().
+
+        :arg dict data: Dictionary containing the k/v to update the
+            record object with.
+        :returns: True if PATCH request was successful.
+        :example:
+
+        >>> x = nb.dcim.devices.get(1)
+        >>> x.update({
+        ...   "name": "test-switch2",
+        ...   "serial": "ABC321",
+        ... })
+        True
+
+        """
+
+        for k, v in data.items():
+            setattr(self, k, v)
+        return self.save()
 
     def delete(self):
         """Deletes an existing object.
@@ -307,74 +408,9 @@ class Record(object):
         """
         req = Request(
             key=self.id,
-            base=self.endpoint_meta.get('url'),
-            token=self.api_kwargs.get('token'),
-            session_key=self.api_kwargs.get('session_key'),
-            version=self.api_kwargs.get('version'),
-            ssl_verify=self.api_kwargs.get('ssl_verify')
+            base=self.endpoint.url,
+            token=self.api.token,
+            session_key=self.api.session_key,
+            ssl_verify=self.api.ssl_verify,
         )
-        if req.delete():
-            return True
-        else:
-            return False
-
-
-class IPRecord(Record):
-    """IP-specific Record for IPAM responses.
-
-    Extends ``Record`` objects to handle replacing ip4/6 strings with
-    instances of ``netaddr.IPNetworks`` instead.
-    """
-
-    def __init__(self, *args, **kwargs):
-        super(IPRecord, self).__init__(*args, **kwargs)
-        self.default_ret = IPRecord
-
-    def __iter__(self):
-        for i in dict(self._full_cache).keys():
-            cur_attr = getattr(self, i)
-            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, cur_attr
-
-    def _parse_values(self, values):
-        """ Parses values init arg. for responses with IPs fields.
-
-        Similar parser as parent, but takes str & unicode fields and
-        trys converting them to IPNetwork objects.
-        """
-
-        def list_parser(list_item):
-            if isinstance(list_item, dict):
-                return self.default_ret(list_item, api_kwargs=self.api_kwargs)
-            return list_item
-
-        for k, v in values.items():
-
-            if k not in JSON_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))
-
-                elif isinstance(v, list):
-                    v = [list_parser(i) for i in v]
-                    to_cache = list(v)
-                    self._add_cache((k, to_cache))
-
-                if isinstance(v, six.string_types):
-                    try:
-                        v = netaddr.IPNetwork(v)
-                        self._add_cache((k, v))
-                    except (netaddr.AddrFormatError, ValueError):
-                        self._add_cache((k, v))
-            else:
-                self._add_cache((k, v.copy()))
-            setattr(self, k, v)
+        return True if req.delete() else False
diff --git a/pynetbox/core/util.py b/pynetbox/core/util.py
new file mode 100644
index 0000000..5fd8e93
--- /dev/null
+++ b/pynetbox/core/util.py
@@ -0,0 +1,3 @@
+class Hashabledict(dict):
+    def __hash__(self):
+        return hash(frozenset(self))
diff --git a/pynetbox/lib/__init__.py b/pynetbox/lib/__init__.py
deleted file mode 100644
index 81718f1..0000000
--- a/pynetbox/lib/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from pynetbox.lib.endpoint import Endpoint
-from pynetbox.lib.response import Record, IPRecord
-from pynetbox.lib.query import Request, RequestError
diff --git a/pynetbox/models/__init__.py b/pynetbox/models/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/pynetbox/circuits.py b/pynetbox/models/circuits.py
similarity index 93%
rename from pynetbox/circuits.py
rename to pynetbox/models/circuits.py
index 6072c81..0e97cc7 100644
--- a/pynetbox/circuits.py
+++ b/pynetbox/models/circuits.py
@@ -1,4 +1,4 @@
-'''
+"""
 (c) 2017 DigitalOcean
 
 Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,17 +12,15 @@ distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
-'''
-from pynetbox.lib.response import Record
+"""
+from pynetbox.core.response import Record
 
 
 class Circuits(Record):
-
     def __str__(self):
         return self.cid
 
 
 class CircuitTerminations(Record):
-
     def __str__(self):
         return self.circuit.cid
diff --git a/pynetbox/dcim.py b/pynetbox/models/dcim.py
similarity index 74%
rename from pynetbox/dcim.py
rename to pynetbox/models/dcim.py
index 58bc2a6..e151ce5 100644
--- a/pynetbox/dcim.py
+++ b/pynetbox/models/dcim.py
@@ -1,4 +1,4 @@
-'''
+"""
 (c) 2017 DigitalOcean
 
 Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +12,14 @@ distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
-'''
-from pynetbox.lib.response import Record
-from pynetbox.lib.endpoint import RODetailEndpoint
-from pynetbox.ipam import IpAddresses
+"""
+from pynetbox.core.response import Record
+from pynetbox.core.endpoint import RODetailEndpoint
+from pynetbox.models.ipam import IpAddresses
+from pynetbox.models.circuits import Circuits
 
 
 class DeviceTypes(Record):
-
     def __str__(self):
         return self.model
 
@@ -38,6 +38,7 @@ class Devices(Record):
                 `device_type` field's value from the API response and
                 return an initialized DeviceType object
     """
+
     has_details = True
     device_type = DeviceTypes
     primary_ip = IpAddresses
@@ -60,33 +61,34 @@ class Devices(Record):
         {"get_facts": {"interface_list": ["ge-0/0/0"]}}
 
         """
-        return RODetailEndpoint(self, 'napalm')
+        return RODetailEndpoint(self, "napalm")
 
 
 class InterfaceConnections(Record):
-
     def __str__(self):
         return self.interface_a.name
 
 
 class InterfaceConnection(Record):
-
     def __str__(self):
         return self.interface.name
 
 
+class ConnectedEndpoint(Record):
+    device = Devices
+
+
 class Interfaces(Record):
     interface_connection = InterfaceConnection
+    connected_endpoint = ConnectedEndpoint
 
 
 class RackReservations(Record):
-
     def __str__(self):
         return self.description
 
 
 class VirtualChassis(Record):
-
     def __str__(self):
         return self.master.display_name
 
@@ -97,7 +99,6 @@ class RUs(Record):
 
 
 class Racks(Record):
-
     @property
     def units(self):
         """ Represents the ``units`` detail endpoint.
@@ -114,4 +115,26 @@ class Racks(Record):
         {"get_facts": {"interface_list": ["ge-0/0/0"]}}
 
         """
-        return RODetailEndpoint(self, 'units', custom_return=RUs)
+        return RODetailEndpoint(self, "units", custom_return=RUs)
+
+
+class Termination(Record):
+    def __str__(self):
+        # hacky check to see if we're a circuit termination to
+        # avoid another call to NetBox because of a non-existent attr
+        # in self.name
+        if "circuit" in str(self.url):
+            return self.circuit.cid
+
+        return self.name
+
+    device = Devices
+    circuit = Circuits
+
+
+class Cables(Record):
+    def __str__(self):
+        return "{} <> {}".format(self.termination_a, self.termination_b)
+
+    termination_a = Termination
+    termination_b = Termination
diff --git a/pynetbox/ipam.py b/pynetbox/models/ipam.py
similarity index 86%
rename from pynetbox/ipam.py
rename to pynetbox/models/ipam.py
index 108dab7..0322c9a 100644
--- a/pynetbox/ipam.py
+++ b/pynetbox/models/ipam.py
@@ -1,4 +1,4 @@
-'''
+"""
 (c) 2017 DigitalOcean
 
 Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,19 +12,17 @@ distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
-'''
-from pynetbox.lib.response import IPRecord
-from pynetbox.lib.endpoint import DetailEndpoint
+"""
+from pynetbox.core.response import Record
+from pynetbox.core.endpoint import DetailEndpoint
 
 
-class IpAddresses(IPRecord):
-
+class IpAddresses(Record):
     def __str__(self):
         return str(self.address)
 
 
-class Prefixes(IPRecord):
-
+class Prefixes(Record):
     def __str__(self):
         return str(self.prefix)
 
@@ -57,11 +55,11 @@ class Prefixes(IPRecord):
         >>> len(create)
         2
         """
-        return DetailEndpoint(self, 'available-ips')
+        return DetailEndpoint(self, "available-ips")
 
     @property
     def available_prefixes(self):
-        ''' Represents the ``available-prefixes`` detail endpoint.
+        """ Represents the ``available-prefixes`` detail endpoint.
 
         Returns a DetailEndpoint object that is the interface for
         viewing and creating prefixes inside a parent prefix.
@@ -87,11 +85,10 @@ class Prefixes(IPRecord):
         >>> new_prefix['prefix']
         u'10.1.1.56/29'
 
-        '''
-        return DetailEndpoint(self, 'available-prefixes')
-
+        """
+        return DetailEndpoint(self, "available-prefixes")
 
-class Aggregates(IPRecord):
 
+class Aggregates(Record):
     def __str__(self):
         return str(self.prefix)
diff --git a/pynetbox/virtualization.py b/pynetbox/models/virtualization.py
similarity index 87%
rename from pynetbox/virtualization.py
rename to pynetbox/models/virtualization.py
index e2f5328..d00d27b 100644
--- a/pynetbox/virtualization.py
+++ b/pynetbox/models/virtualization.py
@@ -1,4 +1,4 @@
-'''
+"""
 (c) 2017 DigitalOcean
 
 Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,9 +12,9 @@ distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
-'''
-from pynetbox.lib.response import Record
-from pynetbox.ipam import IpAddresses
+"""
+from pynetbox.core.response import Record
+from pynetbox.models.ipam import IpAddresses
 
 
 class VirtualMachines(Record):
diff --git a/requirements.txt b/requirements.txt
index bb1da21..4c1a0d4 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,2 @@
-netaddr==0.*
 requests>=2.20.0,<3.0
-six==1.*
+six==1.*
\ No newline at end of file
diff --git a/setup.py b/setup.py
index 9b1b7db..f01b545 100644
--- a/setup.py
+++ b/setup.py
@@ -1,4 +1,4 @@
-from setuptools import setup
+from setuptools import setup, find_packages
 
 setup(
     name='pynetbox',
@@ -10,13 +10,9 @@ setup(
     include_package_data=True,
     use_scm_version=True,
     setup_requires=['setuptools_scm'],
-    packages=[
-        'pynetbox',
-        'pynetbox.lib'
-    ],
+    packages=find_packages(),
     install_requires=[
-        'netaddr==0.*',
-        'requests==2.*',
+        'requests>=2.20.0,<3.0',
         'six==1.*',
     ],
     zip_safe=False,


Test patch:
diff --git a/tests/fixtures/dcim/cable.json b/tests/fixtures/dcim/cable.json
new file mode 100644
index 0000000..87455f6
--- /dev/null
+++ b/tests/fixtures/dcim/cable.json
@@ -0,0 +1,40 @@
+{
+    "id": 1,
+    "termination_a_type": "dcim.consoleport",
+    "termination_a_id": 1,
+    "termination_a": {
+        "id": 1,
+        "url": "http://localhost:8000/api/dcim/console-ports/1/",
+        "device": {
+            "id": 1,
+            "url": "http://localhost:8000/api/dcim/devices/1/",
+            "name": "tst1-test1",
+            "display_name": "tst1-test1"
+        },
+        "name": "Console",
+        "cable": 1
+    },
+    "termination_b_type": "dcim.consoleserverport",
+    "termination_b_id": 2,
+    "termination_b": {
+        "id": 2,
+        "url": "http://localhost:8000/api/dcim/console-server-ports/2/",
+        "device": {
+            "id": 2,
+            "url": "http://localhost:8000/api/dcim/devices/2/",
+            "name": "tst1-test2",
+            "display_name": "tst1-test2"
+        },
+        "name": "Port 10",
+        "cable": 1
+    },
+    "type": null,
+    "status": {
+        "value": true,
+        "label": "Connected"
+    },
+    "label": "",
+    "color": "",
+    "length": null,
+    "length_unit": null
+}
\ No newline at end of file
diff --git a/tests/fixtures/dcim/cables.json b/tests/fixtures/dcim/cables.json
new file mode 100644
index 0000000..67c7b80
--- /dev/null
+++ b/tests/fixtures/dcim/cables.json
@@ -0,0 +1,127 @@
+{
+    "count": 3,
+    "next": null,
+    "previous": null,
+    "results": [
+        {
+            "id": 1,
+            "termination_a_type": "dcim.consoleport",
+            "termination_a_id": 1,
+            "termination_a": {
+                "id": 1,
+                "url": "http://localhost:8000/api/dcim/console-ports/1/",
+                "device": {
+                    "id": 1,
+                    "url": "http://localhost:8000/api/dcim/devices/1/",
+                    "name": "tst1-test1",
+                    "display_name": "tst1-test1"
+                },
+                "name": "Console",
+                "cable": 1
+            },
+            "termination_b_type": "dcim.consoleserverport",
+            "termination_b_id": 2,
+            "termination_b": {
+                "id": 2,
+                "url": "http://localhost:8000/api/dcim/console-server-ports/2/",
+                "device": {
+                    "id": 2,
+                    "url": "http://localhost:8000/api/dcim/devices/2/",
+                    "name": "tst1-test2",
+                    "display_name": "tst1-test2"
+                },
+                "name": "Port 10",
+                "cable": 1
+            },
+            "type": null,
+            "status": {
+                "value": true,
+                "label": "Connected"
+            },
+            "label": "",
+            "color": "",
+            "length": null,
+            "length_unit": null
+        },
+        {
+            "id": 2,
+            "termination_a_type": "dcim.consoleport",
+            "termination_a_id": 3,
+            "termination_a": {
+                "id": 3,
+                "url": "http://localhost:8000/api/dcim/console-ports/3/",
+                "device": {
+                    "id": 3,
+                    "url": "http://localhost:8000/api/dcim/devices/3/",
+                    "name": "tst1-test3",
+                    "display_name": "tst1-test3"
+                },
+                "name": "Console",
+                "cable": 2
+            },
+            "termination_b_type": "dcim.consoleserverport",
+            "termination_b_id": 4,
+            "termination_b": {
+                "id": 4,
+                "url": "http://localhost:8000/api/dcim/console-server-ports/4/",
+                "device": {
+                    "id": 4,
+                    "url": "http://localhost:8000/api/dcim/devices/4/",
+                    "name": "tst1-test4",
+                    "display_name": "tst1-test4"
+                },
+                "name": "Port 11",
+                "cable": 2
+            },
+            "type": null,
+            "status": {
+                "value": true,
+                "label": "Connected"
+            },
+            "label": "",
+            "color": "",
+            "length": null,
+            "length_unit": null
+        },
+        {
+            "id": 3,
+            "termination_a_type": "dcim.consoleport",
+            "termination_a_id": 5,
+            "termination_a": {
+                "id": 5,
+                "url": "http://localhost:8000/api/dcim/console-ports/5/",
+                "device": {
+                    "id": 5,
+                    "url": "http://localhost:8000/api/dcim/devices/5/",
+                    "name": "tst1-test5",
+                    "display_name": "tst1-test5"
+                },
+                "name": "Console",
+                "cable": 3
+            },
+            "termination_b_type": "dcim.consoleserverport",
+            "termination_b_id": 6,
+            "termination_b": {
+                "id": 6,
+                "url": "http://localhost:8000/api/dcim/console-server-ports/6/",
+                "device": {
+                    "id": 6,
+                    "url": "http://localhost:8000/api/dcim/devices/6/",
+                    "name": "tst1-test6",
+                    "display_name": "tst1-test6"
+                },
+                "name": "Port 1",
+                "cable": 3
+            },
+            "type": null,
+            "status": {
+                "value": true,
+                "label": "Connected"
+            },
+            "label": "",
+            "color": "",
+            "length": null,
+            "length_unit": null
+        }
+    ]
+}
\ No newline at end of file
diff --git a/tests/fixtures/dcim/choices.json b/tests/fixtures/dcim/choices.json
new file mode 100644
index 0000000..0fa9944
--- /dev/null
+++ b/tests/fixtures/dcim/choices.json
@@ -0,0 +1,476 @@
+{
+    "device:face": [
+        {
+            "label": "Front",
+            "value": 0
+        },
+        {
+            "label": "Rear",
+            "value": 1
+        }
+    ],
+    "device:status": [
+        {
+            "label": "Active",
+            "value": 1
+        },
+        {
+            "label": "Offline",
+            "value": 0
+        },
+        {
+            "label": "Planned",
+            "value": 2
+        },
+        {
+            "label": "Staged",
+            "value": 3
+        },
+        {
+            "label": "Failed",
+            "value": 4
+        },
+        {
+            "label": "Inventory",
+            "value": 5
+        }
+    ],
+    "console-port:connection_status": [
+        {
+            "label": "Planned",
+            "value": false
+        },
+        {
+            "label": "Connected",
+            "value": true
+        }
+    ],
+    "interface:form_factor": [
+        {
+            "label": "Virtual",
+            "value": 0
+        },
+        {
+            "label": "Link Aggregation Group (LAG)",
+            "value": 200
+        },
+        {
+            "label": "100BASE-TX (10/100ME)",
+            "value": 800
+        },
+        {
+            "label": "1000BASE-T (1GE)",
+            "value": 1000
+        },
+        {
+            "label": "10GBASE-T (10GE)",
+            "value": 1150
+        },
+        {
+            "label": "10GBASE-CX4 (10GE)",
+            "value": 1170
+        },
+        {
+            "label": "GBIC (1GE)",
+            "value": 1050
+        },
+        {
+            "label": "SFP (1GE)",
+            "value": 1100
+        },
+        {
+            "label": "SFP+ (10GE)",
+            "value": 1200
+        },
+        {
+            "label": "XFP (10GE)",
+            "value": 1300
+        },
+        {
+            "label": "XENPAK (10GE)",
+            "value": 1310
+        },
+        {
+            "label": "X2 (10GE)",
+            "value": 1320
+        },
+        {
+            "label": "SFP28 (25GE)",
+            "value": 1350
+        },
+        {
+            "label": "QSFP+ (40GE)",
+            "value": 1400
+        },
+        {
+            "label": "CFP (100GE)",
+            "value": 1500
+        },
+        {
+            "label": "CFP2 (100GE)",
+            "value": 1510
+        },
+        {
+            "label": "CFP4 (100GE)",
+            "value": 1520
+        },
+        {
+            "label": "Cisco CPAK (100GE)",
+            "value": 1550
+        },
+        {
+            "label": "QSFP28 (100GE)",
+            "value": 1600
+        },
+        {
+            "label": "IEEE 802.11a",
+            "value": 2600
+        },
+        {
+            "label": "IEEE 802.11b/g",
+            "value": 2610
+        },
+        {
+            "label": "IEEE 802.11n",
+            "value": 2620
+        },
+        {
+            "label": "IEEE 802.11ac",
+            "value": 2630
+        },
+        {
+            "label": "IEEE 802.11ad",
+            "value": 2640
+        },
+        {
+            "label": "SFP (1GFC)",
+            "value": 3010
+        },
+        {
+            "label": "SFP (2GFC)",
+            "value": 3020
+        },
+        {
+            "label": "SFP (4GFC)",
+            "value": 3040
+        },
+        {
+            "label": "SFP+ (8GFC)",
+            "value": 3080
+        },
+        {
+            "label": "SFP+ (16GFC)",
+            "value": 3160
+        },
+        {
+            "label": "T1 (1.544 Mbps)",
+            "value": 4000
+        },
+        {
+            "label": "E1 (2.048 Mbps)",
+            "value": 4010
+        },
+        {
+            "label": "T3 (45 Mbps)",
+            "value": 4040
+        },
+        {
+            "label": "E3 (34 Mbps)",
+            "value": 4050
+        },
+        {
+            "label": "Cisco StackWise",
+            "value": 5000
+        },
+        {
+            "label": "Cisco StackWise Plus",
+            "value": 5050
+        },
+        {
+            "label": "Cisco FlexStack",
+            "value": 5100
+        },
+        {
+            "label": "Cisco FlexStack Plus",
+            "value": 5150
+        },
+        {
+            "label": "Juniper VCP",
+            "value": 5200
+        },
+        {
+            "label": "Extreme SummitStack",
+            "value": 5300
+        },
+        {
+            "label": "Extreme SummitStack-128",
+            "value": 5310
+        },
+        {
+            "label": "Extreme SummitStack-256",
+            "value": 5320
+        },
+        {
+            "label": "Extreme SummitStack-512",
+            "value": 5330
+        },
+        {
+            "label": "Other",
+            "value": 32767
+        }
+    ],
+    "interface:mode": [
+        {
+            "label": "Access",
+            "value": 100
+        },
+        {
+            "label": "Tagged",
+            "value": 200
+        },
+        {
+            "label": "Tagged All",
+            "value": 300
+        }
+    ],
+    "interface-connection:connection_status": [
+        {
+            "label": "Planned",
+            "value": false
+        },
+        {
+            "label": "Connected",
+            "value": true
+        }
+    ],
+    "interface-template:form_factor": [
+        {
+            "label": "Virtual",
+            "value": 0
+        },
+        {
+            "label": "Link Aggregation Group (LAG)",
+            "value": 200
+        },
+        {
+            "label": "100BASE-TX (10/100ME)",
+            "value": 800
+        },
+        {
+            "label": "1000BASE-T (1GE)",
+            "value": 1000
+        },
+        {
+            "label": "10GBASE-T (10GE)",
+            "value": 1150
+        },
+        {
+            "label": "10GBASE-CX4 (10GE)",
+            "value": 1170
+        },
+        {
+            "label": "GBIC (1GE)",
+            "value": 1050
+        },
+        {
+            "label": "SFP (1GE)",
+            "value": 1100
+        },
+        {
+            "label": "SFP+ (10GE)",
+            "value": 1200
+        },
+        {
+            "label": "XFP (10GE)",
+            "value": 1300
+        },
+        {
+            "label": "XENPAK (10GE)",
+            "value": 1310
+        },
+        {
+            "label": "X2 (10GE)",
+            "value": 1320
+        },
+        {
+            "label": "SFP28 (25GE)",
+            "value": 1350
+        },
+        {
+            "label": "QSFP+ (40GE)",
+            "value": 1400
+        },
+        {
+            "label": "CFP (100GE)",
+            "value": 1500
+        },
+        {
+            "label": "CFP2 (100GE)",
+            "value": 1510
+        },
+        {
+            "label": "CFP4 (100GE)",
+            "value": 1520
+        },
+        {
+            "label": "Cisco CPAK (100GE)",
+            "value": 1550
+        },
+        {
+            "label": "QSFP28 (100GE)",
+            "value": 1600
+        },
+        {
+            "label": "IEEE 802.11a",
+            "value": 2600
+        },
+        {
+            "label": "IEEE 802.11b/g",
+            "value": 2610
+        },
+        {
+            "label": "IEEE 802.11n",
+            "value": 2620
+        },
+        {
+            "label": "IEEE 802.11ac",
+            "value": 2630
+        },
+        {
+            "label": "IEEE 802.11ad",
+            "value": 2640
+        },
+        {
+            "label": "SFP (1GFC)",
+            "value": 3010
+        },
+        {
+            "label": "SFP (2GFC)",
+            "value": 3020
+        },
+        {
+            "label": "SFP (4GFC)",
+            "value": 3040
+        },
+        {
+            "label": "SFP+ (8GFC)",
+            "value": 3080
+        },
+        {
+            "label": "SFP+ (16GFC)",
+            "value": 3160
+        },
+        {
+            "label": "T1 (1.544 Mbps)",
+            "value": 4000
+        },
+        {
+            "label": "E1 (2.048 Mbps)",
+            "value": 4010
+        },
+        {
+            "label": "T3 (45 Mbps)",
+            "value": 4040
+        },
+        {
+            "label": "E3 (34 Mbps)",
+            "value": 4050
+        },
+        {
+            "label": "Cisco StackWise",
+            "value": 5000
+        },
+        {
+            "label": "Cisco StackWise Plus",
+            "value": 5050
+        },
+        {
+            "label": "Cisco FlexStack",
+            "value": 5100
+        },
+        {
+            "label": "Cisco FlexStack Plus",
+            "value": 5150
+        },
+        {
+            "label": "Juniper VCP",
+            "value": 5200
+        },
+        {
+            "label": "Extreme SummitStack",
+            "value": 5300
+        },
+        {
+            "label": "Extreme SummitStack-128",
+            "value": 5310
+        },
+        {
+            "label": "Extreme SummitStack-256",
+            "value": 5320
+        },
+        {
+            "label": "Extreme SummitStack-512",
+            "value": 5330
+        },
+        {
+            "label": "Other",
+            "value": 32767
+        }
+    ],
+    "power-port:connection_status": [
+        {
+            "label": "Planned",
+            "value": false
+        },
+        {
+            "label": "Connected",
+            "value": true
+        }
+    ],
+    "rack:type": [
+        {
+            "label": "2-post frame",
+            "value": 100
+        },
+        {
+            "label": "4-post frame",
+            "value": 200
+        },
+        {
+            "label": "4-post cabinet",
+            "value": 300
+        },
+        {
+            "label": "Wall-mounted frame",
+            "value": 1000
+        },
+        {
+            "label": "Wall-mounted cabinet",
+            "value": 1100
+        }
+    ],
+    "rack:width": [
+        {
+            "label": "19 inches",
+            "value": 19
+        },
+        {
+            "label": "23 inches",
+            "value": 23
+        }
+    ],
+    "site:status": [
+        {
+            "label": "Active",
+            "value": 1
+        },
+        {
+            "label": "Planned",
+            "value": 2
+        },
+        {
+            "label": "Retired",
+            "value": 4
+        }
+    ]
+}
\ No newline at end of file
diff --git a/tests/fixtures/dcim/interface.json b/tests/fixtures/dcim/interface.json
index 324fa7d..711d1ca 100644
--- a/tests/fixtures/dcim/interface.json
+++ b/tests/fixtures/dcim/interface.json
@@ -22,29 +22,25 @@
     "mgmt_only": false,
     "description": "",
     "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": ""
+    "connected_endpoint": {
+        "id": 1,
+        "url": "http://localhost:8000/api/dcim/interfaces/1/",
+        "device": {
+            "id": 1,
+            "url": "http://localhost:8000/api/dcim/devices/1/",
+            "name": "tst-endpoint",
+            "display_name": "tst-endpoint"
         },
-        "status": true
+        "name": "eth0",
+        "cable": 1
     },
-    "circuit_termination": null
+    "connection_status": {
+        "value": true,
+        "label": "Connected"
+    },
+    "cable": {
+        "id": 1,
+        "url": "http://localhost:8000/api/dcim/cables/1/",
+        "label": ""
+    }
 }
\ No newline at end of file
diff --git a/tests/test_api.py b/tests/test_api.py
index f4ba9bb..31aede6 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -1,8 +1,8 @@
 import unittest
 import six
 
-from .util import Response
 import pynetbox
+from .util import Response
 
 if six.PY3:
     from unittest.mock import patch
@@ -14,7 +14,6 @@ host = "http://localhost:8000"
 def_kwargs = {
     'token': 'abc123',
     'private_key_file': 'tests/fixtures/api/get_session_key.json',
-    'version': '2.0'
 }
 
 # Keys are app names, values are arbitrarily selected endpoints
@@ -31,10 +30,10 @@ endpoints = {
 class ApiTestCase(unittest.TestCase):
 
     @patch(
-        'pynetbox.lib.query.requests.post',
+        'pynetbox.core.query.requests.post',
         return_value=Response(fixture='api/get_session_key.json')
     )
-    def test_get(self, mock):
+    def test_get(self, *_):
         api = pynetbox.api(
             host,
             **def_kwargs
@@ -42,25 +41,25 @@ class ApiTestCase(unittest.TestCase):
         self.assertTrue(api)
 
     @patch(
-        'pynetbox.lib.query.requests.post',
+        'pynetbox.core.query.requests.post',
         return_value=Response(fixture='api/get_session_key.json')
     )
-    def test_sanitize_url(self, mock):
+    def test_sanitize_url(self, *_):
         api = pynetbox.api(
             'http://localhost:8000/',
             **def_kwargs
         )
         self.assertTrue(api)
-        self.assertEqual(api.api_kwargs['base_url'], 'http://localhost:8000/api')
+        self.assertEqual(api.base_url, 'http://localhost:8000/api')
 
 
 class ApiArgumentsTestCase(unittest.TestCase):
 
     @patch(
-        'pynetbox.lib.query.requests.post',
+        'pynetbox.core.query.requests.post',
         return_value=Response(fixture='api/get_session_key.json')
     )
-    def common_arguments(self, kwargs, arg, expect, mock):
+    def common_arguments(self, kwargs, arg, expect, *_):
         '''
         Ensures the api and endpoint instances have ssl_verify set
         as expected
@@ -69,7 +68,7 @@ class ApiArgumentsTestCase(unittest.TestCase):
             host,
             **kwargs
         )
-        self.assertIs(api.api_kwargs.get(arg, "fail"), expect)
+        self.assertIs(getattr(api, arg, "fail"), expect)
         for app, endpoint in endpoints.items():
             ep = getattr(getattr(api, app), endpoint)
             self.assertIs(getattr(ep, arg), expect)
diff --git a/tests/test_circuits.py b/tests/test_circuits.py
index 068ffeb..e7af58b 100644
--- a/tests/test_circuits.py
+++ b/tests/test_circuits.py
@@ -11,89 +11,89 @@ else:
 
 api = pynetbox.api(
     "http://localhost:8000",
-    version='2.0'
 )
 
 nb = api.circuits
 
 HEADERS = {
-    'accept': 'application/json; version=2.0;'
+    'accept': 'application/json;'
 }
 
 
-class GenericTest(object):
-    name = None
-    ret = pynetbox.lib.response.Record
-    app = 'circuits'
-
-    def test_get_all(self):
-        with patch(
-            'pynetbox.lib.query.requests.get',
-            return_value=Response(fixture='{}/{}.json'.format(
-                self.app,
-                self.name
-            ))
-        ) as mock:
-            ret = getattr(nb, self.name).all()
-            self.assertTrue(ret)
-            self.assertTrue(isinstance(ret, list))
-            self.assertTrue(isinstance(ret[0], self.ret))
-            mock.assert_called_with(
-                'http://localhost:8000/api/{}/{}/'.format(
+class Generic(object):
+    class Tests(unittest.TestCase):
+        name = ''
+        ret = pynetbox.core.response.Record
+        app = 'circuits'
+
+        def test_get_all(self):
+            with patch(
+                'pynetbox.core.query.requests.get',
+                return_value=Response(fixture='{}/{}.json'.format(
                     self.app,
-                    self.name.replace('_', '-')
-                ),
-                headers=HEADERS,
-                verify=True
-            )
-
-    def test_filter(self):
-        with patch(
-            'pynetbox.lib.query.requests.get',
-            return_value=Response(fixture='{}/{}.json'.format(
-                self.app,
-                self.name
-            ))
-        ) as mock:
-            ret = getattr(nb, self.name).filter(pk=1)
-            self.assertTrue(ret)
-            self.assertTrue(isinstance(ret, list))
-            self.assertTrue(isinstance(ret[0], self.ret))
-            mock.assert_called_with(
-                'http://localhost:8000/api/{}/{}/?pk=1'.format(
+                    self.name
+                ))
+            ) as mock:
+                ret = getattr(nb, self.name).all()
+                self.assertTrue(ret)
+                self.assertTrue(isinstance(ret, list))
+                self.assertTrue(isinstance(ret[0], self.ret))
+                mock.assert_called_with(
+                    'http://localhost:8000/api/{}/{}/'.format(
+                        self.app,
+                        self.name.replace('_', '-')
+                    ),
+                    headers=HEADERS,
+                    verify=True
+                )
+
+        def test_filter(self):
+            with patch(
+                'pynetbox.core.query.requests.get',
+                return_value=Response(fixture='{}/{}.json'.format(
                     self.app,
-                    self.name.replace('_', '-')
-                ),
-                headers=HEADERS,
-                verify=True
-            )
-
-    def test_get(self):
-        with patch(
-            'pynetbox.lib.query.requests.get',
-            return_value=Response(fixture='{}/{}.json'.format(
-                self.app,
-                self.name[:-1]
-            ))
-        ) as mock:
-            ret = getattr(nb, self.name).get(1)
-            self.assertTrue(ret)
-            self.assertTrue(isinstance(ret, self.ret))
-            mock.assert_called_with(
-                'http://localhost:8000/api/{}/{}/1/'.format(
+                    self.name
+                ))
+            ) as mock:
+                ret = getattr(nb, self.name).filter(name='test')
+                self.assertTrue(ret)
+                self.assertTrue(isinstance(ret, list))
+                self.assertTrue(isinstance(ret[0], self.ret))
+                mock.assert_called_with(
+                    'http://localhost:8000/api/{}/{}/?name=test'.format(
+                        self.app,
+                        self.name.replace('_', '-')
+                    ),
+                    headers=HEADERS,
+                    verify=True
+                )
+
+        def test_get(self):
+            with patch(
+                'pynetbox.core.query.requests.get',
+                return_value=Response(fixture='{}/{}.json'.format(
                     self.app,
-                    self.name.replace('_', '-')
-                ),
-                headers=HEADERS,
-                verify=True
-            )
-
-
-class CircuitsTestCase(unittest.TestCase, GenericTest):
+                    self.name[:-1]
+                ))
+            ) as mock:
+                ret = getattr(nb, self.name).get(1)
+                self.assertTrue(ret)
+                self.assertTrue(isinstance(ret, self.ret))
+                mock.assert_called_with(
+                    'http://localhost:8000/api/{}/{}/1/'.format(
+                        self.app,
+                        self.name.replace('_', '-')
+                    ),
+                    headers=HEADERS,
+                    verify=True
+                )
+
+
+class CircuitsTestCase(Generic.Tests):
     name = 'circuits'
 
     @patch(
-        'pynetbox.lib.query.requests.get',
+        'pynetbox.core.query.requests.get',
         return_value=Response(fixture='circuits/circuit.json')
     )
     def test_repr(self, _):
@@ -101,19 +101,19 @@ class CircuitsTestCase(unittest.TestCase, GenericTest):
         self.assertEqual(str(test), '123456')
 
 
-class ProviderTestCase(unittest.TestCase, GenericTest):
+class ProviderTestCase(Generic.Tests):
     name = 'providers'
 
 
-class CircuitTypeTestCase(unittest.TestCase, GenericTest):
+class CircuitTypeTestCase(Generic.Tests):
     name = 'circuit_types'
 
 
-class CircuitTerminationsTestCase(unittest.TestCase, GenericTest):
+class CircuitTerminationsTestCase(Generic.Tests):
     name = 'circuit_terminations'
 
     @patch(
-        'pynetbox.lib.query.requests.get',
+        'pynetbox.core.query.requests.get',
         return_value=Response(fixture='circuits/circuit_termination.json')
     )
     def test_repr(self, _):
diff --git a/tests/test_dcim.py b/tests/test_dcim.py
index fef8063..71b2c7a 100644
--- a/tests/test_dcim.py
+++ b/tests/test_dcim.py
@@ -1,8 +1,8 @@
 import unittest
 import six
 
-from .util import Response
 import pynetbox
+from .util import Response
 
 if six.PY3:
     from unittest.mock import patch
@@ -12,155 +12,155 @@ else:
 
 api = pynetbox.api(
     "http://localhost:8000",
-    version='2.0',
 )
 
 nb = api.dcim
 
 HEADERS = {
-    'accept': 'application/json; version=2.0;',
+    'accept': 'application/json;',
 }
 
 AUTH_HEADERS = {
-    'accept': 'application/json; version=2.0;',
+    'accept': 'application/json;',
     'authorization': 'Token None',
 }
 
 
-class GenericTest(object):
-    name = None
-    ret = pynetbox.lib.response.Record
-    app = 'dcim'
+class Generic(object):
+    class Tests(unittest.TestCase):
+        name = ''
+        ret = pynetbox.core.response.Record
+        app = 'dcim'
 
-    def test_get_all(self):
-        with patch(
-            'pynetbox.lib.query.requests.get',
-            return_value=Response(fixture='{}/{}.json'.format(
-                self.app,
-                self.name
-            ))
-        ) as mock:
-            ret = getattr(nb, self.name).all()
-            self.assertTrue(ret)
-            self.assertTrue(isinstance(ret, list))
-            self.assertTrue(isinstance(ret[0], self.ret))
-            mock.assert_called_with(
-                'http://localhost:8000/api/{}/{}/'.format(
+        def test_get_all(self):
+            with patch(
+                'pynetbox.core.query.requests.get',
+                return_value=Response(fixture='{}/{}.json'.format(
                     self.app,
-                    self.name.replace('_', '-')
-                ),
-                headers=HEADERS,
-                verify=True
-            )
-
-    def test_filter(self):
-        with patch(
-            'pynetbox.lib.query.requests.get',
-            return_value=Response(fixture='{}/{}.json'.format(
-                self.app,
-                self.name
-            ))
-        ) as mock:
-            ret = getattr(nb, self.name).filter(pk=1)
-            self.assertTrue(ret)
-            self.assertTrue(isinstance(ret, list))
-            self.assertTrue(isinstance(ret[0], self.ret))
-            mock.assert_called_with(
-                'http://localhost:8000/api/{}/{}/?pk=1'.format(
+                    self.name
+                ))
+            ) as mock:
+                ret = getattr(nb, self.name).all()
+                self.assertTrue(ret)
+                self.assertTrue(isinstance(ret, list))
+                self.assertTrue(isinstance(ret[0], self.ret))
+                mock.assert_called_with(
+                    'http://localhost:8000/api/{}/{}/'.format(
+                        self.app,
+                        self.name.replace('_', '-')
+                    ),
+                    headers=HEADERS,
+                    verify=True
+                )
+
+        def test_filter(self):
+            with patch(
+                'pynetbox.core.query.requests.get',
+                return_value=Response(fixture='{}/{}.json'.format(
                     self.app,
-                    self.name.replace('_', '-')
-                ),
-                headers=HEADERS,
-                verify=True
-            )
-
-    def test_get(self):
-        with patch(
-            'pynetbox.lib.query.requests.get',
-            return_value=Response(fixture='{}/{}.json'.format(
-                self.app,
-                self.name[:-1]
-            ))
-        ) as mock:
-            ret = getattr(nb, self.name).get(1)
-            self.assertTrue(ret)
-            self.assertTrue(isinstance(ret, self.ret))
-            self.assertTrue(isinstance(str(ret), str))
-            self.assertTrue(isinstance(dict(ret), dict))
-            mock.assert_called_with(
-                'http://localhost:8000/api/{}/{}/1/'.format(
+                    self.name
+                ))
+            ) as mock:
+                ret = getattr(nb, self.name).filter(name='test')
+                self.assertTrue(ret)
+                self.assertTrue(isinstance(ret, list))
+                self.assertTrue(isinstance(ret[0], self.ret))
+                mock.assert_called_with(
+                    'http://localhost:8000/api/{}/{}/?name=test'.format(
+                        self.app,
+                        self.name.replace('_', '-')
+                    ),
+                    headers=HEADERS,
+                    verify=True
+                )
+
+        def test_get(self):
+            with patch(
+                'pynetbox.core.query.requests.get',
+                return_value=Response(fixture='{}/{}.json'.format(
                     self.app,
-                    self.name.replace('_', '-')
-                ),
-                headers=HEADERS,
-                verify=True
-            )
-
-    def test_delete(self):
-        with patch(
-            'pynetbox.lib.query.requests.get',
-            return_value=Response(fixture='{}/{}.json'.format(
-                self.app,
-                self.name[:-1]
-            ))
-        ) as mock, patch('pynetbox.lib.query.requests.delete') as delete:
-            ret = getattr(nb, self.name).get(1)
-            self.assertTrue(ret.delete())
-            mock.assert_called_with(
-                'http://localhost:8000/api/{}/{}/1/'.format(
+                    self.name[:-1]
+                ))
+            ) as mock:
+                ret = getattr(nb, self.name).get(1)
+                self.assertTrue(ret)
+                self.assertTrue(isinstance(ret, self.ret))
+                self.assertTrue(isinstance(str(ret), str))
+                self.assertTrue(isinstance(dict(ret), dict))
+                mock.assert_called_with(
+                    'http://localhost:8000/api/{}/{}/1/'.format(
+                        self.app,
+                        self.name.replace('_', '-')
+                    ),
+                    headers=HEADERS,
+                    verify=True
+                )
+
+        def test_delete(self):
+            with patch(
+                'pynetbox.core.query.requests.get',
+                return_value=Response(fixture='{}/{}.json'.format(
                     self.app,
-                    self.name.replace('_', '-')
-                ),
-                headers=HEADERS,
-                verify=True
-            )
-            delete.assert_called_with(
-                'http://localhost:8000/api/{}/{}/1/'.format(
+                    self.name[:-1]
+                ))
+            ) as mock, patch('pynetbox.core.query.requests.delete') as delete:
+                ret = getattr(nb, self.name).get(1)
+                self.assertTrue(ret.delete())
+                mock.assert_called_with(
+                    'http://localhost:8000/api/{}/{}/1/'.format(
+                        self.app,
+                        self.name.replace('_', '-')
+                    ),
+                    headers=HEADERS,
+                    verify=True
+                )
+                delete.assert_called_with(
+                    'http://localhost:8000/api/{}/{}/1/'.format(
+                        self.app,
+                        self.name.replace('_', '-')
+                    ),
+                    headers=AUTH_HEADERS,
+                    verify=True
+                )
+
+        def test_compare(self):
+            with patch(
+                'pynetbox.core.query.requests.get',
+                return_value=Response(fixture='{}/{}.json'.format(
                     self.app,
-                    self.name.replace('_', '-')
-                ),
-                headers=AUTH_HEADERS,
-                verify=True
-            )
-
-    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())
+                    self.name[:-1]
+                ))
+            ):
+                ret = getattr(nb, self.name).get(1)
+                self.assertTrue(ret)
+                self.assertTrue(ret._compare())
+
+        def test_serialize(self):
+            with patch(
+                'pynetbox.core.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):
+class DeviceTestCase(Generic.Tests):
     name = 'devices'
 
     @patch(
-        'pynetbox.lib.query.requests.get',
+        'pynetbox.core.query.requests.get',
         return_value=Response(fixture='dcim/device.json')
     )
     def test_get(self, mock):
         ret = getattr(nb, self.name).get(1)
         self.assertTrue(ret)
         self.assertTrue(isinstance(ret, self.ret))
-        self.assertTrue(isinstance(ret.primary_ip, pynetbox.ipam.IpAddresses))
-        self.assertTrue(isinstance(ret.primary_ip4, pynetbox.ipam.IpAddresses))
+        self.assertTrue(isinstance(ret.primary_ip, pynetbox.models.ipam.IpAddresses))
+        self.assertTrue(isinstance(ret.primary_ip4, pynetbox.models.ipam.IpAddresses))
         self.assertTrue(isinstance(ret.config_context, dict))
         self.assertTrue(isinstance(ret.custom_fields, dict))
         mock.assert_called_with(
@@ -173,7 +173,7 @@ class DeviceTestCase(unittest.TestCase, GenericTest):
         )
 
     @patch(
-        'pynetbox.lib.query.requests.get',
+        'pynetbox.core.query.requests.get',
         return_value=Response(fixture='dcim/devices.json')
     )
     def test_multi_filter(self, mock):
@@ -191,10 +191,10 @@ class DeviceTestCase(unittest.TestCase, GenericTest):
         )
 
     @patch(
-        'pynetbox.lib.query.requests.get',
+        'pynetbox.core.query.requests.get',
         return_value=Response(fixture='dcim/device.json')
     )
-    def test_modify(self, mock):
+    def test_modify(self, *_):
         ret = nb.devices.get(1)
         ret.serial = '123123123123'
         ret_serialized = ret.serialize()
@@ -203,10 +203,10 @@ class DeviceTestCase(unittest.TestCase, GenericTest):
         self.assertEqual(ret_serialized['serial'], '123123123123')
 
     @patch(
-        'pynetbox.lib.query.requests.post',
+        'pynetbox.core.query.requests.post',
         return_value=Response(fixture='dcim/device.json')
     )
-    def test_create(self, mock):
+    def test_create(self, *_):
         data = {
             'name': 'test-device',
             'site': 1,
@@ -217,10 +217,10 @@ class DeviceTestCase(unittest.TestCase, GenericTest):
         self.assertTrue(ret)
 
     @patch(
-        'pynetbox.lib.query.requests.post',
+        'pynetbox.core.query.requests.post',
         return_value=Response(fixture='dcim/device_bulk_create.json')
     )
-    def test_create_device_bulk(self, mock):
+    def test_create_device_bulk(self, *_):
         data = [
             {
                 'name': 'test-device',
@@ -240,13 +240,13 @@ class DeviceTestCase(unittest.TestCase, GenericTest):
         self.assertTrue(len(ret), 2)
 
     @patch(
-        'pynetbox.lib.query.requests.get',
+        'pynetbox.core.query.requests.get',
         side_effect=[
             Response(fixture='dcim/device.json'),
             Response(fixture='dcim/rack.json'),
         ]
     )
-    def test_get_recurse(self, mock):
+    def test_get_recurse(self, *_):
         '''Test that automatic recursion works, and that nested items
         are converted to Response() objects.
         '''
@@ -259,7 +259,7 @@ class DeviceTestCase(unittest.TestCase, GenericTest):
         ))
 
     @patch(
-        'pynetbox.lib.query.requests.get',
+        'pynetbox.core.query.requests.get',
         side_effect=[
             Response(fixture='dcim/device.json'),
             Response(fixture='dcim/napalm.json'),
@@ -277,14 +277,14 @@ class DeviceTestCase(unittest.TestCase, GenericTest):
         self.assertTrue(ret['get_facts'])
 
 
-class SiteTestCase(unittest.TestCase, GenericTest):
+class SiteTestCase(Generic.Tests):
     name = 'sites'
 
     @patch(
-        'pynetbox.lib.query.requests.get',
+        'pynetbox.core.query.requests.get',
         return_value=Response(fixture='dcim/site.json')
     )
-    def test_modify_custom(self, mock):
+    def test_modify_custom(self, *_):
         '''Test modifying a custom field.
         '''
         ret = getattr(nb, self.name).get(1)
@@ -296,7 +296,7 @@ class SiteTestCase(unittest.TestCase, GenericTest):
         )
 
     @patch(
-        'pynetbox.lib.query.requests.get',
+        'pynetbox.core.query.requests.get',
         return_value=Response(fixture='dcim/site.json')
     )
     def test_custom_selection_serializer(self, _):
@@ -305,18 +305,16 @@ class SiteTestCase(unittest.TestCase, GenericTest):
         ret = getattr(nb, self.name).get(1)
         ret.custom_fields['test_custom'] = "Testing"
         test = ret.serialize()
-        from pprint import pprint
-        pprint(test)
         self.assertEqual(
             test['custom_fields']['test_selection'],
             2
         )
 
     @patch(
-        'pynetbox.lib.query.requests.post',
+        'pynetbox.core.query.requests.post',
         return_value=Response(fixture='dcim/site.json')
     )
-    def test_create(self, mock):
+    def test_create(self, *_):
         data = {
             'name': 'TEST1',
             'custom_fields': {
@@ -327,14 +325,14 @@ class SiteTestCase(unittest.TestCase, GenericTest):
         self.assertTrue(ret)
 
 
-class InterfaceTestCase(unittest.TestCase, GenericTest):
+class InterfaceTestCase(Generic.Tests):
     name = 'interfaces'
 
     @patch(
-        'pynetbox.lib.query.requests.get',
+        'pynetbox.core.query.requests.get',
         return_value=Response(fixture='dcim/interface.json')
     )
-    def test_modify(self, mock):
+    def test_modify(self, *_):
         ret = nb.interfaces.get(1)
         ret.description = 'Testing'
         ret_serialized = ret.serialize()
@@ -343,23 +341,7 @@ class InterfaceTestCase(unittest.TestCase, GenericTest):
         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',
+        'pynetbox.core.query.requests.get',
         side_effect=[
             Response(fixture='dcim/{}.json'.format(name + '_1')),
             Response(fixture='dcim/{}.json'.format(name + '_2')),
@@ -372,17 +354,17 @@ class InterfaceTestCase(unittest.TestCase, GenericTest):
         self.assertTrue(isinstance(ret[0], self.ret))
         self.assertEqual(len(ret), 71)
         mock.assert_called_with(
-            'http://localhost:8000/api/dcim/interfaces/?limit=221&offset=50'.format(self.name),
+            'http://localhost:8000/api/dcim/interfaces/?limit=221&offset=50',
             headers=HEADERS,
             verify=True
         )
 
 
-class RackTestCase(unittest.TestCase, GenericTest):
+class RackTestCase(Generic.Tests):
     name = 'racks'
 
     @patch(
-        'pynetbox.lib.query.requests.get',
+        'pynetbox.core.query.requests.get',
         side_effect=[
             Response(fixture='dcim/rack.json'),
             Response(fixture='dcim/rack_u.json'),
@@ -398,97 +380,177 @@ class RackTestCase(unittest.TestCase, GenericTest):
         )
         self.assertTrue(ret)
         self.assertTrue(
-            isinstance(ret[0].device, pynetbox.dcim.Devices)
+            isinstance(ret[0].device, pynetbox.models.dcim.Devices)
         )
 
 
-class RackRoleTestCase(unittest.TestCase, GenericTest):
+class RackRoleTestCase(Generic.Tests):
     name = 'rack_roles'
 
 
-class RegionTestCase(unittest.TestCase, GenericTest):
+class RegionTestCase(Generic.Tests):
     name = 'regions'
 
 
-class RackGroupsTestCase(unittest.TestCase, GenericTest):
+class RackGroupsTestCase(Generic.Tests):
     name = 'rack_groups'
 
 
-class RackReservationsTestCase(unittest.TestCase, GenericTest):
+class RackReservationsTestCase(Generic.Tests):
     name = 'rack_reservations'
 
 
-class ManufacturersTestCase(unittest.TestCase, GenericTest):
+class ManufacturersTestCase(Generic.Tests):
     name = 'manufacturers'
 
 
-class DeviceTypeTestCase(unittest.TestCase, GenericTest):
+class DeviceTypeTestCase(Generic.Tests):
     name = 'device_types'
 
 
-class ConsolePortTemplateTestCase(unittest.TestCase, GenericTest):
+class ConsolePortTemplateTestCase(Generic.Tests):
     name = 'console_port_templates'
 
 
-class ConsoleServerPortTemplateTestCase(unittest.TestCase, GenericTest):
+class ConsoleServerPortTemplateTestCase(Generic.Tests):
     name = 'console_server_port_templates'
 
 
-class PowerPortTemplateTestCase(unittest.TestCase, GenericTest):
+class PowerPortTemplateTestCase(Generic.Tests):
     name = 'power_port_templates'
 
 
-class PowerOutletTemplateTestCase(unittest.TestCase, GenericTest):
+class PowerOutletTemplateTestCase(Generic.Tests):
     name = 'power_outlet_templates'
 
 
-class InterfaceTemplateTestCase(unittest.TestCase, GenericTest):
+class InterfaceTemplateTestCase(Generic.Tests):
     name = 'interface_templates'
 
 
-class DeviceBayTemplateTestCase(unittest.TestCase, GenericTest):
+class DeviceBayTemplateTestCase(Generic.Tests):
     name = 'device_bay_templates'
 
 
-class DeviceRolesTestCase(unittest.TestCase, GenericTest):
+class DeviceRolesTestCase(Generic.Tests):
     name = 'device_roles'
 
 
-class PlatformsTestCase(unittest.TestCase, GenericTest):
+class PlatformsTestCase(Generic.Tests):
     name = 'platforms'
 
 
-class ConsolePortsTestCase(unittest.TestCase, GenericTest):
+class ConsolePortsTestCase(Generic.Tests):
     name = 'console_ports'
 
 
-class ConsoleServerPortsTestCase(unittest.TestCase, GenericTest):
+class ConsoleServerPortsTestCase(Generic.Tests):
     name = 'console_server_ports'
 
 
-class PowerPortsTestCase(unittest.TestCase, GenericTest):
+class PowerPortsTestCase(Generic.Tests):
     name = 'power_ports'
 
 
-class PowerOutletsTestCase(unittest.TestCase, GenericTest):
+class PowerOutletsTestCase(Generic.Tests):
     name = 'power_outlets'
 
 
-class DeviceBaysTestCase(unittest.TestCase, GenericTest):
+class DeviceBaysTestCase(Generic.Tests):
     name = 'device_bays'
 
 
-# class InventoryItemsTestCase(unittest.TestCase, GenericTest):
+# class InventoryItemsTestCase(Generic.Tests):
 #     name = 'inventory_items'
 
 
-class InterfaceConnectionsTestCase(unittest.TestCase, GenericTest):
+class InterfaceConnectionsTestCase(Generic.Tests):
     name = 'interface_connections'
 
 
-# class ConnectedDevicesTestCase(unittest.TestCase, GenericTest):
+# class ConnectedDevicesTestCase(Generic.Tests):
 #     name = 'connected_device'
 
 
-class VirtualChassisTestCase(unittest.TestCase, GenericTest):
+class VirtualChassisTestCase(Generic.Tests):
     name = 'virtual_chassis_devices'
+
+
+class Choices(unittest.TestCase):
+
+    def test_get(self):
+        with patch(
+            'pynetbox.core.query.requests.get',
+            return_value=Response(fixture='{}/{}.json'.format(
+                'dcim',
+                'choices'
+            ))
+        ) as mock:
+            ret = nb.choices()
+            self.assertTrue(ret)
+            mock.assert_called_with(
+                'http://localhost:8000/api/dcim/_choices/',
+                headers=HEADERS,
+                verify=True
+            )
+
+
+class CablesTestCase(Generic.Tests):
+    name = 'cables'
+
+    def test_get_circuit(self):
+        response_obj = Response(content={
+            "id": 1,
+            "termination_a_type": "circuits.circuittermination",
+            "termination_a_id": 1,
+            "termination_a": {
+                "id": 1,
+                "url": "http://localhost:8000/api/circuits/circuit-terminations/1/",
+                "circuit": {
+                    "id": 346,
+                    "url": "http://localhost:8000/api/circuits/circuits/1/",
+                    "cid": "TEST123321"
+                },
+                "term_side": "A",
+            },
+            "termination_b_type": "dcim.interface",
+            "termination_b_id": 2,
+            "termination_b": {
+                "id": 2,
+                "url": "http://localhost:8000/api/dcim/interfaces/2/",
+                "device": {
+                    "id": 2,
+                    "url": "http://localhost:8000/api/dcim/devices/2/",
+                    "name": "tst1-test2",
+                    "display_name": "tst1-test2"
+                },
+                "name": "xe-0/0/0",
+                "cable": 1
+            },
+            "type": None,
+            "status": {
+                "value": True,
+                "label": "Connected"
+            },
+            "label": "",
+            "color": "",
+            "length": None,
+            "length_unit": None
+        })
+        with patch(
+            'pynetbox.core.query.requests.get',
+            return_value=response_obj
+        ) as mock:
+            ret = getattr(nb, self.name).get(1)
+            self.assertTrue(ret)
+            self.assertTrue(isinstance(ret, self.ret))
+            self.assertTrue(isinstance(str(ret), str))
+            self.assertTrue(isinstance(dict(ret), dict))
+            mock.assert_called_with(
+                'http://localhost:8000/api/{}/{}/1/'.format(
+                    self.app,
+                    self.name.replace('_', '-')
+                ),
+                headers=HEADERS,
+                verify=True
+            )
diff --git a/tests/test_ipam.py b/tests/test_ipam.py
index b97aa75..5d9bf72 100644
--- a/tests/test_ipam.py
+++ b/tests/test_ipam.py
@@ -1,11 +1,10 @@
 import unittest
 import json
 
-import netaddr
 import six
 
-from .util import Response
 import pynetbox
+from .util import Response
 
 if six.PY3:
     from unittest.mock import patch
@@ -15,123 +14,114 @@ else:
 
 api = pynetbox.api(
     "http://localhost:8000",
-    version='2.0'
 )
 
 nb = api.ipam
 
 HEADERS = {
-    'accept': 'application/json; version=2.0;'
+    'accept': 'application/json;'
 }
 
 POST_HEADERS = {
-    'Content-Type': 'application/json; version=2.0;',
+    'Content-Type': 'application/json;',
     'authorization': 'Token None',
 }
 
 
-class GenericTest(object):
-    name = None
-    name_singular = None
-    ret = pynetbox.lib.response.IPRecord
-    app = 'ipam'
-    ip_obj_fields = {}
-
-    def test_get_all(self):
-        with patch(
-            'pynetbox.lib.query.requests.get',
-            return_value=Response(fixture='{}/{}.json'.format(
-                self.app,
-                self.name
-            ))
-        ) as mock:
-            ret = getattr(nb, self.name).all()
-            self.assertTrue(ret)
-            self.assertTrue(isinstance(ret, list))
-            self.assertTrue(isinstance(ret[0], self.ret))
-            mock.assert_called_with(
-                'http://localhost:8000/api/{}/{}/'.format(
+class Generic(object):
+    class Tests(unittest.TestCase):
+        name = ''
+        name_singular = None
+        ret = pynetbox.core.response.Record
+        app = 'ipam'
+
+        def test_get_all(self):
+            with patch(
+                'pynetbox.core.query.requests.get',
+                return_value=Response(fixture='{}/{}.json'.format(
                     self.app,
-                    self.name.replace('_', '-')
-                ),
-                headers=HEADERS,
-                verify=True
-            )
-
-    def test_filter(self):
-        with patch(
-            'pynetbox.lib.query.requests.get',
-            return_value=Response(fixture='{}/{}.json'.format(
-                self.app,
-                self.name
-            ))
-        ) as mock:
-            ret = getattr(nb, self.name).filter(pk=1)
-            self.assertTrue(ret)
-            self.assertTrue(isinstance(ret, list))
-            self.assertTrue(isinstance(ret[0], self.ret))
-            mock.assert_called_with(
-                'http://localhost:8000/api/{}/{}/?pk=1'.format(
+                    self.name
+                ))
+            ) as mock:
+                ret = getattr(nb, self.name).all()
+                self.assertTrue(ret)
+                self.assertTrue(isinstance(ret, list))
+                self.assertTrue(isinstance(ret[0], self.ret))
+                mock.assert_called_with(
+                    'http://localhost:8000/api/{}/{}/'.format(
+                        self.app,
+                        self.name.replace('_', '-')
+                    ),
+                    headers=HEADERS,
+                    verify=True
+                )
+
+        def test_filter(self):
+            with patch(
+                'pynetbox.core.query.requests.get',
+                return_value=Response(fixture='{}/{}.json'.format(
                     self.app,
-                    self.name.replace('_', '-')
-                ),
-                headers=HEADERS,
-                verify=True
-            )
-
-    def test_get(self):
-        with patch(
-            'pynetbox.lib.query.requests.get',
-            return_value=Response(fixture='{}/{}.json'.format(
-                self.app,
-                self.name_singular or self.name[:-1]
-            ))
-        ) as mock:
-            ret = getattr(nb, self.name).get(1)
-            self.assertTrue(ret)
-            self.assertTrue(isinstance(ret, self.ret))
-            self.assertTrue(isinstance(dict(ret), dict))
-            self.assertTrue(isinstance(str(ret), str))
-            mock.assert_called_with(
-                'http://localhost:8000/api/{}/{}/1/'.format(
+                    self.name
+                ))
+            ) as mock:
+                ret = getattr(nb, self.name).filter(name='test')
+                self.assertTrue(ret)
+                self.assertTrue(isinstance(ret, list))
+                self.assertTrue(isinstance(ret[0], self.ret))
+                mock.assert_called_with(
+                    'http://localhost:8000/api/{}/{}/?name=test'.format(
+                        self.app,
+                        self.name.replace('_', '-')
+                    ),
+                    headers=HEADERS,
+                    verify=True
+                )
+
+        def test_get(self):
+            with patch(
+                'pynetbox.core.query.requests.get',
+                return_value=Response(fixture='{}/{}.json'.format(
                     self.app,
-                    self.name.replace('_', '-')
-                ),
-                headers=HEADERS,
-                verify=True
-            )
-            if self.ip_obj_fields:
-                for field in self.ip_obj_fields:
-                    self.assertTrue(
-                        isinstance(getattr(ret, field), netaddr.IPNetwork)
-                    )
-                    self.assertTrue(netaddr.IPNetwork(dict(ret)[field]))
-
-
-class PrefixTestCase(unittest.TestCase, GenericTest):
+                    self.name_singular or self.name[:-1]
+                ))
+            ) as mock:
+                ret = getattr(nb, self.name).get(1)
+                self.assertTrue(ret)
+                self.assertTrue(isinstance(ret, self.ret))
+                self.assertTrue(isinstance(dict(ret), dict))
+                self.assertTrue(isinstance(str(ret), str))
+                mock.assert_called_with(
+                    'http://localhost:8000/api/{}/{}/1/'.format(
+                        self.app,
+                        self.name.replace('_', '-')
+                    ),
+                    headers=HEADERS,
+                    verify=True
+                )
+
+
+class PrefixTestCase(Generic.Tests):
     name = 'prefixes'
     name_singular = 'prefix'
-    ip_obj_fields = ['prefix']
 
     @patch(
-        'pynetbox.lib.query.requests.get',
+        'pynetbox.core.query.requests.get',
         return_value=Response(fixture='ipam/prefix.json')
     )
-    def test_modify(self, mock):
+    def test_modify(self, *_):
         ret = nb.prefixes.get(1)
         ret.prefix = '10.1.2.0/24'
         ret_serialized = ret.serialize()
         self.assertTrue(ret_serialized)
         self.assertFalse(ret._compare())
         self.assertEqual(ret_serialized['prefix'], '10.1.2.0/24')
-        self.assertTrue(netaddr.IPNetwork(ret_serialized['prefix']))
 
     @patch(
-        'pynetbox.lib.query.requests.put',
+        'pynetbox.core.query.requests.put',
         return_value=Response(fixture='ipam/prefix.json')
     )
     @patch(
-        'pynetbox.lib.query.requests.get',
+        'pynetbox.core.query.requests.get',
         return_value=Response(fixture='ipam/prefix.json')
     )
     def test_idempotence(self, *_):
@@ -140,7 +130,7 @@ class PrefixTestCase(unittest.TestCase, GenericTest):
         self.assertFalse(test)
 
     @patch(
-        'pynetbox.lib.query.requests.get',
+        'pynetbox.core.query.requests.get',
         side_effect=[
             Response(fixture='ipam/prefix.json'),
             Response(fixture='ipam/available-ips.json'),
@@ -150,7 +140,7 @@ class PrefixTestCase(unittest.TestCase, GenericTest):
         pfx = nb.prefixes.get(1)
         ret = pfx.available_ips.list()
         mock.assert_called_with(
-            'http://localhost:8000/api/ipam/prefixes/1/available-ips/'.format(self.name),
+            'http://localhost:8000/api/ipam/prefixes/1/available-ips/',
             headers=HEADERS,
             verify=True
         )
@@ -158,14 +148,14 @@ class PrefixTestCase(unittest.TestCase, GenericTest):
         self.assertEqual(len(ret), 3)
 
     @patch(
-        'pynetbox.lib.query.requests.post',
+        'pynetbox.core.query.requests.post',
         return_value=Response(fixture='ipam/available-ips-post.json')
     )
     @patch(
-        'pynetbox.lib.query.requests.get',
+        'pynetbox.core.query.requests.get',
         return_value=Response(fixture='ipam/prefix.json'),
     )
-    def test_create_available_ips(self, get, post):
+    def test_create_available_ips(self, _, post):
         expected_result = {
             'status': 1,
             'description': '',
@@ -184,7 +174,7 @@ class PrefixTestCase(unittest.TestCase, GenericTest):
         pfx = nb.prefixes.get(1)
         ret = pfx.available_ips.create(create_parms)
         post.assert_called_with(
-            'http://localhost:8000/api/ipam/prefixes/1/available-ips/'.format(self.name),
+            'http://localhost:8000/api/ipam/prefixes/1/available-ips/',
             headers=POST_HEADERS,
             data=json.dumps(create_parms),
             verify=True
@@ -193,7 +183,7 @@ class PrefixTestCase(unittest.TestCase, GenericTest):
         self.assertEqual(ret, expected_result)
 
     @patch(
-        'pynetbox.lib.query.requests.get',
+        'pynetbox.core.query.requests.get',
         side_effect=[
             Response(fixture='ipam/prefix.json'),
             Response(fixture='ipam/available-prefixes.json'),
@@ -203,28 +193,28 @@ class PrefixTestCase(unittest.TestCase, GenericTest):
         pfx = nb.prefixes.get(1)
         ret = pfx.available_prefixes.list()
         mock.assert_called_with(
-            'http://localhost:8000/api/ipam/prefixes/1/available-prefixes/'.format(self.name),
+            'http://localhost:8000/api/ipam/prefixes/1/available-prefixes/',
             headers=HEADERS,
             verify=True
         )
         self.assertTrue(ret)
 
     @patch(
-        'pynetbox.lib.query.requests.post',
+        'pynetbox.core.query.requests.post',
         return_value=Response(fixture='ipam/available-prefixes-post.json')
     )
     @patch(
-        'pynetbox.lib.query.requests.get',
+        'pynetbox.core.query.requests.get',
         return_value=Response(fixture='ipam/prefix.json'),
     )
-    def test_create_available_prefixes(self, get, post):
+    def test_create_available_prefixes(self, _, post):
         create_parms = dict(
             prefix_length=30,
         )
         pfx = nb.prefixes.get(1)
         ret = pfx.available_prefixes.create(create_parms)
         post.assert_called_with(
-            'http://localhost:8000/api/ipam/prefixes/1/available-prefixes/'.format(self.name),
+            'http://localhost:8000/api/ipam/prefixes/1/available-prefixes/',
             headers=POST_HEADERS,
             data=json.dumps(create_parms),
             verify=True
@@ -232,16 +222,15 @@ class PrefixTestCase(unittest.TestCase, GenericTest):
         self.assertTrue(ret)
 
 
-class IPAddressTestCase(unittest.TestCase, GenericTest):
+class IPAddressTestCase(Generic.Tests):
     name = 'ip_addresses'
     name_singular = 'ip_address'
-    ip_obj_fields = ['address']
 
     @patch(
-        'pynetbox.lib.query.requests.get',
+        'pynetbox.core.query.requests.get',
         return_value=Response(fixture='ipam/ip_address.json')
     )
-    def test_modify(self, mock):
+    def test_modify(self, *_):
         ret = nb.prefixes.get(1)
         ret.description = 'testing'
         ret_serialized = ret.serialize()
@@ -249,33 +238,31 @@ class IPAddressTestCase(unittest.TestCase, GenericTest):
         self.assertFalse(ret._compare())
         self.assertEqual(ret_serialized['address'], '10.0.255.1/32')
         self.assertEqual(ret_serialized['description'], 'testing')
-        self.assertTrue(netaddr.IPNetwork(ret_serialized['address']))
 
 
-class RoleTestCase(unittest.TestCase, GenericTest):
+class RoleTestCase(Generic.Tests):
     name = 'roles'
 
 
-class RIRTestCase(unittest.TestCase, GenericTest):
+class RIRTestCase(Generic.Tests):
     name = 'rirs'
 
 
-class AggregatesTestCase(unittest.TestCase, GenericTest):
+class AggregatesTestCase(Generic.Tests):
     name = 'aggregates'
-    ip_obj_fields = ['prefix']
 
 
-class VlanTestCase(unittest.TestCase, GenericTest):
+class VlanTestCase(Generic.Tests):
     name = 'vlans'
 
 
-class VlanGroupsTestCase(unittest.TestCase, GenericTest):
+class VlanGroupsTestCase(Generic.Tests):
     name = 'vlan_groups'
 
 
-class VRFTestCase(unittest.TestCase, GenericTest):
+class VRFTestCase(Generic.Tests):
     name = 'vrfs'
 
 
-# class ServicesTestCase(unittest.TestCase, GenericTest):
+# class ServicesTestCase(Generic.Tests):
 #     name = 'services'
diff --git a/tests/test_query.py b/tests/test_query.py
index 721dbbf..6d30388 100644
--- a/tests/test_query.py
+++ b/tests/test_query.py
@@ -1,7 +1,7 @@
 import unittest
 from collections import OrderedDict
 
-from pynetbox.lib.query import url_param_builder
+from pynetbox.core.query import url_param_builder
 
 
 class TestQuery(unittest.TestCase):
diff --git a/tests/test_tenancy.py b/tests/test_tenancy.py
index 35c6dde..3a94fe5 100644
--- a/tests/test_tenancy.py
+++ b/tests/test_tenancy.py
@@ -1,8 +1,8 @@
 import unittest
 import six
 
-from .util import Response
 import pynetbox
+from .util import Response
 
 if six.PY3:
     from unittest.mock import patch
@@ -12,87 +12,87 @@ else:
 
 api = pynetbox.api(
     "http://localhost:8000",
-    version='2.0'
 )
 
 nb = api.tenancy
 
 HEADERS = {
-    'accept': 'application/json; version=2.0;'
+    'accept': 'application/json;'
 }
 
 
-class GenericTest(object):
-    name = None
-    ret = pynetbox.lib.response.Record
-    app = 'tenancy'
+class Generic(object):
+    class Tests(unittest.TestCase):
+        name = ''
+        ret = pynetbox.core.response.Record
+        app = 'tenancy'
 
-    def test_get_all(self):
-        with patch(
-            'pynetbox.lib.query.requests.get',
-            return_value=Response(fixture='{}/{}.json'.format(
-                self.app,
-                self.name
-            ))
-        ) as mock:
-            ret = getattr(nb, self.name).all()
-            self.assertTrue(ret)
-            self.assertTrue(isinstance(ret, list))
-            self.assertTrue(isinstance(ret[0], self.ret))
-            mock.assert_called_with(
-                'http://localhost:8000/api/{}/{}/'.format(
+        def test_get_all(self):
+            with patch(
+                'pynetbox.core.query.requests.get',
+                return_value=Response(fixture='{}/{}.json'.format(
                     self.app,
-                    self.name.replace('_', '-')
-                ),
-                headers=HEADERS,
-                verify=True
-            )
+                    self.name
+                ))
+            ) as mock:
+                ret = getattr(nb, self.name).all()
+                self.assertTrue(ret)
+                self.assertTrue(isinstance(ret, list))
+                self.assertTrue(isinstance(ret[0], self.ret))
+                mock.assert_called_with(
+                    'http://localhost:8000/api/{}/{}/'.format(
+                        self.app,
+                        self.name.replace('_', '-')
+                    ),
+                    headers=HEADERS,
+                    verify=True
+                )
 
-    def test_filter(self):
-        with patch(
-            'pynetbox.lib.query.requests.get',
-            return_value=Response(fixture='{}/{}.json'.format(
-                self.app,
-                self.name
-            ))
-        ) as mock:
-            ret = getattr(nb, self.name).filter(pk=1)
-            self.assertTrue(ret)
-            self.assertTrue(isinstance(ret, list))
-            self.assertTrue(isinstance(ret[0], self.ret))
-            mock.assert_called_with(
-                'http://localhost:8000/api/{}/{}/?pk=1'.format(
+        def test_filter(self):
+            with patch(
+                'pynetbox.core.query.requests.get',
+                return_value=Response(fixture='{}/{}.json'.format(
                     self.app,
-                    self.name.replace('_', '-')
-                ),
-                headers=HEADERS,
-                verify=True
-            )
+                    self.name
+                ))
+            ) as mock:
+                ret = getattr(nb, self.name).filter(name='test')
+                self.assertTrue(ret)
+                self.assertTrue(isinstance(ret, list))
+                self.assertTrue(isinstance(ret[0], self.ret))
+                mock.assert_called_with(
+                    'http://localhost:8000/api/{}/{}/?name=test'.format(
+                        self.app,
+                        self.name.replace('_', '-')
+                    ),
+                    headers=HEADERS,
+                    verify=True
+                )
 
-    def test_get(self):
-        with patch(
-            'pynetbox.lib.query.requests.get',
-            return_value=Response(fixture='{}/{}.json'.format(
-                self.app,
-                self.name[:-1]
-            ))
-        ) as mock:
-            ret = getattr(nb, self.name).get(1)
-            self.assertTrue(ret)
-            self.assertTrue(isinstance(ret, self.ret))
-            mock.assert_called_with(
-                'http://localhost:8000/api/{}/{}/1/'.format(
+        def test_get(self):
+            with patch(
+                'pynetbox.core.query.requests.get',
+                return_value=Response(fixture='{}/{}.json'.format(
                     self.app,
-                    self.name.replace('_', '-')
-                ),
-                headers=HEADERS,
-                verify=True
-            )
+                    self.name[:-1]
+                ))
+            ) as mock:
+                ret = getattr(nb, self.name).get(1)
+                self.assertTrue(ret)
+                self.assertTrue(isinstance(ret, self.ret))
+                mock.assert_called_with(
+                    'http://localhost:8000/api/{}/{}/1/'.format(
+                        self.app,
+                        self.name.replace('_', '-')
+                    ),
+                    headers=HEADERS,
+                    verify=True
+                )
 
 
-class TenantsTestCase(unittest.TestCase, GenericTest):
+class TenantsTestCase(Generic.Tests):
     name = 'tenants'
 
 
-class TenantGroupsTestCase(unittest.TestCase, GenericTest):
+class TenantGroupsTestCase(Generic.Tests):
     name = 'tenant_groups'
diff --git a/tests/test_virtualization.py b/tests/test_virtualization.py
index e80ffd8..dcab59f 100644
--- a/tests/test_virtualization.py
+++ b/tests/test_virtualization.py
@@ -12,99 +12,99 @@ else:
 
 api = pynetbox.api(
     "http://localhost:8000",
-    version='2.0'
 )
 
 nb = api.virtualization
 
 HEADERS = {
-    'accept': 'application/json; version=2.0;'
+    'accept': 'application/json;'
 }
 
 
-class GenericTest(object):
-    name = None
-    ret = pynetbox.lib.response.Record
-    app = 'virtualization'
-
-    def test_get_all(self):
-        with patch(
-            'pynetbox.lib.query.requests.get',
-            return_value=Response(fixture='{}/{}.json'.format(
-                self.app,
-                self.name
-            ))
-        ) as mock:
-            ret = getattr(nb, self.name).all()
-            self.assertTrue(ret)
-            self.assertTrue(isinstance(ret, list))
-            self.assertTrue(isinstance(ret[0], self.ret))
-            mock.assert_called_with(
-                'http://localhost:8000/api/{}/{}/'.format(
+class Generic(object):
+    class Tests(unittest.TestCase):
+        name = ''
+        ret = pynetbox.core.response.Record
+        app = 'virtualization'
+
+        def test_get_all(self):
+            with patch(
+                'pynetbox.core.query.requests.get',
+                return_value=Response(fixture='{}/{}.json'.format(
                     self.app,
-                    self.name.replace('_', '-')
-                ),
-                headers=HEADERS,
-                verify=True
-            )
-
-    def test_filter(self):
-        with patch(
-            'pynetbox.lib.query.requests.get',
-            return_value=Response(fixture='{}/{}.json'.format(
-                self.app,
-                self.name
-            ))
-        ) as mock:
-            ret = getattr(nb, self.name).filter(pk=1)
-            self.assertTrue(ret)
-            self.assertTrue(isinstance(ret, list))
-            self.assertTrue(isinstance(ret[0], self.ret))
-            mock.assert_called_with(
-                'http://localhost:8000/api/{}/{}/?pk=1'.format(
+                    self.name
+                ))
+            ) as mock:
+                ret = getattr(nb, self.name).all()
+                self.assertTrue(ret)
+                self.assertTrue(isinstance(ret, list))
+                self.assertTrue(isinstance(ret[0], self.ret))
+                mock.assert_called_with(
+                    'http://localhost:8000/api/{}/{}/'.format(
+                        self.app,
+                        self.name.replace('_', '-')
+                    ),
+                    headers=HEADERS,
+                    verify=True
+                )
+
+        def test_filter(self):
+            with patch(
+                'pynetbox.core.query.requests.get',
+                return_value=Response(fixture='{}/{}.json'.format(
                     self.app,
-                    self.name.replace('_', '-')
-                ),
-                headers=HEADERS,
-                verify=True
-            )
-
-    def test_get(self):
-        with patch(
-            'pynetbox.lib.query.requests.get',
-            return_value=Response(fixture='{}/{}.json'.format(
-                self.app,
-                self.name[:-1]
-            ))
-        ) as mock:
-            ret = getattr(nb, self.name).get(1)
-            self.assertTrue(ret)
-            self.assertTrue(isinstance(ret, self.ret))
-            mock.assert_called_with(
-                'http://localhost:8000/api/{}/{}/1/'.format(
+                    self.name
+                ))
+            ) as mock:
+                ret = getattr(nb, self.name).filter(name='test')
+                self.assertTrue(ret)
+                self.assertTrue(isinstance(ret, list))
+                self.assertTrue(isinstance(ret[0], self.ret))
+                mock.assert_called_with(
+                    'http://localhost:8000/api/{}/{}/?name=test'.format(
+                        self.app,
+                        self.name.replace('_', '-')
+                    ),
+                    headers=HEADERS,
+                    verify=True
+                )
+
+        def test_get(self):
+            with patch(
+                'pynetbox.core.query.requests.get',
+                return_value=Response(fixture='{}/{}.json'.format(
                     self.app,
-                    self.name.replace('_', '-')
-                ),
-                headers=HEADERS,
-                verify=True
-            )
-
-
-class ClusterTypesTestCase(unittest.TestCase, GenericTest):
+                    self.name[:-1]
+                ))
+            ) as mock:
+                ret = getattr(nb, self.name).get(1)
+                self.assertTrue(ret)
+                self.assertTrue(isinstance(ret, self.ret))
+                mock.assert_called_with(
+                    'http://localhost:8000/api/{}/{}/1/'.format(
+                        self.app,
+                        self.name.replace('_', '-')
+                    ),
+                    headers=HEADERS,
+                    verify=True
+                )
+
+
+class ClusterTypesTestCase(Generic.Tests):
     name = 'cluster_types'
 
 
-class ClusterGroupsTestCase(unittest.TestCase, GenericTest):
+class ClusterGroupsTestCase(Generic.Tests):
     name = 'cluster_groups'
 
 
-class ClustersTestCase(unittest.TestCase, GenericTest):
+class ClustersTestCase(Generic.Tests):
     name = 'clusters'
 
 
-class VirtualMachinesTestCase(unittest.TestCase, GenericTest):
+class VirtualMachinesTestCase(Generic.Tests):
     name = 'virtual_machines'
 
 
-class InterfacesTestCase(unittest.TestCase, GenericTest):
+class InterfacesTestCase(Generic.Tests):
     name = 'interfaces'
diff --git a/tests/unit/test_endpoint.py b/tests/unit/test_endpoint.py
new file mode 100644
index 0000000..1a4d6a6
--- /dev/null
+++ b/tests/unit/test_endpoint.py
@@ -0,0 +1,40 @@
+import unittest
+
+import six
+
+from pynetbox.core.endpoint import Endpoint
+
+if six.PY3:
+    from unittest.mock import patch, Mock
+else:
+    from mock import patch, Mock
+
+
+class EndPointTestCase(unittest.TestCase):
+
+    def test_filter(self):
+        with patch(
+            "pynetbox.core.query.Request.get", return_value=Mock()
+        ) as mock:
+            api = Mock(base_url="http://localhost:8000/api")
+            app = Mock(name="test")
+            mock.return_value = [{'id': 123}, {'id': 321}]
+            test_obj = Endpoint(api, app, "test")
+            test = test_obj.filter(test="test")
+            self.assertEqual(len(test), 2)
+
+    def test_filter_empty_kwargs(self):
+
+        api = Mock(base_url="http://localhost:8000/api")
+        app = Mock(name="test")
+        test_obj = Endpoint(api, app, "test")
+        with self.assertRaises(ValueError) as _:
+            test_obj.filter()
+
+    def test_filter_reserved_kwargs(self):
+
+        api = Mock(base_url="http://localhost:8000/api")
+        app = Mock(name="test")
+        test_obj = Endpoint(api, app, "test")
+        with self.assertRaises(ValueError) as _:
+            test_obj.filter(id=1)
diff --git a/tests/unit/test_response.py b/tests/unit/test_response.py
index 031771e0..7bdb320 100644
--- a/tests/unit/test_response.py
+++ b/tests/unit/test_response.py
@@ -1,6 +1,6 @@
 import unittest
 
-from pynetbox.lib.response import Record
+from pynetbox.core.response import Record
 
 
 class RecordTestCase(unittest.TestCase):
@@ -25,7 +25,7 @@ class RecordTestCase(unittest.TestCase):
                 }
             ],
         }
-        test_obj = Record(test_values)
+        test_obj = Record(test_values, None, None)
         test = test_obj.serialize()
         self.assertEqual(test['tagged_vlans'], [1, 2])
 
@@ -34,7 +34,7 @@ class RecordTestCase(unittest.TestCase):
             'id': 123,
             'units': [12],
         }
-        test_obj = Record(test_values)
+        test_obj = Record(test_values, None, None)
         test = test_obj.serialize()
         self.assertEqual(test['units'], [12])
 
@@ -45,7 +45,30 @@ class RecordTestCase(unittest.TestCase):
                 'foo',
                 'bar',
                 'foo',
-            ]
+            ],
         }
-        test = Record(test_values).serialize()
+        test = Record(test_values, None, None).serialize()
         self.assertEqual(len(test['tags']), 2)
+
+    def test_diff(self):
+        test_values = {
+            'id': 123,
+            'custom_fields': {
+                'foo': 'bar'
+            },
+            'string_field': 'foobar',
+            'int_field': 1,
+            "nested_dict": {
+                "id": 222,
+                "name": 'bar',
+            },
+            'tags': [
+                'foo',
+                'bar',
+            ]
+        }
+        test = Record(test_values, None, None)
+        test.tags.append('baz')
+        test.nested_dict = 1
+        test.string_field = 'foobaz'
+        self.assertEqual(test._diff(), {'tags', 'nested_dict', 'string_field'})
diff --git a/tests/util.py b/tests/util.py
index c049c54..3e48159 100644
--- a/tests/util.py
+++ b/tests/util.py
@@ -1,10 +1,10 @@
 import json
 
 
-class Response:
-    def __init__(self, fixture=None, status_code=200, ok=True):
+class Response(object):
+    def __init__(self, fixture=None, status_code=200, ok=True, content=None):
         self.status_code = status_code
-        self.content = self.load_fixture(fixture)
+        self.content = json.dumps(content) if content else self.load_fixture(fixture)
         self.ok = ok
 
     def load_fixture(self, path):