Repository: nautobot/nautobot-plugin-ssot-arista-cloudvision (language: python)

Issue:
Sync interfaces when syncing devices from CloudVision to Nautobot
### Environment
* Nautobot version:  <!-- Example: 1.0.0 --> 1.0.1
* aristacv-sync version:  <!-- Example: 0.1.0 --> 1.0.2

<!--
    Describe in detail the new functionality you are proposing.
-->
### Proposed Functionality
Sync device interfaces from CloudVision to Nautobot when syncing devices. 

<!--
    Convey an example use case for your proposed feature. Write from the
    perspective of a user who would benefit from the proposed
    functionality and describe how.
--->
### Use Case
Syncing of interfaces from CloudVision to Nautobot would allow Nautobot to be the SSoT of more info.



Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index 4dfdcb7..0248f40 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 # Nautobot to Arista CloudVision Sync
 
-A plugin for [Nautobot](https://github.com/nautobot/nautobot) that allows synchronization of data directly between CloudVision and Nautobot. It synchronizes user device tags from Nautobot into CloudVision while using devices and system tags from CloudVision to ensure device synchronization and populate device metadata.  Here is a table showing the data mappings when syncing from CloudVision.
+A plugin for [Nautobot](https://github.com/nautobot/nautobot) that allows synchronization of data directly between CloudVision and Nautobot. From Nautobot into CloudVision, it synchronizes user device tags. From CloudVision into Nautobot, it synchronizes devices, their interfaces and their system tags. Here is a table showing the data mappings when syncing from CloudVision.
 
 | CloudVision System Tags | Nautobot Device Custom Field |
 |-------------------------|------------------------------|
@@ -26,6 +26,8 @@ When syncing User tags from Nautobot to CloudVision the data mappings are as fol
 
 | Nautobot | CloudVision |
 |----------|-------------|
+| Interface| Interface   |
+|----------|-------------|
 | Tags     | Device Tags |
 
 > This plugin is an extension of the [Nautobot Single Source of Truth (SSoT)](https://github.com/nautobot/nautobot-plugin-ssot) and you must have that plugin installed before installing this extension.
@@ -42,7 +44,7 @@ This screenshow shows the Nautobot to CloudVision home page. It also contains da
 
 ## Installation
 
-The plugin is available as a Python package in pypi and can be installed with pip
+The plugin is available as a Python package in PyPI and can be installed with pip
 
 ```shell
 pip install nautobot_ssot_aristacv
@@ -116,18 +118,18 @@ The plugin can connect to either on-premise or a cloud instance of CloudVision.
 |------------------------|---------|--------------------------------------------------------------------------------------------------|
 | cvp_host               | string  | Hostname or ip address of the onprem instance of CloudVision.                                    |
 | cvp_port               | string  | gRPC port (defaults to 8443, but this port has changed to 443 as of CVP 2021.3.0)                |
-| cvp_user               | string  | The username used to connect to the onprem instance of CloudVision.                              |
+| cvp_user               | string  | The username used to connect to the on-prem instance of CloudVision.                              |
 | cvp_password           | string  | The password used by the user specified above.                                                   |
-| verify               | boolean | If False, the plugin will download the certificate from CloudVision and trust it for gRPC calls.  |
+| cvp_token              | string  | Token to be used when connecting to CloudVision.                                                 |
+| verify                 | boolean | If False, the plugin will download the certificate from CloudVision and trust it for gRPC calls. |
 
 To connect to a cloud instance of CloudVision you must set the following variable:
 
 | Configuration Variable | Type   | Usage                                                         | Default            |
 |------------------------|--------|---------------------------------------------------------------|--------------------|
 | cvaas_url              | string | URL used to connect to your CvaaS instance.                   | www.arista.io:443  |
-| cvaas_token            | string | Token to be used when connecting to CloudVision as a Service. | No default set     |
 
-When syncing from CloudVision, this plugin will create new Arista devices that do not exist in Nautobot. When creating new devices in Nautobot, a site, device role, device role color, device status, and device are required. You may define which values to use by configuring the following values in your nautobot config file. If you define a `default_device_role` and `default_device_status` that already exist, the default color value for both of those will be ignored as it will pull that information from Nautobot.
+When syncing from CloudVision, this plugin will create new Arista devices that do not exist in Nautobot. When creating new devices in Nautobot, a site, device role, device role color, device status, and device are required. You may define which values to use by configuring the following values in your `nautobot_config.py` file. If you define a `default_device_role` and `default_device_status` that already exist, the default color value for both of those will be ignored as it will pull that information from Nautobot.
 
 | Configuration Variable                       | Type   | Usage                                                      | Default              |
 |----------------------------------------------|--------|------------------------------------------------------------|----------------------|
@@ -139,7 +141,7 @@ When syncing from CloudVision, this plugin will create new Arista devices that d
 
 > When these variables are not defined in the plugin settings, the plugin will use the default values mentioned.
 
-When an Arista device exists in Nautobot but not in CloudVision, this plugin can either delete or leave the device in Nautobot. That behavior can be set with the following variable in the nautobot config file.
+When an Arista device exists in Nautobot but not in CloudVision, this plugin can either delete or leave the device in Nautobot. That behavior can be set with the following variable in the `nautobot_config.py` file.
 
 | Configuration Variable           | Type    | Usage                                                                                                                                                              | Default |
 |----------------------------------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|
@@ -163,16 +165,17 @@ Lastly, you can control whether only active devices are imported or whether all
 
 ## Usage
 
-This extension can sync data both `to` and `from` Nautobot. Once the plugin has been installed succesfully two new options are available under the [Nautobot Single Source of Truth (SSoT)](https://github.com/nautobot/nautobot-plugin-ssot) plugin.
+This extension can sync data both `to` and `from` Nautobot. Once the plugin has been installed successfully two new options are available under the [Nautobot Single Source of Truth (SSoT)](https://github.com/nautobot/nautobot-plugin-ssot) plugin.
 
 ![Arista Extension](https://user-images.githubusercontent.com/38091261/124857275-9a6c0100-df71-11eb-8ace-2ddf67a2471f.PNG)
 
+Please be aware that interfaces that are part of a breakout bundle, ie a 40G port broken out into 4x10G ports, will show the base interface SFP transceiver as the interface type. This is due to the way interfaces and transceivers are returned from CloudVision.
+
 ### Syncing From CloudVision
 
 > When loading Nautobot data, this tool only loads devices with a device type that has a manufacturer of "Arista"
 
-When syncing data from CloudVision to Nautobot, system tags as well as devices are synced.  When a device exists in CloudVision that doesn't exist in Nautobot, this tool creates the device in Nautobot with the default values specified in the configuration file.
-When a device exists in Nautobot that does not exist in CloudVision, this tool can be configured to either delete or skip that device.
+When syncing data from CloudVision to Nautobot, devices along with their interfaces and tags are synchronized.  When a device exists in CloudVision that doesn't exist in Nautobot, this tool creates the device in Nautobot with the default values specified in the configuration file. When a device exists in Nautobot that does not exist in CloudVision, this tool can be configured to either delete or skip that device.
 You can watch the below video for an example.
 
 ![fromcv_sync](https://user-images.githubusercontent.com/38091261/126499331-e41946c4-4e61-4b5e-8b7f-73efb9cd8d3f.gif)
@@ -183,11 +186,11 @@ When syncing data from Nautobot to CloudVision, the tag data in Nautobot is copi
 
 ## Contributing
 
-Pull requests are welcomed and automatically built and tested against multiple version of Python and multiple version of Nautobot through TravisCI.
+Pull requests are welcomed and automatically built and tested against multiple versions of Python and multiple versions of Nautobot through GitHub Actions.
 
-The project is packaged with a light development environment based on `docker-compose` to help with the local development of the project and to run the tests within TravisCI.
+The project is packaged with a light development environment based on `docker-compose` to help with the local development of the project and to run the tests within a Docker container.
 
-The project is following Network to Code software development guideline and is leveraging:
+The project is following Network to Code software development guidelines and is leveraging:
 
 - Black, Pylint, Bandit and pydocstyle for Python linting and formatting.
 - Django unit test to ensure the plugin is working properly.
@@ -198,7 +201,7 @@ The development environment can be used in 2 ways. First, with a local poetry en
 
 #### Invoke tasks
 
-The [PyInvoke](http://www.pyinvoke.org/) library is used to provide some helper commands based on the environment.  There are a few configuration parameters which can be passed to PyInvoke to override the default configuration:
+The [PyInvoke](http://www.pyinvoke.org/) library is used to provide some helper commands based on the environment.  There are a few configuration parameters that can be passed to PyInvoke to override the default configuration:
 
 - `nautobot_ver`: the version of Nautobot to use as a base for any built docker containers (default: develop-latest)
 - `project_name`: the default docker compose project name (default: aristacv-sync)
@@ -207,7 +210,7 @@ The [PyInvoke](http://www.pyinvoke.org/) library is used to provide some helper
 - `compose_dir`: the full path to a directory containing the project compose files
 - `compose_files`: a list of compose files applied in order (see [Multiple Compose files](https://docs.docker.com/compose/extends/#multiple-compose-files) for more information)
 
-Using PyInvoke these configuration options can be overridden using [several methods](http://docs.pyinvoke.org/en/stable/concepts/configuration.html).  Perhaps the simplest is simply setting an environment variable `INVOKE_ARISTACV-SYNC_VARIABLE_NAME` where `VARIABLE_NAME` is the variable you are trying to override.  The only exception is `compose_files`, because it is a list it must be overridden in a yaml file.  There is an example `invoke.yml` in this directory which can be used as a starting point.
+Using PyInvoke these configuration options can be overridden using [several methods](http://docs.pyinvoke.org/en/stable/concepts/configuration.html).  Perhaps the simplest is simply setting an environment variable `INVOKE_ARISTACV-SYNC_VARIABLE_NAME` where `VARIABLE_NAME` is the variable you are trying to override.  The only exception is `compose_files`, because it is a list it must be overridden in a YAML file.  There is an example `invoke.yml` in this directory which can be used as a starting point.
 
 #### Local Poetry Development Environment
 
@@ -259,7 +262,7 @@ Nautobot server can now be accessed at [http://localhost:8080](http://localhost:
 
 ### CLI Helper Commands
 
-The project is coming with a CLI helper based on [invoke](http://www.pyinvoke.org/) to help setup the development environment. The commands are listed below in 3 categories `dev environment`, `utility` and `testing`.
+The project is coming with a CLI helper based on [invoke](http://www.pyinvoke.org/) to help set up the development environment. The commands are listed below in 3 categories `dev environment`, `utility` and `testing`.
 
 Each command can be executed with `invoke <command>`. Environment variables `INVOKE_ARISTACV-SYNC_PYTHON_VER` and `INVOKE_ARISTACV-SYNC_NAUTOBOT_VER` may be specified to override the default versions. Each command also has its own help `invoke <command> --help`
 
diff --git a/nautobot_ssot_aristacv/constant.py b/nautobot_ssot_aristacv/constant.py
new file mode 100644
index 0000000..e1833e8
--- /dev/null
+++ b/nautobot_ssot_aristacv/constant.py
@@ -0,0 +1,74 @@
+"""Storage of data that will not change throughout the life cycle of the application."""
+
+PORT_TYPE_MAP = {
+    "xcvr1000BaseT": "1000base-t",
+    "xcvr10GBaseSr": "10gbase-x-xfp",
+    "xcvr10GBaseLr": "10gbase-x-xfp",
+    "1000BASE-SX": "1000base-x-gbic",
+    "1000BASE-LX": "1000base-x-gbic",
+    "1000BASE-T": "1000base-t",
+    "10GBASE-T": "10gbase-t",
+    "10GBASE-MRA-T": "10gbase-t",
+    "10GBASE-CR": "10gbase-cx4",
+    "10GBASE-CR (QSFP+)": "10gbase-cx4",
+    "10GBASE-AOC": "10gbase-x-xfp",
+    "10GBASE-SR": "10gbase-x-xfp",
+    "10GBASE-SRL": "10gbase-x-xfp",
+    "10GBASE-LR": "10gbase-x-xfp",
+    "10GBASE-LRL": "10gbase-x-xfp",
+    "10GBASE-ER": "10gbase-x-sfpp",
+    "10GBASE-ERLBD": "10gbase-x-sfpp",
+    "10GBASE-ERBD": "10gbase-x-sfpp",
+    "10GBASE-ZR": "10gbase-x-sfpp",
+    "10GBASE-DWDM": "10gbase-x-x2",
+    "25GBASE-CR": "25gbase-x-sfp28",
+    "25GBASE-CR (QSFP)": "25gbase-x-sfp28",
+    "25GBASE-AOC": "25gbase-x-sfp28",
+    "25GBASE-SR": "25gbase-x-sfp28",
+    "25GBASE-MR-SR": "25gbase-x-sfp28",
+    "25GBASE-MR-XSR": "25gbase-x-sfp28",
+    "25GBASE-LR": "25gbase-x-sfp28",
+    "25GBASE-MR-LR": "25gbase-x-sfp28",
+    "40GBASE-CR4": "40gbase-x-qsfpp",
+    "40GBASE-AOC": "40gbase-x-qsfpp",
+    "40GBASE-SR4": "40gbase-x-qsfpp",
+    "40GBASE-XSR4": "40gbase-x-qsfpp",
+    "40GBASE-BIDI": "40gbase-x-qsfpp",
+    "40GBASE-LR4": "40gbase-x-qsfpp",
+    "40GBASE-LRL4": "40gbase-x-qsfpp",
+    "40GBASE-PLRL4": "40gbase-x-qsfpp",
+    "40GBASE-PLR4": "40gbase-x-qsfpp",
+    "40GBASE-ER4": "40gbase-x-qsfpp",
+    "100GBASE-CR4": "100gbase-x-cfp4",
+    "100GBASE-AOC": "100gbase-x-qsfp28",
+    "100GBASE-SR4": "100gbase-x-qsfp28",
+    "100GBASE-XSR4": "100gbase-x-qsfp28",
+    "100GBASE-SWDM4": "100gbase-x-qsfp28",
+    "100GBASE-BIDI": "100gbase-x-qsfp28",
+    "100GBASE-PSM4": "100gbase-x-qsfp28",
+    "100GBASE-PLRL4": "100gbase-x-qsfp28",
+    "100GBASE-LR4": "100gbase-x-qsfp28",
+    "100GBASE-LRL4": "100gbase-x-qsfp28",
+    "100GBASE-CWDM4": "100gbase-x-qsfp28",
+    "100GBASE-XCWDM4": "100gbase-x-qsfp28",
+    "100GBASE-ERL4": "100gbase-x-qsfp28",
+    "100GBASE-ZR4": "100gbase-x-qsfp28",
+    "100GBASE-DR": "100gbase-x-qsfp28",
+    "100GBASE-FR": "100gbase-x-qsfp28",
+    "100GBASE-LR": "100gbase-x-qsfp28",
+    "200GBASE-CR4": "200gbase-x-cfp2",
+    "200GBASE-AOC": "200gbase-x-qsfp56",
+    "200GBASE-SR4": "200gbase-x-qsfp56",
+    "200GBASE-FR4": "200gbase-x-qsfp56",
+    "400GBASE-CR8": "400gbase-x-qsfpdd",
+    "400GBASE-AOC": "400gbase-x-qsfpdd",
+    "400GBASE-SR8": "400gbase-x-qsfpdd",
+    "400GBASE-SR8-C": "400gbase-x-qsfpdd",
+    "400GBASE-DR4": "400gbase-x-qsfpdd",
+    "400GBASE-XDR4": "400gbase-x-qsfpdd",
+    "400GBASE-PLR4": "400gbase-x-qsfpdd",
+    "400GBASE-FR4": "400gbase-x-qsfpdd",
+    "400GBASE-LR4": "400gbase-x-qsfpdd",
+    "400GBASE-2FR4": "400gbase-x-osfp",
+    "400GBASE-ZR": "400gbase-x-qsfpdd",
+}
diff --git a/nautobot_ssot_aristacv/diffsync/adapters/cloudvision.py b/nautobot_ssot_aristacv/diffsync/adapters/cloudvision.py
index 645f6aa..c247458 100644
--- a/nautobot_ssot_aristacv/diffsync/adapters/cloudvision.py
+++ b/nautobot_ssot_aristacv/diffsync/adapters/cloudvision.py
@@ -1,66 +1,126 @@
 """DiffSync adapter for Arista CloudVision."""
-from diffsync import DiffSync
-from django.forms import ValidationError
-import arista.tag.v1 as TAG
-from diffsync.exceptions import ObjectAlreadyExists
 import distutils
+import re
 
-from nautobot_ssot_aristacv.diffsync.models.cloudvision import CloudvisionDevice, CloudvisionCustomField
+import arista.tag.v1 as TAG
+from diffsync import DiffSync
+from diffsync.exceptions import ObjectAlreadyExists
+from nautobot_ssot_aristacv.diffsync.models.cloudvision import (
+    CloudvisionCustomField,
+    CloudvisionDevice,
+    CloudvisionPort,
+)
+from nautobot_ssot_aristacv.utils import cloudvision
 
 
 class CloudvisionAdapter(DiffSync):
     """DiffSync adapter implementation for CloudVision user-defined device tags."""
 
     device = CloudvisionDevice
+    port = CloudvisionPort
     cf = CloudvisionCustomField
 
-    top_level = ["device"]
+    top_level = ["device", "cf"]
 
-    def __init__(self, *args, job=None, conn=None, **kwargs):
+    def __init__(self, *args, job=None, conn: cloudvision.CloudvisionApi, **kwargs):
         """Initialize the CloudVision DiffSync adapter."""
         super().__init__(*args, **kwargs)
         self.job = job
         self.conn = conn
 
-    def load(self):
-        """Load tag data from CloudVision."""
-        devices = self.conn.get_devices()
-        system_tags = self.conn.get_tags_by_type(TAG.models.CREATOR_TYPE_SYSTEM)
-
-        for dev in devices:
-            new_device = None
+    def load_devices(self):
+        """Load devices from CloudVision."""
+        for dev in cloudvision.get_devices(client=self.conn.comm_channel):
             if dev["hostname"] != "":
-                if self.job.kwargs.get("debug"):
-                    self.job.log_debug(message=f"Device being loaded: {dev}")
-                new_device = self.device(name=dev["hostname"], device_id=dev["device_id"], device_model=dev["model"])
+                new_device = self.device(
+                    name=dev["hostname"], serial=dev["device_id"], device_model=dev["model"], uuid=None
+                )
                 try:
                     self.add(new_device)
-                except ValidationError as err:
-                    self.job.log_warning(message=f"Unable to load Device {dev['hostname']}. {err}")
+                except ObjectAlreadyExists as err:
+                    self.job.log_warning(
+                        message=f"Duplicate device {dev['hostname']} {dev['device_id']} found and ignored. {err}"
+                    )
                     continue
+                self.load_interfaces(device=new_device)
+                self.load_device_tags(device=new_device)
             else:
                 self.job.log_warning(message=f"Device {dev} is missing hostname so won't be imported.")
                 continue
 
-            dev_tags = [tag for tag in self.conn.get_device_tags(device_id=dev["device_id"]) if tag in system_tags]
-
-            # Check if topology_type tag exists
-            list_of_tag_names = [value["label"] for value in dev_tags]
-            if "topology_type" not in list_of_tag_names:
-                dev_tags.append({"label": "topology_type", "value": "-"})
-
-            for tag in dev_tags:
-                if tag["label"] in ["hostname", "serialnumber", "Container"]:
-                    continue
-                if tag["label"] == "mpls" or tag["label"] == "ztp":
-                    tag["value"] = bool(distutils.util.strtobool(tag["value"]))
-
-                new_cf = self.cf(name=f"arista_{tag['label']}", value=tag["value"], device_name=dev["hostname"])
+    def load_interfaces(self, device):
+        """Load device interface from CloudVision."""
+        chassis_type = cloudvision.get_device_type(client=self.conn, dId=device.serial)
+        if self.job.kwargs.get("debug"):
+            self.job.log_debug(message=f"Chassis type for {device.name} is {chassis_type}.")
+        port_info = []
+        if chassis_type == "modular":
+            port_info = cloudvision.get_interfaces_chassis(client=self.conn, dId=device.serial)
+        elif chassis_type == "fixedSystem":
+            port_info = cloudvision.get_interfaces_fixed(client=self.conn, dId=device.serial)
+        if self.job.kwargs.get("debug"):
+            self.job.log_debug(message=f"Device being loaded: {device.name}. Port: {port_info}.")
+        for port in port_info:
+            if self.job.kwargs.get("debug"):
+                self.job.log_debug(message=f"Port {port['interface']} being loaded for {device.name}.")
+            port_mode = cloudvision.get_interface_mode(client=self.conn, dId=device.serial, interface=port)
+            transceiver = cloudvision.get_interface_transceiver(client=self.conn, dId=device.serial, interface=port)
+            if transceiver == "Unknown":
+                # Breakout transceivers, ie 40G -> 4x10G, shows up as 4 interfaces and requires looking at base interface to find transceiver, ie Ethernet1 if Ethernet1/1
+                base_port_name = re.sub(r"/\d", "", port["interface"])
+                transceiver = cloudvision.get_interface_transceiver(
+                    client=self.conn, dId=device.serial, interface=base_port_name
+                )
+            port_status = cloudvision.get_interface_status(port_info=port)
+            port_type = cloudvision.get_port_type(port_info=port, transceiver=transceiver)
+            if port["interface"] != "":
+                new_port = self.port(
+                    name=port["interface"],
+                    device=device.name,
+                    mac_addr=port["mac_addr"] if port.get("mac_addr") else "",
+                    mode="tagged" if port_mode == "trunk" else "access",
+                    mtu=port["mtu"],
+                    enabled=port["enabled"],
+                    status=port_status,
+                    port_type=port_type,
+                    uuid=None,
+                )
                 try:
-                    self.add(new_cf)
-                    if new_device:
-                        new_device.add_child(new_cf)
-                except ObjectAlreadyExists:
+                    self.add(new_port)
+                    device.add_child(new_port)
+                except ObjectAlreadyExists as err:
                     self.job.log_warning(
-                        message=f"Duplicate object encountered for {tag['label']} on device {dev['hostname']}"
+                        message=f"Duplicate port {port['interface']} found for {device.name} and ignored. {err}"
                     )
+
+    def load_device_tags(self, device):
+        """Load device tags from CloudVision."""
+        system_tags = cloudvision.get_tags_by_type(
+            client=self.conn.comm_channel, creator_type=TAG.models.CREATOR_TYPE_SYSTEM
+        )
+        dev_tags = [
+            tag
+            for tag in cloudvision.get_device_tags(client=self.conn.comm_channel, device_id=device.serial)
+            if tag in system_tags
+        ]
+
+        # Check if topology_type tag exists
+        list_of_tag_names = [value["label"] for value in dev_tags]
+        if "topology_type" not in list_of_tag_names:
+            dev_tags.append({"label": "topology_type", "value": "-"})
+
+        for tag in dev_tags:
+            if tag["label"] in ["hostname", "serialnumber", "Container"]:
+                continue
+            if tag["label"] == "mpls" or tag["label"] == "ztp":
+                tag["value"] = bool(distutils.util.strtobool(tag["value"]))
+
+            new_cf = self.cf(name=f"arista_{tag['label']}", value=tag["value"], device_name=device.name)
+            try:
+                self.add(new_cf)
+            except ObjectAlreadyExists:
+                self.job.log_warning(message=f"Duplicate tag encountered for {tag['label']} on device {device.name}")
+
+    def load(self):
+        """Load devices and associated data from CloudVision."""
+        self.load_devices()
diff --git a/nautobot_ssot_aristacv/diffsync/adapters/nautobot.py b/nautobot_ssot_aristacv/diffsync/adapters/nautobot.py
index 82dc1f6..fd5de99 100644
--- a/nautobot_ssot_aristacv/diffsync/adapters/nautobot.py
+++ b/nautobot_ssot_aristacv/diffsync/adapters/nautobot.py
@@ -1,17 +1,20 @@
 """DiffSync adapter for Nautobot."""
 from nautobot.dcim.models import Device as OrmDevice
+from nautobot.dcim.models import Interface as OrmInterface
 from diffsync import DiffSync
+from diffsync.exceptions import ObjectNotFound
 
-from nautobot_ssot_aristacv.diffsync.models.nautobot import NautobotDevice, NautobotCustomField
+from nautobot_ssot_aristacv.diffsync.models.nautobot import NautobotDevice, NautobotCustomField, NautobotPort
 
 
 class NautobotAdapter(DiffSync):
     """DiffSync adapter implementation for Nautobot custom fields."""
 
     device = NautobotDevice
+    port = NautobotPort
     cf = NautobotCustomField
 
-    top_level = ["device"]
+    top_level = ["device", "cf"]
 
     def __init__(self, *args, job=None, **kwargs):
         """Initialize the Nautobot DiffSync adapter."""
@@ -24,7 +27,9 @@ def load(self):
         for dev in devices:
             try:
                 if dev.device_type.manufacturer.name.lower() == "arista":
-                    new_device = self.device(name=dev.name, device_model=dev.device_type.name)
+                    new_device = self.device(
+                        name=dev.name, device_model=dev.device_type.name, serial=dev.serial, uuid=dev.id
+                    )
                     self.add(new_device)
                     dev_custom_fields = dev.custom_field_data
 
@@ -33,11 +38,30 @@ def load(self):
                             cf_value = ""
                         new_cf = self.cf(name=cf_name, value=cf_value, device_name=dev.name)
                         self.add(new_cf)
-                        new_device.add_child(new_cf)
 
                     # Gets model from device and puts it into CustomField Object.
                     new_cf = self.cf(name="arista_model", value=str(dev.platform), device_name=dev.name)
                     self.add(new_cf)
-                    new_device.add_child(new_cf)
             except AttributeError:
                 continue
+
+        for intf in OrmInterface.objects.all():
+            new_port = self.port(
+                name=intf.name,
+                device=intf.device.name,
+                mac_addr=intf.mac_address if intf.mac_address else "",
+                enabled=intf.enabled,
+                mode=intf.mode,
+                mtu=intf.mtu,
+                port_type=intf.type,
+                status=intf.status.slug,
+                uuid=intf.id,
+            )
+            self.add(new_port)
+            try:
+                dev = self.get(self.device, intf.device.name)
+                dev.add_child(new_port)
+            except ObjectNotFound as err:
+                self.job.log_warning(
+                    message=f"Unable to find Device {intf.device.name} in diff to assign to port {intf.name}. {err}"
+                )
diff --git a/nautobot_ssot_aristacv/diffsync/models/base.py b/nautobot_ssot_aristacv/diffsync/models/base.py
index f9790ed..71b0a57 100644
--- a/nautobot_ssot_aristacv/diffsync/models/base.py
+++ b/nautobot_ssot_aristacv/diffsync/models/base.py
@@ -1,6 +1,7 @@
 """DiffSyncModel subclasses for Nautobot-to-AristaCV data sync."""
+from uuid import UUID
 from diffsync import DiffSyncModel
-from typing import List
+from typing import List, Optional
 
 
 class Device(DiffSyncModel):
@@ -8,13 +9,46 @@ class Device(DiffSyncModel):
 
     _modelname = "device"
     _identifiers = ("name",)
-    _shortname = ()
-    _attributes = ("device_model",)
-    _children = {"cf": "cfs"}
+    _attributes = (
+        "device_model",
+        "serial",
+    )
+    _children = {"port": "ports"}
 
     name: str
-    cfs: List = list()
     device_model: str
+    serial: str
+    ports: List["Port"] = list()
+    uuid: Optional[UUID]
+
+
+class Port(DiffSyncModel):
+    """Port Model."""
+
+    _modelname = "port"
+    _identifiers = (
+        "name",
+        "device",
+    )
+    _attributes = (
+        "mac_addr",
+        "enabled",
+        "mode",
+        "mtu",
+        "port_type",
+        "status",
+    )
+    _children = {}
+
+    name: str
+    device: str
+    mac_addr: str
+    enabled: bool
+    mode: str
+    mtu: Optional[int]
+    port_type: str
+    status: str
+    uuid: Optional[UUID]
 
 
 class CustomField(DiffSyncModel):
@@ -22,8 +56,8 @@ class CustomField(DiffSyncModel):
 
     _modelname = "cf"
     _identifiers = ("name", "device_name")
-    _shortname = ()
     _attributes = ("value",)
+    _children = {}
 
     name: str
     value: str
diff --git a/nautobot_ssot_aristacv/diffsync/models/cloudvision.py b/nautobot_ssot_aristacv/diffsync/models/cloudvision.py
index 6bef401..c47d1ae 100644
--- a/nautobot_ssot_aristacv/diffsync/models/cloudvision.py
+++ b/nautobot_ssot_aristacv/diffsync/models/cloudvision.py
@@ -1,6 +1,6 @@
 """Cloudvision DiffSync models for AristaCV SSoT."""
 from django.conf import settings
-from nautobot_ssot_aristacv.diffsync.models.base import Device, CustomField
+from nautobot_ssot_aristacv.diffsync.models.base import Device, CustomField, Port
 from nautobot_ssot_aristacv.utils.cloudvision import CloudvisionApi
 
 PLUGIN_SETTINGS = settings.PLUGINS_CONFIG["nautobot_ssot_aristacv"]
@@ -23,6 +23,23 @@ def delete(self):
         return self
 
 
+class CloudvisionPort(Port):
+    """Nautobot Port model."""
+
+    @classmethod
+    def create(cls, diffsync, ids, attrs):
+        """Create Interface in AristaCV from Port object."""
+        return super().create(ids=ids, diffsync=diffsync, attrs=attrs)
+
+    def update(self, attrs):
+        """Update Interface in AristaCV from Port object."""
+        return super().update(attrs)
+
+    def delete(self):
+        """Delete Interface in AristaCV from Port object."""
+        return self
+
+
 class CloudvisionCustomField(CustomField):
     """Cloudvision CustomField model."""
 
diff --git a/nautobot_ssot_aristacv/diffsync/models/nautobot.py b/nautobot_ssot_aristacv/diffsync/models/nautobot.py
index 6b3f8c2..7221f9b 100644
--- a/nautobot_ssot_aristacv/diffsync/models/nautobot.py
+++ b/nautobot_ssot_aristacv/diffsync/models/nautobot.py
@@ -1,10 +1,12 @@
 """Nautobot DiffSync models for AristaCV SSoT."""
 from django.core.exceptions import ValidationError
 from django.conf import settings
+from nautobot.core.settings_funcs import is_truthy
 from nautobot.dcim.models import Device as OrmDevice
+from nautobot.dcim.models import Interface as OrmInterface
 from nautobot.dcim.models import Platform as OrmPlatform
-from nautobot.extras.models import CustomField as OrmCustomField
-from nautobot_ssot_aristacv.diffsync.models.base import Device, CustomField
+from nautobot.extras.models import Status as OrmStatus
+from nautobot_ssot_aristacv.diffsync.models.base import Device, CustomField, Port
 from nautobot_ssot_aristacv.utils import nautobot
 import distutils
 
@@ -17,6 +19,7 @@
 DEFAULT_DELETE_DEVICES_ON_SYNC = False
 APPLY_IMPORT_TAG = False
 MISSING_CUSTOM_FIELDS = []
+PLUGIN_SETTINGS = settings.PLUGINS_CONFIG["nautobot_ssot_aristacv"]
 
 
 class NautobotDevice(Device):
@@ -25,20 +28,19 @@ class NautobotDevice(Device):
     @classmethod
     def create(cls, diffsync, ids, attrs):
         """Create device object in Nautobot."""
-        configs = settings.PLUGINS_CONFIG.get("nautobot_ssot_aristacv", {})
-        default_site_object = nautobot.verify_site(configs.get("from_cloudvision_default_site", DEFAULT_SITE))
+        default_site_object = nautobot.verify_site(PLUGIN_SETTINGS.get("from_cloudvision_default_site", DEFAULT_SITE))
 
         device_type_cv = attrs["device_model"]
 
         device_type_object = nautobot.verify_device_type_object(device_type_cv)
         device_role_object = nautobot.verify_device_role_object(
-            configs.get("from_cloudvision_default_device_role", DEFAULT_DEVICE_ROLE),
-            configs.get("from_cloudvision_default_device_role_color", DEFAULT_DEVICE_ROLE_COLOR),
+            PLUGIN_SETTINGS.get("from_cloudvision_default_device_role", DEFAULT_DEVICE_ROLE),
+            PLUGIN_SETTINGS.get("from_cloudvision_default_device_role_color", DEFAULT_DEVICE_ROLE_COLOR),
         )
 
         device_status = nautobot.verify_device_status(
-            configs.get("from_cloudvision_default_device_status", DEFAULT_DEVICE_STATUS),
-            configs.get("from_cloudvision_default_device_status_color", DEFAULT_DEVICE_STATUS_COLOR),
+            PLUGIN_SETTINGS.get("from_cloudvision_default_device_status", DEFAULT_DEVICE_STATUS),
+            PLUGIN_SETTINGS.get("from_cloudvision_default_device_status_color", DEFAULT_DEVICE_STATUS_COLOR),
         )
 
         new_device = OrmDevice(
@@ -47,38 +49,98 @@ def create(cls, diffsync, ids, attrs):
             device_role=device_role_object,
             site=default_site_object,
             name=ids["name"],
+            serial=attrs["serial"] if attrs.get("serial") else "",
         )
-
-        new_device = nautobot.assign_arista_cf(new_device)
-
-        new_device.validated_save()
-
-        if configs.get("apply_import_tag", APPLY_IMPORT_TAG):
+        if PLUGIN_SETTINGS.get("apply_import_tag", APPLY_IMPORT_TAG):
             import_tag = nautobot.verify_import_tag()
             new_device.tags.add(import_tag)
+        try:
             new_device.validated_save()
-
-        return super().create(ids=ids, diffsync=diffsync, attrs=attrs)
+            return super().create(ids=ids, diffsync=diffsync, attrs=attrs)
+        except ValidationError as err:
+            diffsync.job.log_warning(message=f"Unable to create Device {ids['name']}. {err}")
+            return None
 
     def update(self, attrs):
         """Update device object in Nautobot."""
-        return super().update(attrs)
+        dev = OrmDevice.objects.get(id=self.uuid)
+        if "device_model" in attrs:
+            dev.device_type = nautobot.verify_device_type_object(attrs["device_model"])
+        if "serial" in attrs:
+            dev.serial = attrs["serial"]
+        try:
+            dev.validated_save()
+            return super().update(attrs)
+        except ValidationError as err:
+            self.diffsync.job.log_warning(message=f"Unable to update Device {self.name}. {err}")
+            return None
 
     def delete(self):
         """Delete device object in Nautobot."""
-        configs = settings.PLUGINS_CONFIG.get("nautobot_ssot_aristacv", {})
-        if configs.get("delete_devices_on_sync", DEFAULT_DELETE_DEVICES_ON_SYNC):
+        if PLUGIN_SETTINGS.get("delete_devices_on_sync", DEFAULT_DELETE_DEVICES_ON_SYNC):
             self.diffsync.job.log_warning(message=f"Device {self.name} will be deleted per plugin settings.")
-            device = OrmDevice.objects.get(name=self.name)
+            device = OrmDevice.objects.get(id=self.uuid)
             device.delete()
             super().delete()
         return self
 
-    def ensure_default_cf(obj, model):
-        """Update objects's default custom fields."""
-        for cf in OrmCustomField.objects.get_for_model(model):
-            if (cf.default is not None) and (cf.name not in obj.cf):
-                obj.cf[cf.name] = cf.default
+
+class NautobotPort(Port):
+    """Nautobot Port model."""
+
+    @classmethod
+    def create(cls, diffsync, ids, attrs):
+        """Create Interface in Nautobot."""
+        device = OrmDevice.objects.get(name=ids["device"])
+        new_port = OrmInterface(
+            name=ids["name"],
+            device=device,
+            enabled=is_truthy(attrs["enabled"]),
+            mtu=attrs["mtu"],
+            mode=attrs["mode"],
+            status=OrmStatus.objects.get(slug=attrs["status"]),
+            type=attrs["port_type"],
+        )
+        try:
+            new_port.validated_save()
+            return super().create(ids=ids, diffsync=diffsync, attrs=attrs)
+        except ValidationError as err:
+            diffsync.job.log_warning(message=err)
+            return None
+
+    def update(self, attrs):
+        """Update Interface in Nautobot."""
+        _port = OrmInterface.objects.get(id=self.uuid)
+        if "mac_addr" in attrs:
+            _port.mac_address = attrs["mac_addr"]
+        if "mode" in attrs:
+            _port.mode = attrs["mode"]
+        if "enabled" in attrs:
+            _port.enabled = is_truthy(attrs["enabled"])
+        if "mtu" in attrs:
+            _port.mtu = attrs["mtu"]
+        if "status" in attrs:
+            _port.status = OrmStatus.objects.get(slug=attrs["status"])
+        if "port_type" in attrs:
+            _port.type = attrs["port_type"]
+        try:
+            _port.validated_save()
+            return super().update(attrs)
+        except ValidationError as err:
+            self.diffsync.job.log_warning(
+                message=f"Unable to update port {self.name} for {self.device} with {attrs}: {err}"
+            )
+            return None
+
+    def delete(self):
+        """Delete Interface in Nautobot."""
+        if PLUGIN_SETTINGS.get("delete_devices_on_sync"):
+            super().delete()
+            if self.diffsync.job.kwargs.get("debug"):
+                self.diffsync.job.log_warning(message=f"Interface {self.name} for {self.device} will be deleted.")
+            _port = OrmInterface.objects.get(id=self.uuid)
+            _port.delete()
+        return self
 
 
 class NautobotCustomField(CustomField):
diff --git a/nautobot_ssot_aristacv/jobs.py b/nautobot_ssot_aristacv/jobs.py
index feb0082..13f46dc 100644
--- a/nautobot_ssot_aristacv/jobs.py
+++ b/nautobot_ssot_aristacv/jobs.py
@@ -122,17 +122,17 @@ def sync_data(self):
                 message="Devices not present in Cloudvision but present in Nautobot will not be deleted from Nautobot."
             )
         self.log("Connecting to CloudVision")
-        cvp = CloudvisionApi(
+        with CloudvisionApi(
             cvp_host=PLUGIN_SETTINGS["cvp_host"],
             cvp_port=PLUGIN_SETTINGS.get("cvp_port", "8443"),
             verify=PLUGIN_SETTINGS["verify"],
             username=PLUGIN_SETTINGS["cvp_user"],
             password=PLUGIN_SETTINGS["cvp_password"],
             cvp_token=PLUGIN_SETTINGS["cvp_token"],
-        )
-        self.log("Loading data from CloudVision")
-        cv = CloudvisionAdapter(job=self, conn=cvp)
-        cv.load()
+        ) as cvp:
+            self.log("Loading data from CloudVision")
+            cv = CloudvisionAdapter(job=self, conn=cvp)
+            cv.load()
         self.log("Loading data from Nautobot")
         nb = NautobotAdapter(job=self)
         nb.load()
@@ -149,7 +149,6 @@ def sync_data(self):
                 self.log_failure("Sync failed.")
                 raise e
             self.log_success(message="Sync complete.")
-        cvp.disconnect()
 
     def lookup_object(self, model_name, unique_id):
         """Lookup object for SSoT plugin integration."""
@@ -201,17 +200,17 @@ def data_mappings(cls):
     def sync_data(self):
         """Sync device tags from CloudVision to Nautobot."""
         self.log("Connecting to CloudVision")
-        cvp = CloudvisionApi(
+        with CloudvisionApi(
             cvp_host=PLUGIN_SETTINGS["cvp_host"],
             cvp_port=PLUGIN_SETTINGS.get("cvp_port", "8443"),
             verify=PLUGIN_SETTINGS["verify"],
             username=PLUGIN_SETTINGS["cvp_user"],
             password=PLUGIN_SETTINGS["cvp_password"],
             cvp_token=PLUGIN_SETTINGS["cvp_token"],
-        )
-        self.log("Loading data from CloudVision")
-        cv = CloudvisionAdapter(job=self, conn=cvp)
-        cv.load()
+        ) as cvp:
+            self.log("Loading data from CloudVision")
+            cv = CloudvisionAdapter(job=self, conn=cvp)
+            cv.load()
         self.log("Loading data from Nautobot")
         nb = NautobotAdapter()
         nb.load()
@@ -230,7 +229,6 @@ def sync_data(self):
                 self.log_failure("Sync failed.")
                 raise e
             self.log_success(message="Sync complete")
-        cvp.disconnect()
 
     def lookup_object(self, model_name, unique_id):
         """Lookup object for SSoT plugin integration."""
diff --git a/nautobot_ssot_aristacv/utils/cloudvision.py b/nautobot_ssot_aristacv/utils/cloudvision.py
index d3b2bef..991fa0f 100644
--- a/nautobot_ssot_aristacv/utils/cloudvision.py
+++ b/nautobot_ssot_aristacv/utils/cloudvision.py
@@ -1,17 +1,33 @@
+# pylint: disable=invalid-name, no-member
 """Utility functions for CloudVision Resource API."""
 import ssl
+from datetime import datetime
+from typing import Any, Iterable, List, Optional, Tuple, Union
 
-import requests
+import google.protobuf.timestamp_pb2 as pbts
 import grpc
+import requests
 from arista.inventory.v1 import models, services
 from arista.tag.v1 import models as tag_models
 from arista.tag.v1 import services as tag_services
-from google.protobuf.wrappers_pb2 import StringValue  # pylint: disable=no-name-in-module
 from django.conf import settings
+from google.protobuf.wrappers_pb2 import StringValue  # pylint: disable=no-name-in-module
 
-RPC_TIMEOUT = 30
+import cloudvision.Connector.gen.notification_pb2 as ntf
+import cloudvision.Connector.gen.router_pb2 as rtr
+import cloudvision.Connector.gen.router_pb2_grpc as rtr_client
+from cloudvision.Connector import codec
+from cloudvision.Connector.codec import Wildcard
+from cloudvision.Connector.codec.custom_types import FrozenDict
+from cloudvision.Connector.grpc_client.grpcClient import create_query, to_pbts
+
+from nautobot_ssot_aristacv.constant import PORT_TYPE_MAP
 
+RPC_TIMEOUT = 30
 PLUGIN_SETTINGS = settings.PLUGINS_CONFIG["nautobot_ssot_aristacv"]
+TIME_TYPE = Union[pbts.Timestamp, datetime]
+UPDATE_TYPE = Tuple[Any, Any]
+UPDATES_TYPE = List[UPDATE_TYPE]
 
 
 class AuthFailure(Exception):
@@ -25,7 +41,9 @@ def __init__(self, error_code, message):
 
 
 class CloudvisionApi:  # pylint: disable=too-many-instance-attributes, too-many-arguments
-    """Arista Cloudvision Api."""
+    """Arista Cloudvision gRPC client."""
+
+    AUTH_KEY_PATH = "access_token"
 
     def __init__(
         self,
@@ -37,7 +55,7 @@ def __init__(
         cvp_token: str = None,
     ):
         """Create Cloudvision API connection."""
-        self.comm_channel = None
+        self.metadata = None
         self.cvp_host = cvp_host
         self.cvp_port = cvp_port
         self.cvp_url = f"{cvp_host}:{cvp_port}"
@@ -46,19 +64,14 @@ def __init__(
         self.password = password
         self.cvp_token = cvp_token
 
-        self.connect()
-
-    def connect(self):
-        """Connect shared gRPC channel to the configured CloudVision instance."""
         # If CVP_HOST is defined, we assume an on-prem installation.
         if self.cvp_host:
-            # If we don't want to verify the cert, it will be downloaded from the server and automatically trusted for gRPC.
             if self.verify:
-                # Otherwise, the server is expected to have a valid certificate signed by a well-known CA.
                 channel_creds = grpc.ssl_channel_credentials()
             else:
-                cert = bytes(ssl.get_server_certificate((self.cvp_host, int(self.cvp_port))), "utf-8")
-                channel_creds = grpc.ssl_channel_credentials(cert)
+                channel_creds = grpc.ssl_channel_credentials(
+                    bytes(ssl.get_server_certificate((self.cvp_host, int(self.cvp_port))), "utf-8")
+                )
             if self.cvp_token:
                 call_creds = grpc.access_token_call_credentials(self.cvp_token)
             elif self.username != "" and self.password != "":  # nosec
@@ -72,11 +85,14 @@ def connect(self):
                     error_code = response.json().get("errorCode")
                     error_message = response.json().get("errorMessage")
                     raise AuthFailure(error_code, error_message)
+                if not self.cvp_token:
+                    self.cvp_token = session_id
                 call_creds = grpc.access_token_call_credentials(session_id)
             else:
                 raise AuthFailure(
                     error_code="Missing Credentials", message="Unable to authenticate due to missing credentials."
                 )
+            self.metadata = ((self.AUTH_KEY_PATH, self.cvp_token),)
         # Set up credentials for CVaaS using supplied token.
         else:
             self.cvp_url = PLUGIN_SETTINGS.get("cvaas_url", "www.arista.io:443")
@@ -84,144 +100,548 @@ def connect(self):
             channel_creds = grpc.ssl_channel_credentials()
         conn_creds = grpc.composite_channel_credentials(channel_creds, call_creds)
         self.comm_channel = grpc.secure_channel(self.cvp_url, conn_creds)
+        self.__client = rtr_client.RouterV1Stub(self.comm_channel)
+        self.__auth_client = rtr_client.AuthStub(self.comm_channel)
+        self.__search_client = rtr_client.SearchStub(self.comm_channel)
+        self.encoder = codec.Encoder()
+        self.decoder = codec.Decoder()
+
+    def __enter__(self):
+        """Magic method to enable use of class with `with` statement."""
+        return self
+
+    def __exit__(self, exit_type, value, traceback):
+        """Magic method for exiting context manager when using with `with` statement."""
+        return self.comm_channel.__exit__(exit_type, value, traceback)
 
-    def disconnect(self):
+    def close(self):
         """Close the shared gRPC channel."""
         self.comm_channel.close()
 
-    def get_devices(self):
-        """Get active devices from CloudVision inventory."""
-        device_stub = services.DeviceServiceStub(self.comm_channel)
-        if PLUGIN_SETTINGS.get("import_active"):
-            req = services.DeviceStreamRequest(
-                partial_eq_filter=[models.Device(streaming_status=models.STREAMING_STATUS_ACTIVE)]
-            )
-        else:
-            req = services.DeviceStreamRequest()
-        responses = device_stub.GetAll(req)
-        devices = []
-        for resp in responses:
-            device = {
-                "device_id": resp.value.key.device_id.value,
-                "hostname": resp.value.hostname.value,
-                "fqdn": resp.value.fqdn.value,
-                "sw_ver": resp.value.software_version.value,
-                "model": resp.value.model_name.value,
-                "system_mac_address": resp.value.system_mac_address.value,
-            }
-            devices.append(device)
-        return devices
-
-    def get_device_id(self, device_name: str):
-        """Get device_id for device_name from CloudVision inventory."""
-        device_stub = services.DeviceServiceStub(self.comm_channel)
-        req = services.DeviceStreamRequest(
-            partial_eq_filter=[models.Device(hostname=device_name, streaming_status=models.STREAMING_STATUS_ACTIVE)]
+    def get(
+        self,
+        queries: List[rtr.Query],
+        start: Optional[TIME_TYPE] = None,
+        end: Optional[TIME_TYPE] = None,
+        versions=0,
+        sharding=None,
+        exact_range=False,
+    ):
+        """Get creates and executes a Get protobuf message, returning a stream of notificationBatch.
+
+        queries must be a list of query protobuf messages.
+        start and end, if present, must be nanoseconds timestamps (uint64).
+        sharding, if present must be a protobuf sharding message.
+        """
+        end_ts = 0
+        start_ts = 0
+        if end:
+            end_ts = to_pbts(end).ToNanoseconds()
+
+        if start:
+            start_ts = to_pbts(start).ToNanoseconds()
+
+        request = rtr.GetRequest(
+            query=queries,
+            start=start_ts,
+            end=end_ts,
+            versions=versions,
+            sharded_sub=sharding,
+            exact_range=exact_range,
         )
-        resp = device_stub.GetOne(req)
-        return resp.value.key.device_id.value
-
-    def get_tags(self):
-        """Get all tags from CloudVision."""
-        tag_stub = tag_services.DeviceTagServiceStub(self.comm_channel)
-        req = tag_services.DeviceTagStreamRequest()
-        responses = tag_stub.GetAll(req)
-        tags = []
-        for resp in responses:
-            dev_tag = {
-                "label": resp.value.key.label.value,
-                "value": resp.value.key.value.value,
-                "creator_type": resp.value.creator_type,
-            }
-            tags.append(dev_tag)
-        return tags
-
-    def get_tags_by_type(self, creator_type: int = tag_models.CREATOR_TYPE_USER):
-        """Get tags by creator type from CloudVision."""
-        tag_stub = tag_services.DeviceTagServiceStub(self.comm_channel)
-        req = tag_services.DeviceTagStreamRequest(partial_eq_filter=[tag_models.DeviceTag(creator_type=creator_type)])
-        responses = tag_stub.GetAll(req)
-        tags = []
-        for resp in responses:
-            dev_tag = {
-                "label": resp.value.key.label.value,
-                "value": resp.value.key.value.value,
-            }
-            tags.append(dev_tag)
-        return tags
-
-    def get_device_tags(self, device_id: str):
-        """Get tags for specific device."""
-        tag_stub = tag_services.DeviceTagAssignmentConfigServiceStub(self.comm_channel)
-        req = tag_services.DeviceTagAssignmentConfigStreamRequest(
-            partial_eq_filter=[
-                tag_models.DeviceTagAssignmentConfig(
-                    key=tag_models.DeviceTagAssignmentKey(
-                        device_id=StringValue(value=device_id),
-                    )
-                )
-            ]
+        stream = self.__client.Get(request, metadata=self.metadata)
+        return (self.decode_batch(nb) for nb in stream)
+
+    def subscribe(self, queries, sharding=None):
+        """Subscribe creates and executes a Subscribe protobuf message, returning a stream of notificationBatch.
+
+        queries must be a list of query protobuf messages.
+        sharding, if present must be a protobuf sharding message.
+        """
+        req = rtr.SubscribeRequest(query=queries, sharded_sub=sharding)
+        stream = self.__client.Subscribe(req, metadata=self.metadata)
+        return (self.decode_batch(nb) for nb in stream)
+
+    def publish(
+        self,
+        dId,
+        notifs: List[ntf.Notification],
+        dtype: str = "device",
+        sync: bool = True,
+        compare: Optional[UPDATE_TYPE] = None,
+    ) -> None:
+        """Publish creates and executes a Publish protobuf message.
+
+        refer to cloudvision/Connector/protobufs/router.proto:124
+        default to sync publish being true so that changes are reflected
+        """
+        comp_pb = None
+        if compare:
+            key = compare[0]
+            value = compare[1]
+            comp_pb = ntf.Notification.Update(key=self.encoder.encode(key), value=self.encoder.encode(value))
+
+        req = rtr.PublishRequest(
+            batch=ntf.NotificationBatch(d="device", dataset=ntf.Dataset(type=dtype, name=dId), notifications=notifs),
+            sync=sync,
+            compare=comp_pb,
         )
-        responses = tag_stub.GetAll(req)
-        tags = []
-        for resp in responses:
-            dev_tag = {
-                "label": resp.value.key.label.value,
-                "value": resp.value.key.value.value,
-            }
-            tags.append(dev_tag)
-        return tags
-
-    def create_tag(self, label: str, value: str):
-        """Create user-defined tag in CloudVision."""
-        tag_stub = tag_services.DeviceTagConfigServiceStub(self.comm_channel)
-        req = tag_services.DeviceTagConfigSetRequest(
-            value=tag_models.DeviceTagConfig(
-                key=tag_models.TagKey(label=StringValue(value=label), value=StringValue(value=value))
-            )
+        self.__client.Publish(req, metadata=self.metadata)
+
+    def get_datasets(self, types: Optional[List[str]] = None):
+        """Get Datasets retrieves all the datasets streaming on CloudVision.
+
+        types, if present, filter the queried dataset by types
+        """
+        req = rtr.DatasetsRequest(types=types)
+        stream = self.__client.GetDatasets(req, metadata=self.metadata)
+        return stream
+
+    def create_dataset(self, dtype, dId) -> None:
+        """Create Datasets will create a dataset request on CloudVision."""
+        req = rtr.CreateDatasetRequest(dataset=ntf.Dataset(type=dtype, name=dId))
+        self.__auth_client.CreateDataset(req, metadata=self.metadata)
+
+    def decode_batch(self, batch):
+        """Decode a batch of notifications from CloudVision."""
+        res = {
+            "dataset": {"name": batch.dataset.name, "type": batch.dataset.type},
+            "notifications": [self.decode_notification(n) for n in batch.notifications],
+        }
+        return res
+
+    def decode_notification(self, notif):
+        """Decode notifications into standardize format."""
+        res = {
+            "timestamp": notif.timestamp,
+            "deletes": [self.decoder.decode(d) for d in notif.deletes],
+            "updates": {self.decoder.decode(u.key): self.decoder.decode(u.value) for u in notif.updates},
+            "retracts": [self.decoder.decode(r) for r in notif.retracts],
+            "path_elements": [self.decoder.decode(elt) for elt in notif.path_elements],
+        }
+        return res
+
+    def search(  # pylint:disable=dangerous-default-value, too-many-locals
+        self,
+        search_type=rtr.SearchRequest.CUSTOM,
+        d_type: str = "device",
+        d_name: str = "",
+        result_size: int = 1,
+        start: Optional[TIME_TYPE] = None,
+        end: Optional[TIME_TYPE] = None,
+        path_elements=[],
+        key_filters: Iterable[rtr.Filter] = [],
+        value_filters: Iterable[rtr.Filter] = [],
+        exact_range: bool = False,
+        offset: int = 0,
+        exact_term: bool = False,
+        sort: Iterable[rtr.Sort] = [],
+        count_only: bool = False,
+    ):
+        """Format a search request to CloudVision."""
+        start_ts = to_pbts(start).ToNanoseconds() if start else 0
+        end_ts = to_pbts(end).ToNanoseconds() if end else 0
+        encoded_path_elements = [self.encoder.encode(x) for x in path_elements]
+        req = rtr.SearchRequest(
+            search_type=search_type,
+            start=start_ts,
+            end=end_ts,
+            query=[
+                rtr.Query(
+                    dataset=ntf.Dataset(type=d_type, name=d_name), paths=[rtr.Path(path_elements=encoded_path_elements)]
+                )
+            ],
+            result_size=result_size,
+            key_filters=key_filters,
+            value_filters=value_filters,
+            exact_range=exact_range,
+            offset=offset,
+            exact_term=exact_term,
+            sort=sort,
+            count_only=count_only,
         )
-        try:
-            tag_stub.Set(req)
-        except grpc.RpcError as err:
-            # Ignore RPC error if tag already exists for idempotency
-            print(f"Failure to create tag: {err}")
-            raise err
-
-    def delete_tag(self, label: str, value: str):
-        """Delete user-defined tag in CloudVision."""
-        tag_stub = tag_services.DeviceTagConfigServiceStub(self.comm_channel)
-        req = tag_services.DeviceTagConfigDeleteRequest(
-            key=tag_models.TagKey(label=StringValue(value=label), value=StringValue(value=value))
+        res = self.__search_client.Search(req)
+        return (self.decode_batch(nb) for nb in res)
+
+
+def get_devices(client):
+    """Get devices from CloudVision inventory."""
+    device_stub = services.DeviceServiceStub(client)
+    if PLUGIN_SETTINGS.get("import_active"):
+        req = services.DeviceStreamRequest(
+            partial_eq_filter=[models.Device(streaming_status=models.STREAMING_STATUS_ACTIVE)]
         )
-        try:
-            tag_stub.Delete(req)
-        # Skip error of tags that may be assigned to devices manually in CloudVision
-        except grpc.RpcError as err:
-            print(f"Failure to delete tag: {err}")
-            raise err
-
-    def assign_tag_to_device(self, device_id: str, label: str, value: str):
-        """Assign user-defined tag to device in CloudVision."""
-        tag_stub = tag_services.DeviceTagAssignmentConfigServiceStub(self.comm_channel)
-        req = tag_services.DeviceTagAssignmentConfigSetRequest(
-            value=tag_models.DeviceTagAssignmentConfig(
+    else:
+        req = services.DeviceStreamRequest()
+    responses = device_stub.GetAll(req)
+    devices = []
+    for resp in responses:
+        device = {
+            "device_id": resp.value.key.device_id.value,
+            "hostname": resp.value.hostname.value,
+            "fqdn": resp.value.fqdn.value,
+            "sw_ver": resp.value.software_version.value,
+            "model": resp.value.model_name.value,
+            "system_mac_address": resp.value.system_mac_address.value,
+        }
+        devices.append(device)
+    return devices
+
+
+# def get_device_id(self, device_name: str):
+#     """Get device_id for device_name from CloudVision inventory."""
+#     device_stub = services.DeviceServiceStub(self.comm_channel)
+#     req = services.DeviceStreamRequest(
+#         partial_eq_filter=[models.Device(hostname=device_name, streaming_status=models.STREAMING_STATUS_ACTIVE)]
+#     )
+#     resp = device_stub.GetOne(req)
+#     return resp.value.key.device_id.value
+
+
+def get_tags(client):
+    """Get all tags from CloudVision."""
+    tag_stub = tag_services.DeviceTagServiceStub(client)
+    req = tag_services.DeviceTagStreamRequest()
+    responses = tag_stub.GetAll(req)
+    tags = []
+    for resp in responses:
+        dev_tag = {
+            "label": resp.value.key.label.value,
+            "value": resp.value.key.value.value,
+            "creator_type": resp.value.creator_type,
+        }
+        tags.append(dev_tag)
+    return tags
+
+
+def get_tags_by_type(client, creator_type: int = tag_models.CREATOR_TYPE_USER):
+    """Get tags by creator type from CloudVision."""
+    tag_stub = tag_services.DeviceTagServiceStub(client)
+    req = tag_services.DeviceTagStreamRequest(partial_eq_filter=[tag_models.DeviceTag(creator_type=creator_type)])
+    responses = tag_stub.GetAll(req)
+    tags = []
+    for resp in responses:
+        dev_tag = {
+            "label": resp.value.key.label.value,
+            "value": resp.value.key.value.value,
+        }
+        tags.append(dev_tag)
+    return tags
+
+
+def get_device_tags(client, device_id: str):
+    """Get tags for specific device."""
+    tag_stub = tag_services.DeviceTagAssignmentConfigServiceStub(client)
+    req = tag_services.DeviceTagAssignmentConfigStreamRequest(
+        partial_eq_filter=[
+            tag_models.DeviceTagAssignmentConfig(
                 key=tag_models.DeviceTagAssignmentKey(
-                    label=StringValue(value=label),
-                    value=StringValue(value=value),
                     device_id=StringValue(value=device_id),
                 )
             )
+        ]
+    )
+    responses = tag_stub.GetAll(req)
+    tags = []
+    for resp in responses:
+        dev_tag = {
+            "label": resp.value.key.label.value,
+            "value": resp.value.key.value.value,
+        }
+        tags.append(dev_tag)
+    return tags
+
+
+def create_tag(client, label: str, value: str):
+    """Create user-defined tag in CloudVision."""
+    tag_stub = tag_services.DeviceTagConfigServiceStub(client)
+    req = tag_services.DeviceTagConfigSetRequest(
+        value=tag_models.DeviceTagConfig(
+            key=tag_models.TagKey(label=StringValue(value=label), value=StringValue(value=value))
         )
+    )
+    try:
         tag_stub.Set(req)
+    except grpc.RpcError as err:
+        # Ignore RPC error if tag already exists for idempotency
+        print(f"Failure to create tag: {err}")
+        raise err
+
 
-    def remove_tag_from_device(self, device_id: str, label: str, value: str):
-        """Unassign a tag from a device in CloudVision."""
-        tag_stub = tag_services.DeviceTagAssignmentConfigServiceStub(self.comm_channel)
-        req = tag_services.DeviceTagAssignmentConfigDeleteRequest(
+def delete_tag(client, label: str, value: str):
+    """Delete user-defined tag in CloudVision."""
+    tag_stub = tag_services.DeviceTagConfigServiceStub(client)
+    req = tag_services.DeviceTagConfigDeleteRequest(
+        key=tag_models.TagKey(label=StringValue(value=label), value=StringValue(value=value))
+    )
+    try:
+        tag_stub.Delete(req)
+    # Skip error of tags that may be assigned to devices manually in CloudVision
+    except grpc.RpcError as err:
+        print(f"Failure to delete tag: {err}")
+        raise err
+
+
+def assign_tag_to_device(client, device_id: str, label: str, value: str):
+    """Assign user-defined tag to device in CloudVision."""
+    tag_stub = tag_services.DeviceTagAssignmentConfigServiceStub(client)
+    req = tag_services.DeviceTagAssignmentConfigSetRequest(
+        value=tag_models.DeviceTagAssignmentConfig(
             key=tag_models.DeviceTagAssignmentKey(
                 label=StringValue(value=label),
                 value=StringValue(value=value),
                 device_id=StringValue(value=device_id),
             )
         )
-        tag_stub.Delete(req, timeout=RPC_TIMEOUT)
+    )
+    tag_stub.Set(req)
+
+
+def remove_tag_from_device(client, device_id: str, label: str, value: str):
+    """Unassign a tag from a device in CloudVision."""
+    tag_stub = tag_services.DeviceTagAssignmentConfigServiceStub(client)
+    req = tag_services.DeviceTagAssignmentConfigDeleteRequest(
+        key=tag_models.DeviceTagAssignmentKey(
+            label=StringValue(value=label),
+            value=StringValue(value=value),
+            device_id=StringValue(value=device_id),
+        )
+    )
+    tag_stub.Delete(req, timeout=RPC_TIMEOUT)
+
+
+# This section is based off example code from Arista: https://github.com/aristanetworks/cloudvision-python/blob/trunk/examples/Connector/get_intf_status.py
+
+
+def get_query(client, dataset, pathElts):
+    """Returns a query on a path element.
+
+    Args:
+        client (obj): GRPC client connection.
+        dataset (dict): Data related to query.
+        pathElts (List[str]): List of strings denoting path elements for query.
+
+    Returns:
+        dict: Query from dataset and path elements.
+    """
+    result = {}
+    query = [create_query([(pathElts, [])], dataset)]
+
+    for batch in client.get(query):
+        for notif in batch["notifications"]:
+            result.update(notif["updates"])
+    return result
+
+
+def unfreeze_frozen_dict(frozen_dict):
+    """Used to unfreeze Frozen dictionaries.
+
+    Args:
+        frozen_dict (FrozenDict|dict|str): Potentially frozen dict to be unfrozen.
+
+    Returns:
+        dict|str|list: Unfrozen contents of FrozenDict that was passed in.
+    """
+    if isinstance(frozen_dict, (dict, FrozenDict)):
+        return dict({k: unfreeze_frozen_dict(v) for k, v in frozen_dict.items()})
+
+    if isinstance(frozen_dict, (str)):
+        return frozen_dict
+
+    try:
+        return [unfreeze_frozen_dict(i) for i in frozen_dict]
+    except TypeError:
+        pass
+
+    return frozen_dict
+
+
+def get_device_type(client: CloudvisionApi, dId: str):
+    """Returns the type of the device: modular/fixed.
+
+    Args:
+        client (CloudvisionApi): Cloudvision connection.
+        dId (str): Device ID to determine type for.
+
+    Returns:
+        str: Type of device, either modular or fixed.
+    """
+    pathElts = ["Sysdb", "hardware", "entmib"]
+    query = get_query(client, dId, pathElts)
+    query = unfreeze_frozen_dict(query)
+    if query["fixedSystem"] is None:
+        dType = "modular"
+    else:
+        dType = "fixedSystem"
+    return dType
+
+
+def get_interfaces_chassis(client: CloudvisionApi, dId):
+    """Gets information about interfaces for a modular device.
+
+    Args:
+        client (CloudvisionApi): Cloudvision connection.
+        dId (str): Device ID to determine type for.
+    """
+    # Fetch the list of slices/linecards
+    pathElts = ["Sysdb", "interface", "status", "eth", "phy", "slice"]
+    dataset = dId
+    query = get_query(client, dataset, pathElts)
+    queryLC = unfreeze_frozen_dict(query).keys()
+    intfStatusChassis = []
+
+    # Go through each linecard and get the state of all interfaces
+    for lc in queryLC:
+        pathElts = ["Sysdb", "interface", "status", "eth", "phy", "slice", lc, "intfStatus", Wildcard()]
+
+        query = [create_query([(pathElts, [])], dataset)]
+
+        for batch in client.get(query):
+            for notif in batch["notifications"]:
+                results = notif["updates"]
+                if results.get("intfId"):
+                    intfStatusChassis.append(
+                        {
+                            "interface": results["intfId"],
+                            "link_status": "up"
+                            if results.get("linkStatus") and results["linkStatus"]["Name"] == "linkUp"
+                            else "down",
+                            "oper_status": "up"
+                            if results.get("operStatus") and results["operStatus"]["Name"] == "intfOperUp"
+                            else "down",
+                            "enabled": bool(results["enabledState"]["Name"] == "enabled"),
+                            "mac_addr": results["burnedInAddr"],
+                            "mtu": results["mtu"],
+                        }
+                    )
+    return intfStatusChassis
+
+
+def get_interfaces_fixed(client: CloudvisionApi, dId: str):
+    """Gets information about interfaces for a fixed system device.
+
+    Args:
+        client (CloudvisionApi): Cloudvision connection.
+        dId (str): Device ID to determine type for.
+    """
+    pathElts = ["Sysdb", "interface", "status", "eth", "phy", "slice", "1", "intfStatus", Wildcard()]
+    query = [create_query([(pathElts, [])], dId)]
+    query = unfreeze_frozen_dict(query)
+
+    intfStatusFixed = []
+    for batch in client.get(query):
+        for notif in batch["notifications"]:
+            try:
+                results = notif["updates"]
+                if results.get("intfId"):
+                    intfStatusFixed.append(
+                        {
+                            "interface": results["intfId"],
+                            "link_status": "up"
+                            if results.get("linkStatus") and results["linkStatus"]["Name"] == "linkUp"
+                            else "down",
+                            "oper_status": "up"
+                            if results.get("operStatus") and results["operStatus"]["Name"] == "intfOperUp"
+                            else "down",
+                            "enabled": bool(
+                                results["enabledState"]["Name"] == "enabled" if results.get("enabledState") else False
+                            ),
+                            "mac_addr": results["burnedInAddr"] if results.get("burnedInAddr") else "",
+                            "mtu": results["mtu"] if results.get("mtu") else 1500,
+                        }
+                    )
+            except KeyError as e:
+                print(
+                    f"Unknown key {e} for intfStatusFixed on interface {notif['updates']['intfId'] if notif['updates'].get('intfId') else notif['updates'].get('name')}.\n\nUpdate: {notif['updates']}"
+                )
+                continue
+    return intfStatusFixed
+
+
+def get_interface_transceiver(client: CloudvisionApi, dId: str, interface: str):
+    """Gets transceiver information for specified interface on specific device.
+
+    Args:
+        client (CloudvisionApi): Cloudvision connection.
+        dId (str): Device ID to determine transceiver type for.
+        interface (str): Name of interface to get transceiver information for.
+    """
+    pathElts = ["Sysdb", "hardware", "archer", "xcvr", "status", "all", interface]
+    query = [create_query([(pathElts, [])], dId)]
+    query = unfreeze_frozen_dict(query)
+
+    for batch in client.get(query):
+        for notif in batch["notifications"]:
+            if notif["updates"].get("actualIdEepromContents") and notif["updates"]["actualIdEepromContents"].get(
+                "mediaType"
+            ):
+                return notif["updates"]["actualIdEepromContents"]["mediaType"]
+            if notif["updates"].get("localMediaType"):
+                return notif["updates"]["localMediaType"]["Name"]
+    return "Unknown"
+
+
+def get_interface_mode(client: CloudvisionApi, dId: str, interface: str):
+    """Gets interface mode, ie access/trunked.
+
+    Args:
+        client (CloudvisionApi): Cloudvision connection.
+        dId (str): Device ID to determine type for.
+        interface (str): Name of interface to get mode information for.
+    """
+    pathElts = ["Sysdb", "bridging", "switchIntfConfig", "switchIntfConfig", interface]
+    query = [create_query([(pathElts, [])], dId)]
+    query = unfreeze_frozen_dict(query)
+
+    for batch in client.get(query):
+        for notif in batch["notifications"]:
+            if notif["updates"].get("switchportMode"):
+                return notif["updates"]["switchportMode"]["Name"]
+    return "Unknown"
+
+
+def get_port_type(port_info: dict, transceiver: str) -> str:
+    """Returns the type of port mapping CVP to Nautobot.
+
+    This attempts to determine what the port type is by looking at transceiver or speed.
+
+    Args:
+        port_info (dict): Data required to determine the port type.
+
+    Returns:
+        str: The Nautobot string for port type.
+    """
+    if transceiver != "Unknown" and transceiver in PORT_TYPE_MAP:
+        return PORT_TYPE_MAP[transceiver]
+
+    if port_info.get("interface") and "Management" in port_info["interface"]:
+        return "1000base-t"
+
+    if port_info.get("interface") and ("Vlan" in port_info["interface"] or "Loopback" in port_info["interface"]):
+        return "virtual"
+
+    if port_info.get("interface") and "Port-Channel" in port_info["interface"]:
+        return "lag"
+
+    return "other"
+
+
+def get_interface_status(port_info: dict) -> str:
+    """Returns the status of Interface based on link and operational status.
+
+    Args:
+        port_info (dict): Information about port including link and operational status.
+
+    Returns:
+        str: The status of a port: active|decommissioned|maintenance|planned.
+    """
+    status = "decommissioning"
+    if port_info["oper_status"] == "up" and port_info["link_status"] == "up":
+        status = "active"
+
+    if port_info["oper_status"] == "up" and port_info["link_status"] == "down":
+        status = "planned"
+
+    if port_info["oper_status"] == "down" and port_info["link_status"] == "down":
+        status = "maintenance"
+    return status
diff --git a/nautobot_ssot_aristacv/utils/nautobot.py b/nautobot_ssot_aristacv/utils/nautobot.py
index 03d05ed..cedd3f2 100644
--- a/nautobot_ssot_aristacv/utils/nautobot.py
+++ b/nautobot_ssot_aristacv/utils/nautobot.py
@@ -2,7 +2,6 @@
 from nautobot.dcim.models import DeviceType, DeviceRole, Site, Manufacturer
 from nautobot.extras.models.statuses import Status
 from nautobot.extras.models.tags import Tag
-from nautobot.extras.models.customfields import CustomField
 from django.contrib.contenttypes.models import ContentType
 
 
@@ -90,11 +89,3 @@ def verify_import_tag():
         import_tag = Tag(name="cloudvision_imported", slug="cloudvision_imported", color="ff0000")
         import_tag.validated_save()
     return import_tag
-
-
-def assign_arista_cf(device):
-    """Assigns arista custom fields to device."""
-    for custom_field in CustomField.objects.filter(name__contains="arista"):
-        device.cf[custom_field.name] = custom_field.default
-
-    return device
diff --git a/poetry.lock b/poetry.lock
index dcf777e..002cdff 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -99,7 +99,7 @@ python-versions = "*"
 
 [[package]]
 name = "black"
-version = "22.6.0"
+version = "22.8.0"
 description = "The uncompromising code formatter."
 category = "dev"
 optional = false
@@ -182,7 +182,7 @@ zstd = ["zstandard"]
 
 [[package]]
 name = "certifi"
-version = "2022.6.15"
+version = "2022.9.24"
 description = "Python package for providing Mozilla's CA Bundle."
 category = "main"
 optional = false
@@ -201,7 +201,7 @@ pycparser = "*"
 
 [[package]]
 name = "charset-normalizer"
-version = "2.1.0"
+version = "2.1.1"
 description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
 category = "main"
 optional = false
@@ -262,7 +262,7 @@ six = "*"
 
 [[package]]
 name = "cloudvision"
-version = "1.4.4"
+version = "1.5.0.post3"
 description = "A Python library for Arista's CloudVision APIs."
 category = "main"
 optional = false
@@ -312,7 +312,7 @@ jinja2 = "*"
 
 [[package]]
 name = "coverage"
-version = "6.4.3"
+version = "6.5.0"
 description = "Code coverage measurement for Python"
 category = "dev"
 optional = false
@@ -323,7 +323,7 @@ toml = ["tomli"]
 
 [[package]]
 name = "cryptography"
-version = "37.0.4"
+version = "38.0.1"
 description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
 category = "main"
 optional = false
@@ -336,7 +336,7 @@ cffi = ">=1.12"
 docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"]
 docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"]
 pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"]
-sdist = ["setuptools_rust (>=0.11.4)"]
+sdist = ["setuptools-rust (>=0.11.4)"]
 ssh = ["bcrypt (>=3.1.5)"]
 test = ["pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"]
 
@@ -392,7 +392,7 @@ graph = ["objgraph (>=1.7.2)"]
 
 [[package]]
 name = "django"
-version = "3.2.15"
+version = "3.2.16"
 description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design."
 category = "main"
 optional = false
@@ -471,14 +471,14 @@ redis = ["redis"]
 
 [[package]]
 name = "django-cors-headers"
-version = "3.10.1"
+version = "3.13.0"
 description = "django-cors-headers is a Django application for handling the server headers required for Cross-Origin Resource Sharing (CORS)."
 category = "main"
 optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
 
 [package.dependencies]
-Django = ">=2.2"
+Django = ">=3.2"
 
 [[package]]
 name = "django-cryptography"
@@ -505,26 +505,26 @@ Django = "*"
 
 [[package]]
 name = "django-debug-toolbar"
-version = "3.5.0"
+version = "3.7.0"
 description = "A configurable set of panels that display various debug information about the current request/response."
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 
 [package.dependencies]
-Django = ">=3.2"
+Django = ">=3.2.4"
 sqlparse = ">=0.2.0"
 
 [[package]]
 name = "django-extensions"
-version = "3.1.5"
+version = "3.2.1"
 description = "Extensions for Django"
 category = "main"
 optional = false
 python-versions = ">=3.6"
 
 [package.dependencies]
-Django = ">=2.2"
+Django = ">=3.2"
 
 [[package]]
 name = "django-filter"
@@ -685,6 +685,17 @@ pytz = "*"
 [package.extras]
 rest_framework = ["djangorestframework (>=3.0.0)"]
 
+[[package]]
+name = "django-tree-queries"
+version = "0.11.0"
+description = "Tree queries with explicit opt-in, without configurability"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+tests = ["coverage"]
+
 [[package]]
 name = "django-webserver"
 version = "1.2.0"
@@ -717,7 +728,7 @@ pytz = "*"
 
 [[package]]
 name = "drf-spectacular"
-version = "0.22.1"
+version = "0.24.2"
 description = "Sane and flexible OpenAPI 3 schema generation for Django REST framework"
 category = "main"
 optional = false
@@ -739,7 +750,7 @@ sidecar = ["drf-spectacular-sidecar"]
 
 [[package]]
 name = "drf-spectacular-sidecar"
-version = "2022.8.1"
+version = "2022.10.1"
 description = "Serve self-contained distribution builds of Swagger UI and Redoc with Django"
 category = "main"
 optional = false
@@ -750,7 +761,7 @@ Django = ">=2.2"
 
 [[package]]
 name = "drf-yasg"
-version = "1.20.3"
+version = "1.21.4"
 description = "Automated generation of real Swagger/OpenAPI 2.0 schemas from Django Rest Framework code."
 category = "main"
 optional = false
@@ -911,21 +922,21 @@ six = ">=1.12"
 
 [[package]]
 name = "grpcio"
-version = "1.48.0"
+version = "1.49.1"
 description = "HTTP/2-based RPC framework"
 category = "main"
 optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
 
 [package.dependencies]
 six = ">=1.5.2"
 
 [package.extras]
-protobuf = ["grpcio-tools (>=1.48.0)"]
+protobuf = ["grpcio-tools (>=1.49.1)"]
 
 [[package]]
 name = "idna"
-version = "3.3"
+version = "3.4"
 description = "Internationalized Domain Names in Applications (IDNA)"
 category = "main"
 optional = false
@@ -933,19 +944,20 @@ python-versions = ">=3.5"
 
 [[package]]
 name = "importlib-metadata"
-version = "4.4.0"
+version = "4.13.0"
 description = "Read metadata from Python packages"
 category = "main"
 optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
 
 [package.dependencies]
 typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
 zipp = ">=0.5"
 
 [package.extras]
-docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
-testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"]
+docs = ["sphinx (>=3.5)", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "furo", "jaraco.tidelift (>=1.4)"]
+perf = ["ipython"]
+testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "flake8 (<5)", "pytest-cov", "pytest-enabler (>=1.3)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"]
 
 [[package]]
 name = "importlib-resources"
@@ -972,7 +984,7 @@ python-versions = ">=3.5"
 
 [[package]]
 name = "invoke"
-version = "1.7.1"
+version = "1.7.3"
 description = "Pythonic task execution"
 category = "dev"
 optional = false
@@ -1016,7 +1028,7 @@ i18n = ["Babel (>=2.7)"]
 
 [[package]]
 name = "jsonschema"
-version = "4.4.0"
+version = "4.7.2"
 description = "An implementation of JSON Schema validation for Python"
 category = "main"
 optional = false
@@ -1031,7 +1043,7 @@ typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
 
 [package.extras]
 format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"]
-format_nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"]
+format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"]
 
 [[package]]
 name = "kombu"
@@ -1122,22 +1134,23 @@ python-versions = ">=3.6"
 
 [[package]]
 name = "mkdocs"
-version = "1.3.1"
+version = "1.4.0"
 description = "Project documentation with Markdown."
 category = "dev"
 optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
 
 [package.dependencies]
-click = ">=3.3"
+click = ">=7.0"
 ghp-import = ">=1.0"
-importlib-metadata = ">=4.3"
-Jinja2 = ">=2.10.2"
+importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""}
+Jinja2 = ">=2.11.1"
 Markdown = ">=3.2.1,<3.4"
 mergedeep = ">=1.3.4"
 packaging = ">=20.5"
-PyYAML = ">=3.10"
+PyYAML = ">=5.1"
 pyyaml-env-tag = ">=0.1"
+typing-extensions = {version = ">=3.10", markers = "python_version < \"3.8\""}
 watchdog = ">=2.0"
 
 [package.extras]
@@ -1161,26 +1174,26 @@ python-versions = "*"
 
 [[package]]
 name = "nautobot"
-version = "1.3.10"
+version = "1.4.5"
 description = "Source of truth and network automation platform."
 category = "main"
 optional = false
 python-versions = ">=3.7,<4.0"
 
 [package.dependencies]
-celery = ">=5.2.3,<5.3.0"
-Django = ">=3.2.14,<3.3.0"
+celery = ">=5.2.7,<5.3.0"
+Django = ">=3.2.15,<3.3.0"
 django-ajax-tables = ">=1.1.1,<1.2.0"
 django-cacheops = ">=6.0,<6.1"
 django-celery-beat = ">=2.2.1,<2.3.0"
 django-constance = {version = ">=2.8.0,<2.9.0", extras = ["database"]}
-django-cors-headers = ">=3.10.1,<3.11.0"
+django-cors-headers = ">=3.13.0,<3.14.0"
 django-cryptography = ">=1.0,<1.1"
 django-db-file-storage = ">=0.5.5,<0.6.0"
-django-extensions = ">=3.1.5,<3.2.0"
+django-extensions = ">=3.2.0,<3.3.0"
 django-filter = ">=21.1,<21.2"
 django-health-check = ">=3.16.5,<3.17.0"
-django-jinja = ">=2.10.0,<2.11.0"
+django-jinja = ">=2.10.2,<2.11.0"
 django-mptt = ">=0.13.4,<0.14.0"
 django-prometheus = ">=2.2.0,<2.3.0"
 django-redis = ">=5.2.0,<5.3.0"
@@ -1188,35 +1201,36 @@ django-rq = ">=2.5.1,<2.6.0"
 django-tables2 = ">=2.4.1,<2.5.0"
 django-taggit = ">=1.5.1,<1.6.0"
 django-timezone-field = ">=4.1.2,<4.2.0"
+django-tree-queries = ">=0.11,<0.12"
 django-webserver = ">=1.2.0,<1.3.0"
 djangorestframework = ">=3.13.1,<3.14.0"
-drf-spectacular = {version = ">=0.22.1,<0.23.0", extras = ["sidecar"]}
-drf-yasg = {version = ">=1.20.0,<1.21.0", extras = ["validation"]}
+drf-spectacular = {version = ">=0.24.0,<0.25.0", extras = ["sidecar"]}
+drf-yasg = {version = ">=1.20.0,<2.0.0", extras = ["validation"]}
 GitPython = ">=3.1.27,<3.2.0"
 graphene-django = ">=2.15.0,<2.16.0"
 graphene-django-optimizer = ">=0.8.0,<0.9.0"
-importlib-metadata = {version = ">=4.4,<4.5", markers = "python_version < \"3.10\""}
+importlib-metadata = {version = ">=4.4,<5.0", markers = "python_version < \"3.8\""}
 Jinja2 = ">=3.0.3,<3.1.0"
-jsonschema = ">=4.4.0,<4.5.0"
-Markdown = ">=3.3.6,<3.4.0"
-MarkupSafe = ">=2.1.0,<2.2.0"
+jsonschema = ">=4.7.0,<4.8.0"
+Markdown = ">=3.3.7,<3.4.0"
+MarkupSafe = ">=2.1.1,<2.2.0"
 netaddr = ">=0.8.0,<0.9.0"
-netutils = ">=1.1.0,<1.2.0"
-Pillow = ">=9.0.1,<9.1.0"
+netutils = ">=1.2.0,<1.3.0"
+Pillow = ">=9.2.0,<9.3.0"
 psycopg2-binary = ">=2.9.3,<2.10.0"
 pycryptodome = ">=3.13.0,<3.14.0"
 pyuwsgi = ">=2.0.20,<2.1.0"
 PyYAML = ">=6.0,<6.1"
 social-auth-app-django = ">=5.0.0,<5.1.0"
-svgwrite = ">=1.4.1,<1.5.0"
+svgwrite = ">=1.4.2,<1.5.0"
 
 [package.extras]
-all = ["django-auth-ldap (>=4.0.0,<4.1.0)", "django-storages (>=1.12.3,<1.13.0)", "mysqlclient (>=2.1.0,<2.2.0)", "napalm (>=3.3.1,<3.4.0)", "social-auth-core[saml,openidconnect] (>=4.2.0,<4.3.0)"]
-ldap = ["django-auth-ldap (>=4.0.0,<4.1.0)"]
+all = ["django-auth-ldap (>=4.1.0,<4.2.0)", "django-storages (>=1.12.3,<1.13.0)", "mysqlclient (>=2.1.0,<2.2.0)", "napalm (>=3.4.1,<3.5.0)", "social-auth-core[openidconnect,saml] (>=4.3.0,<4.4.0)"]
+ldap = ["django-auth-ldap (>=4.1.0,<4.2.0)"]
 remote_storage = ["django-storages (>=1.12.3,<1.13.0)"]
 mysql = ["mysqlclient (>=2.1.0,<2.2.0)"]
-napalm = ["napalm (>=3.3.1,<3.4.0)"]
-sso = ["social-auth-core[saml,openidconnect] (>=4.2.0,<4.3.0)"]
+napalm = ["napalm (>=3.4.1,<3.5.0)"]
+sso = ["social-auth-core[openidconnect,saml] (>=4.3.0,<4.4.0)"]
 
 [[package]]
 name = "nautobot-ssot"
@@ -1241,7 +1255,7 @@ python-versions = "*"
 
 [[package]]
 name = "netutils"
-version = "1.1.0"
+version = "1.2.0"
 description = "Common helper functions useful in network automation."
 category = "main"
 optional = false
@@ -1249,7 +1263,7 @@ python-versions = ">=3.6,<4.0"
 
 [[package]]
 name = "oauthlib"
-version = "3.2.0"
+version = "3.2.1"
 description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic"
 category = "main"
 optional = false
@@ -1271,17 +1285,28 @@ python-versions = ">=3.6"
 [package.dependencies]
 pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
 
+[[package]]
+name = "parameterized"
+version = "0.8.1"
+description = "Parameterized testing with any Python test framework"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.extras]
+dev = ["jinja2"]
+
 [[package]]
 name = "pathspec"
-version = "0.9.0"
+version = "0.10.1"
 description = "Utility library for gitignore style pattern matching of file paths."
 category = "dev"
 optional = false
-python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
+python-versions = ">=3.7"
 
 [[package]]
 name = "pbr"
-version = "5.9.0"
+version = "5.10.0"
 description = "Python Build Reasonableness"
 category = "dev"
 optional = false
@@ -1289,12 +1314,16 @@ python-versions = ">=2.6"
 
 [[package]]
 name = "pillow"
-version = "9.0.1"
+version = "9.2.0"
 description = "Python Imaging Library (Fork)"
 category = "main"
 optional = false
 python-versions = ">=3.7"
 
+[package.extras]
+docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinxext-opengraph"]
+tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
+
 [[package]]
 name = "platformdirs"
 version = "2.5.2"
@@ -1334,7 +1363,7 @@ test = ["pytest (>=2.7.3)", "pytest-cov", "coveralls", "futures", "pytest-benchm
 
 [[package]]
 name = "prompt-toolkit"
-version = "3.0.30"
+version = "3.0.31"
 description = "Library for building powerful interactive command lines in Python"
 category = "main"
 optional = false
@@ -1345,7 +1374,7 @@ wcwidth = "*"
 
 [[package]]
 name = "protobuf"
-version = "3.20.1"
+version = "3.20.3"
 description = "Protocol Buffers"
 category = "main"
 optional = false
@@ -1385,14 +1414,14 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
 
 [[package]]
 name = "pydantic"
-version = "1.9.1"
+version = "1.10.2"
 description = "Data validation and settings management using python type hints"
 category = "main"
 optional = false
-python-versions = ">=3.6.1"
+python-versions = ">=3.7"
 
 [package.dependencies]
-typing-extensions = ">=3.7.4.3"
+typing-extensions = ">=4.1.0"
 
 [package.extras]
 dotenv = ["python-dotenv (>=0.10.4)"]
@@ -1422,16 +1451,16 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 
 [[package]]
 name = "pyjwt"
-version = "2.4.0"
+version = "2.5.0"
 description = "JSON Web Token implementation in Python"
 category = "main"
 optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
 
 [package.extras]
-crypto = ["cryptography (>=3.3.1)"]
-dev = ["sphinx", "sphinx-rtd-theme", "zope.interface", "cryptography (>=3.3.1)", "pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)", "mypy", "pre-commit"]
-docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"]
+crypto = ["cryptography (>=3.3.1)", "types-cryptography (>=3.3.21)"]
+dev = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface", "cryptography (>=3.3.1)", "types-cryptography (>=3.3.21)", "pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)", "pre-commit"]
+docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"]
 tests = ["pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)"]
 
 [[package]]
@@ -1544,7 +1573,7 @@ postgresql = ["psycopg2"]
 
 [[package]]
 name = "pytz"
-version = "2022.1"
+version = "2022.4"
 description = "World timezone definitions, modern and historical"
 category = "main"
 optional = false
@@ -1631,7 +1660,7 @@ rsa = ["oauthlib[signedtoken] (>=3.0.0)"]
 
 [[package]]
 name = "rq"
-version = "1.11.0"
+version = "1.11.1"
 description = "RQ is a simple, lightweight, library for creating background jobs, and processing them."
 category = "main"
 optional = false
@@ -1748,7 +1777,7 @@ saml = ["python3-saml (>=1.2.1)", "lxml (<4.7)"]
 
 [[package]]
 name = "sqlparse"
-version = "0.4.2"
+version = "0.4.3"
 description = "A non-validating SQL parser."
 category = "main"
 optional = false
@@ -1792,7 +1821,7 @@ python-versions = ">=3.6"
 
 [[package]]
 name = "swagger-spec-validator"
-version = "2.7.4"
+version = "2.7.6"
 description = "Validation of Swagger specifications"
 category = "main"
 optional = false
@@ -1829,7 +1858,7 @@ python-versions = ">=3.6"
 
 [[package]]
 name = "types-protobuf"
-version = "3.19.22"
+version = "3.20.4"
 description = "Typing stubs for protobuf"
 category = "main"
 optional = false
@@ -1837,7 +1866,7 @@ python-versions = "*"
 
 [[package]]
 name = "types-pyyaml"
-version = "6.0.11"
+version = "6.0.12"
 description = "Typing stubs for PyYAML"
 category = "main"
 optional = false
@@ -1845,7 +1874,7 @@ python-versions = "*"
 
 [[package]]
 name = "types-requests"
-version = "2.28.8"
+version = "2.28.11.1"
 description = "Typing stubs for requests"
 category = "main"
 optional = false
@@ -1856,7 +1885,7 @@ types-urllib3 = "<1.27"
 
 [[package]]
 name = "types-urllib3"
-version = "1.26.22"
+version = "1.26.25"
 description = "Typing stubs for urllib3"
 category = "main"
 optional = false
@@ -1880,7 +1909,7 @@ python-versions = ">=3.6"
 
 [[package]]
 name = "urllib3"
-version = "1.26.11"
+version = "1.26.12"
 description = "HTTP library with thread-safe connection pooling, file post, and more."
 category = "main"
 optional = false
@@ -1888,7 +1917,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*,
 
 [package.extras]
 brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"]
-secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
+secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "urllib3-secure-extra", "ipaddress"]
 socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
 
 [[package]]
@@ -1928,7 +1957,7 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
 
 [[package]]
 name = "yamllint"
-version = "1.27.1"
+version = "1.28.0"
 description = "A linter for YAML files."
 category = "dev"
 optional = false
@@ -1953,7 +1982,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-
 [metadata]
 lock-version = "1.1"
 python-versions = "^3.7"
-content-hash = "ba75b1f35379db1019bd3213cf6eb33aca8fa5361538a87509e266abc42a8e47"
+content-hash = "337405844556baa9d2fd3b934461145a60481c3036f8e11e9bd3713fdcf595d3"
 
 [metadata.files]
 amqp = [
@@ -1982,31 +2011,7 @@ billiard = [
     {file = "billiard-3.6.4.0-py3-none-any.whl", hash = "sha256:87103ea78fa6ab4d5c751c4909bcff74617d985de7fa8b672cf8618afd5a875b"},
     {file = "billiard-3.6.4.0.tar.gz", hash = "sha256:299de5a8da28a783d51b197d496bef4f1595dd023a93a4f59dde1886ae905547"},
 ]
-black = [
-    {file = "black-22.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f586c26118bc6e714ec58c09df0157fe2d9ee195c764f630eb0d8e7ccce72e69"},
-    {file = "black-22.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b270a168d69edb8b7ed32c193ef10fd27844e5c60852039599f9184460ce0807"},
-    {file = "black-22.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6797f58943fceb1c461fb572edbe828d811e719c24e03375fd25170ada53825e"},
-    {file = "black-22.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c85928b9d5f83b23cee7d0efcb310172412fbf7cb9d9ce963bd67fd141781def"},
-    {file = "black-22.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:f6fe02afde060bbeef044af7996f335fbe90b039ccf3f5eb8f16df8b20f77666"},
-    {file = "black-22.6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cfaf3895a9634e882bf9d2363fed5af8888802d670f58b279b0bece00e9a872d"},
-    {file = "black-22.6.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94783f636bca89f11eb5d50437e8e17fbc6a929a628d82304c80fa9cd945f256"},
-    {file = "black-22.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2ea29072e954a4d55a2ff58971b83365eba5d3d357352a07a7a4df0d95f51c78"},
-    {file = "black-22.6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e439798f819d49ba1c0bd9664427a05aab79bfba777a6db94fd4e56fae0cb849"},
-    {file = "black-22.6.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:187d96c5e713f441a5829e77120c269b6514418f4513a390b0499b0987f2ff1c"},
-    {file = "black-22.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:074458dc2f6e0d3dab7928d4417bb6957bb834434516f21514138437accdbe90"},
-    {file = "black-22.6.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a218d7e5856f91d20f04e931b6f16d15356db1c846ee55f01bac297a705ca24f"},
-    {file = "black-22.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:568ac3c465b1c8b34b61cd7a4e349e93f91abf0f9371eda1cf87194663ab684e"},
-    {file = "black-22.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6c1734ab264b8f7929cef8ae5f900b85d579e6cbfde09d7387da8f04771b51c6"},
-    {file = "black-22.6.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9a3ac16efe9ec7d7381ddebcc022119794872abce99475345c5a61aa18c45ad"},
-    {file = "black-22.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:b9fd45787ba8aa3f5e0a0a98920c1012c884622c6c920dbe98dbd05bc7c70fbf"},
-    {file = "black-22.6.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7ba9be198ecca5031cd78745780d65a3f75a34b2ff9be5837045dce55db83d1c"},
-    {file = "black-22.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a3db5b6409b96d9bd543323b23ef32a1a2b06416d525d27e0f67e74f1446c8f2"},
-    {file = "black-22.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:560558527e52ce8afba936fcce93a7411ab40c7d5fe8c2463e279e843c0328ee"},
-    {file = "black-22.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b154e6bbde1e79ea3260c4b40c0b7b3109ffcdf7bc4ebf8859169a6af72cd70b"},
-    {file = "black-22.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:4af5bc0e1f96be5ae9bd7aaec219c901a94d6caa2484c21983d043371c733fc4"},
-    {file = "black-22.6.0-py3-none-any.whl", hash = "sha256:ac609cf8ef5e7115ddd07d85d988d074ed00e10fbc3445aee393e70164a2219c"},
-    {file = "black-22.6.0.tar.gz", hash = "sha256:6c6d39e28aed379aec40da1c65434c77d75e65bb59a1e1c283de545fb4e7c6c9"},
-]
+black = []
 cached-property = [
     {file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"},
     {file = "cached_property-1.5.2-py2.py3-none-any.whl", hash = "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"},
@@ -2015,10 +2020,7 @@ celery = [
     {file = "celery-5.2.7-py3-none-any.whl", hash = "sha256:138420c020cd58d6707e6257b6beda91fd39af7afde5d36c6334d175302c0e14"},
     {file = "celery-5.2.7.tar.gz", hash = "sha256:fafbd82934d30f8a004f81e8f7a062e31413a23d444be8ee3326553915958c6d"},
 ]
-certifi = [
-    {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"},
-    {file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"},
-]
+certifi = []
 cffi = [
     {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"},
     {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"},
@@ -2085,10 +2087,7 @@ cffi = [
     {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"},
     {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"},
 ]
-charset-normalizer = [
-    {file = "charset-normalizer-2.1.0.tar.gz", hash = "sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413"},
-    {file = "charset_normalizer-2.1.0-py3-none-any.whl", hash = "sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5"},
-]
+charset-normalizer = []
 click = [
     {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
     {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
@@ -2119,30 +2118,7 @@ coreschema = [
     {file = "coreschema-0.0.4.tar.gz", hash = "sha256:9503506007d482ab0867ba14724b93c18a33b22b6d19fb419ef2d239dd4a1607"},
 ]
 coverage = []
-cryptography = [
-    {file = "cryptography-37.0.4-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:549153378611c0cca1042f20fd9c5030d37a72f634c9326e225c9f666d472884"},
-    {file = "cryptography-37.0.4-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:a958c52505c8adf0d3822703078580d2c0456dd1d27fabfb6f76fe63d2971cd6"},
-    {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f721d1885ecae9078c3f6bbe8a88bc0786b6e749bf32ccec1ef2b18929a05046"},
-    {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3d41b965b3380f10e4611dbae366f6dc3cefc7c9ac4e8842a806b9672ae9add5"},
-    {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80f49023dd13ba35f7c34072fa17f604d2f19bf0989f292cedf7ab5770b87a0b"},
-    {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2dcb0b3b63afb6df7fd94ec6fbddac81b5492513f7b0436210d390c14d46ee8"},
-    {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:b7f8dd0d4c1f21759695c05a5ec8536c12f31611541f8904083f3dc582604280"},
-    {file = "cryptography-37.0.4-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:30788e070800fec9bbcf9faa71ea6d8068f5136f60029759fd8c3efec3c9dcb3"},
-    {file = "cryptography-37.0.4-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:190f82f3e87033821828f60787cfa42bff98404483577b591429ed99bed39d59"},
-    {file = "cryptography-37.0.4-cp36-abi3-win32.whl", hash = "sha256:b62439d7cd1222f3da897e9a9fe53bbf5c104fff4d60893ad1355d4c14a24157"},
-    {file = "cryptography-37.0.4-cp36-abi3-win_amd64.whl", hash = "sha256:f7a6de3e98771e183645181b3627e2563dcde3ce94a9e42a3f427d2255190327"},
-    {file = "cryptography-37.0.4-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bc95ed67b6741b2607298f9ea4932ff157e570ef456ef7ff0ef4884a134cc4b"},
-    {file = "cryptography-37.0.4-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:f8c0a6e9e1dd3eb0414ba320f85da6b0dcbd543126e30fcc546e7372a7fbf3b9"},
-    {file = "cryptography-37.0.4-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:e007f052ed10cc316df59bc90fbb7ff7950d7e2919c9757fd42a2b8ecf8a5f67"},
-    {file = "cryptography-37.0.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bc997818309f56c0038a33b8da5c0bfbb3f1f067f315f9abd6fc07ad359398d"},
-    {file = "cryptography-37.0.4-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d204833f3c8a33bbe11eda63a54b1aad7aa7456ed769a982f21ec599ba5fa282"},
-    {file = "cryptography-37.0.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:75976c217f10d48a8b5a8de3d70c454c249e4b91851f6838a4e48b8f41eb71aa"},
-    {file = "cryptography-37.0.4-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:7099a8d55cd49b737ffc99c17de504f2257e3787e02abe6d1a6d136574873441"},
-    {file = "cryptography-37.0.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2be53f9f5505673eeda5f2736bea736c40f051a739bfae2f92d18aed1eb54596"},
-    {file = "cryptography-37.0.4-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:91ce48d35f4e3d3f1d83e29ef4a9267246e6a3be51864a5b7d2247d5086fa99a"},
-    {file = "cryptography-37.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4c590ec31550a724ef893c50f9a97a0c14e9c851c85621c5650d699a7b88f7ab"},
-    {file = "cryptography-37.0.4.tar.gz", hash = "sha256:63f9c17c0e2474ccbebc9302ce2f07b55b3b3fcb211ded18a42d5764f5c10a82"},
-]
+cryptography = []
 defusedxml = [
     {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"},
     {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"},
@@ -2177,10 +2153,7 @@ django-constance = [
     {file = "django-constance-2.8.0.tar.gz", hash = "sha256:0a492454acc78799ce7b9f7a28a00c53427d513f34f8bf6fdc90a46d8864b2af"},
     {file = "django_constance-2.8.0-py3-none-any.whl", hash = "sha256:60fec73e397d5f4f7440f611b18d3e7ce5342647f316fedc47b62e1411c849e7"},
 ]
-django-cors-headers = [
-    {file = "django-cors-headers-3.10.1.tar.gz", hash = "sha256:b5a874b492bcad99f544bb76ef679472259eb41ee5644ca62d1a94ddb26b7f6e"},
-    {file = "django_cors_headers-3.10.1-py3-none-any.whl", hash = "sha256:1390b5846e9835b0911e2574409788af87cd9154246aafbdc8ec546c93698fe6"},
-]
+django-cors-headers = []
 django-cryptography = [
     {file = "django-cryptography-1.0.tar.gz", hash = "sha256:13de5cf8f1250744c104b9e24774d03aa6d8488959dd40cdc016934043652445"},
     {file = "django_cryptography-1.0-py3-none-any.whl", hash = "sha256:0a99980b1cee7cc5e52f9b20b322620fea7cc124d770273e7bd285b20fd9d222"},
@@ -2188,14 +2161,8 @@ django-cryptography = [
 django-db-file-storage = [
     {file = "django-db-file-storage-0.5.5.tar.gz", hash = "sha256:5d5da694b78ab202accab4508b958e0e37b3d146310e76f6f6125e1bdeaaad14"},
 ]
-django-debug-toolbar = [
-    {file = "django-debug-toolbar-3.5.0.tar.gz", hash = "sha256:97965f2630692de316ea0c1ca5bfa81660d7ba13146dbc6be2059cf55b35d0e5"},
-    {file = "django_debug_toolbar-3.5.0-py3-none-any.whl", hash = "sha256:89a52128309eb4da12738801ff0c202d2ff8730d1c3225fac6acf630c303e661"},
-]
-django-extensions = [
-    {file = "django-extensions-3.1.5.tar.gz", hash = "sha256:28e1e1bf49f0e00307ba574d645b0af3564c981a6dfc87209d48cb98f77d0b1a"},
-    {file = "django_extensions-3.1.5-py3-none-any.whl", hash = "sha256:9238b9e016bb0009d621e05cf56ea8ce5cce9b32e91ad2026996a7377ca28069"},
-]
+django-debug-toolbar = []
+django-extensions = []
 django-filter = [
     {file = "django-filter-21.1.tar.gz", hash = "sha256:632a251fa8f1aadb4b8cceff932bb52fe2f826dd7dfe7f3eac40e5c463d6836e"},
     {file = "django_filter-21.1-py3-none-any.whl", hash = "sha256:f4a6737a30104c98d2e2a5fb93043f36dd7978e0c7ddc92f5998e85433ea5063"},
@@ -2244,6 +2211,7 @@ django-timezone-field = [
     {file = "django-timezone-field-4.1.2.tar.gz", hash = "sha256:cffac62452d060e365938aa9c9f7b72d70d8b26b9c60243bce227b35abd1b9df"},
     {file = "django_timezone_field-4.1.2-py3-none-any.whl", hash = "sha256:897c06e40b619cf5731a30d6c156886a7c64cba3a90364832148da7ef32ccf36"},
 ]
+django-tree-queries = []
 django-webserver = [
     {file = "django-webserver-1.2.0.tar.gz", hash = "sha256:c976979d15b5ff9a212f7904d3b779e22219aebb4857860fcaf20e4e40f1da40"},
     {file = "django_webserver-1.2.0-py2.py3-none-any.whl", hash = "sha256:09200631f266484b9e944e38e92681d6e9aa7d90d089a5c86d5fb08fddad84fe"},
@@ -2252,10 +2220,7 @@ djangorestframework = [
     {file = "djangorestframework-3.13.1-py3-none-any.whl", hash = "sha256:24c4bf58ed7e85d1fe4ba250ab2da926d263cd57d64b03e8dcef0ac683f8b1aa"},
     {file = "djangorestframework-3.13.1.tar.gz", hash = "sha256:0c33407ce23acc68eca2a6e46424b008c9c02eceb8cf18581921d0092bc1f2ee"},
 ]
-drf-spectacular = [
-    {file = "drf-spectacular-0.22.1.tar.gz", hash = "sha256:866e16ddaae167a1234c76cd8c351161373551db994ce9665b347b32d5daf38b"},
-    {file = "drf_spectacular-0.22.1-py3-none-any.whl", hash = "sha256:17ac5e31e5d6150dd5fa10843b429202f4f38069202acc44394cc5a771de63d9"},
-]
+drf-spectacular = []
 drf-spectacular-sidecar = []
 drf-yasg = []
 flake8 = [
@@ -2298,23 +2263,14 @@ graphql-relay = [
     {file = "graphql_relay-2.0.1-py3-none-any.whl", hash = "sha256:ac514cb86db9a43014d7e73511d521137ac12cf0101b2eaa5f0a3da2e10d913d"},
 ]
 grpcio = []
-idna = [
-    {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
-    {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
-]
-importlib-metadata = [
-    {file = "importlib_metadata-4.4.0-py3-none-any.whl", hash = "sha256:960d52ba7c21377c990412aca380bf3642d734c2eaab78a2c39319f67c6a5786"},
-    {file = "importlib_metadata-4.4.0.tar.gz", hash = "sha256:e592faad8de1bda9fe920cf41e15261e7131bcf266c30306eec00e8e225c1dd5"},
-]
+idna = []
+importlib-metadata = []
 importlib-resources = []
 inflection = [
     {file = "inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2"},
     {file = "inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417"},
 ]
-invoke = [
-    {file = "invoke-1.7.1-py3-none-any.whl", hash = "sha256:2dc975b4f92be0c0a174ad2d063010c8a1fdb5e9389d69871001118b4fcac4fb"},
-    {file = "invoke-1.7.1.tar.gz", hash = "sha256:7b6deaf585eee0a848205d0b8c0014b9bf6f287a8eb798818a642dff1df14b19"},
-]
+invoke = []
 isort = [
     {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"},
     {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"},
@@ -2327,10 +2283,7 @@ jinja2 = [
     {file = "Jinja2-3.0.3-py3-none-any.whl", hash = "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8"},
     {file = "Jinja2-3.0.3.tar.gz", hash = "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"},
 ]
-jsonschema = [
-    {file = "jsonschema-4.4.0-py3-none-any.whl", hash = "sha256:77281a1f71684953ee8b3d488371b162419767973789272434bbc3f29d9c8823"},
-    {file = "jsonschema-4.4.0.tar.gz", hash = "sha256:636694eb41b3535ed608fe04129f26542b59ed99808b4f688aa32dcf55317a83"},
-]
+jsonschema = []
 kombu = [
     {file = "kombu-5.2.4-py3-none-any.whl", hash = "sha256:8b213b24293d3417bcf0d2f5537b7f756079e3ea232a8386dcc89a59fd2361a4"},
     {file = "kombu-5.2.4.tar.gz", hash = "sha256:37cee3ee725f94ea8bb173eaab7c1760203ea53bbebae226328600f9d2799610"},
@@ -2497,60 +2450,19 @@ netaddr = [
     {file = "netaddr-0.8.0-py2.py3-none-any.whl", hash = "sha256:9666d0232c32d2656e5e5f8d735f58fd6c7457ce52fc21c98d45f2af78f990ac"},
     {file = "netaddr-0.8.0.tar.gz", hash = "sha256:d6cc57c7a07b1d9d2e917aa8b36ae8ce61c35ba3fcd1b83ca31c5a0ee2b5a243"},
 ]
-netutils = [
-    {file = "netutils-1.1.0-py3-none-any.whl", hash = "sha256:bcb4367689c773cd30bf898b15bed38f22dc8bd69e64f2272bb9dd4726bf41d4"},
-    {file = "netutils-1.1.0.tar.gz", hash = "sha256:90ab637c60ae18c515191224c52a7b0a1eb6fdde2f8dd40dfac638f2dd06ef2c"},
-]
-oauthlib = [
-    {file = "oauthlib-3.2.0-py3-none-any.whl", hash = "sha256:6db33440354787f9b7f3a6dbd4febf5d0f93758354060e802f6c06cb493022fe"},
-    {file = "oauthlib-3.2.0.tar.gz", hash = "sha256:23a8208d75b902797ea29fd31fa80a15ed9dc2c6c16fe73f5d346f83f6fa27a2"},
-]
+netutils = []
+oauthlib = []
 packaging = [
     {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
     {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
 ]
-pathspec = [
-    {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"},
-    {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"},
-]
-pbr = [
-    {file = "pbr-5.9.0-py2.py3-none-any.whl", hash = "sha256:e547125940bcc052856ded43be8e101f63828c2d94239ffbe2b327ba3d5ccf0a"},
-    {file = "pbr-5.9.0.tar.gz", hash = "sha256:e8dca2f4b43560edef58813969f52a56cef023146cbb8931626db80e6c1c4308"},
-]
-pillow = [
-    {file = "Pillow-9.0.1-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:9bfdb82cdfeccec50aad441afc332faf8606dfa5e8efd18a6692b5d6e79f00fd"},
-    {file = "Pillow-9.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5100b45a4638e3c00e4d2320d3193bdabb2d75e79793af7c3eb139e4f569f16f"},
-    {file = "Pillow-9.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:528a2a692c65dd5cafc130de286030af251d2ee0483a5bf50c9348aefe834e8a"},
-    {file = "Pillow-9.0.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f29d831e2151e0b7b39981756d201f7108d3d215896212ffe2e992d06bfe049"},
-    {file = "Pillow-9.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:855c583f268edde09474b081e3ddcd5cf3b20c12f26e0d434e1386cc5d318e7a"},
-    {file = "Pillow-9.0.1-cp310-cp310-win32.whl", hash = "sha256:d9d7942b624b04b895cb95af03a23407f17646815495ce4547f0e60e0b06f58e"},
-    {file = "Pillow-9.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:81c4b81611e3a3cb30e59b0cf05b888c675f97e3adb2c8672c3154047980726b"},
-    {file = "Pillow-9.0.1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:413ce0bbf9fc6278b2d63309dfeefe452835e1c78398efb431bab0672fe9274e"},
-    {file = "Pillow-9.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80fe64a6deb6fcfdf7b8386f2cf216d329be6f2781f7d90304351811fb591360"},
-    {file = "Pillow-9.0.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cef9c85ccbe9bee00909758936ea841ef12035296c748aaceee535969e27d31b"},
-    {file = "Pillow-9.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d19397351f73a88904ad1aee421e800fe4bbcd1aeee6435fb62d0a05ccd1030"},
-    {file = "Pillow-9.0.1-cp37-cp37m-win32.whl", hash = "sha256:d21237d0cd37acded35154e29aec853e945950321dd2ffd1a7d86fe686814669"},
-    {file = "Pillow-9.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ede5af4a2702444a832a800b8eb7f0a7a1c0eed55b644642e049c98d589e5092"},
-    {file = "Pillow-9.0.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:b5b3f092fe345c03bca1e0b687dfbb39364b21ebb8ba90e3fa707374b7915204"},
-    {file = "Pillow-9.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:335ace1a22325395c4ea88e00ba3dc89ca029bd66bd5a3c382d53e44f0ccd77e"},
-    {file = "Pillow-9.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db6d9fac65bd08cea7f3540b899977c6dee9edad959fa4eaf305940d9cbd861c"},
-    {file = "Pillow-9.0.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f154d173286a5d1863637a7dcd8c3437bb557520b01bddb0be0258dcb72696b5"},
-    {file = "Pillow-9.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d4b1341ac07ae07eb2cc682f459bec932a380c3b122f5540432d8977e64eae"},
-    {file = "Pillow-9.0.1-cp38-cp38-win32.whl", hash = "sha256:effb7749713d5317478bb3acb3f81d9d7c7f86726d41c1facca068a04cf5bb4c"},
-    {file = "Pillow-9.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:7f7609a718b177bf171ac93cea9fd2ddc0e03e84d8fa4e887bdfc39671d46b00"},
-    {file = "Pillow-9.0.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:80ca33961ced9c63358056bd08403ff866512038883e74f3a4bf88ad3eb66838"},
-    {file = "Pillow-9.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1c3c33ac69cf059bbb9d1a71eeaba76781b450bc307e2291f8a4764d779a6b28"},
-    {file = "Pillow-9.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12875d118f21cf35604176872447cdb57b07126750a33748bac15e77f90f1f9c"},
-    {file = "Pillow-9.0.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:514ceac913076feefbeaf89771fd6febde78b0c4c1b23aaeab082c41c694e81b"},
-    {file = "Pillow-9.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3c5c79ab7dfce6d88f1ba639b77e77a17ea33a01b07b99840d6ed08031cb2a7"},
-    {file = "Pillow-9.0.1-cp39-cp39-win32.whl", hash = "sha256:718856856ba31f14f13ba885ff13874be7fefc53984d2832458f12c38205f7f7"},
-    {file = "Pillow-9.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:f25ed6e28ddf50de7e7ea99d7a976d6a9c415f03adcaac9c41ff6ff41b6d86ac"},
-    {file = "Pillow-9.0.1-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:011233e0c42a4a7836498e98c1acf5e744c96a67dd5032a6f666cc1fb97eab97"},
-    {file = "Pillow-9.0.1-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:253e8a302a96df6927310a9d44e6103055e8fb96a6822f8b7f514bb7ef77de56"},
-    {file = "Pillow-9.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6295f6763749b89c994fcb6d8a7f7ce03c3992e695f89f00b741b4580b199b7e"},
-    {file = "Pillow-9.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:a9f44cd7e162ac6191491d7249cceb02b8116b0f7e847ee33f739d7cb1ea1f70"},
-    {file = "Pillow-9.0.1.tar.gz", hash = "sha256:6c8bc8238a7dfdaf7a75f5ec5a663f4173f8c367e5a39f87e720495e1eed75fa"},
+parameterized = [
+    {file = "parameterized-0.8.1-py2.py3-none-any.whl", hash = "sha256:9cbb0b69a03e8695d68b3399a8a5825200976536fe1cb79db60ed6a4c8c9efe9"},
+    {file = "parameterized-0.8.1.tar.gz", hash = "sha256:41bbff37d6186430f77f900d777e5bb6a24928a1c46fb1de692f8b52b8833b5c"},
 ]
+pathspec = []
+pbr = []
+pillow = []
 platformdirs = [
     {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
     {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
@@ -2562,36 +2474,8 @@ prometheus-client = [
 promise = [
     {file = "promise-2.3.tar.gz", hash = "sha256:dfd18337c523ba4b6a58801c164c1904a9d4d1b1747c7d5dbf45b693a49d93d0"},
 ]
-prompt-toolkit = [
-    {file = "prompt_toolkit-3.0.30-py3-none-any.whl", hash = "sha256:d8916d3f62a7b67ab353a952ce4ced6a1d2587dfe9ef8ebc30dd7c386751f289"},
-    {file = "prompt_toolkit-3.0.30.tar.gz", hash = "sha256:859b283c50bde45f5f97829f77a4674d1c1fcd88539364f1b28a37805cfd89c0"},
-]
-protobuf = [
-    {file = "protobuf-3.20.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3cc797c9d15d7689ed507b165cd05913acb992d78b379f6014e013f9ecb20996"},
-    {file = "protobuf-3.20.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:ff8d8fa42675249bb456f5db06c00de6c2f4c27a065955917b28c4f15978b9c3"},
-    {file = "protobuf-3.20.1-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cd68be2559e2a3b84f517fb029ee611546f7812b1fdd0aa2ecc9bc6ec0e4fdde"},
-    {file = "protobuf-3.20.1-cp310-cp310-win32.whl", hash = "sha256:9016d01c91e8e625141d24ec1b20fed584703e527d28512aa8c8707f105a683c"},
-    {file = "protobuf-3.20.1-cp310-cp310-win_amd64.whl", hash = "sha256:32ca378605b41fd180dfe4e14d3226386d8d1b002ab31c969c366549e66a2bb7"},
-    {file = "protobuf-3.20.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9be73ad47579abc26c12024239d3540e6b765182a91dbc88e23658ab71767153"},
-    {file = "protobuf-3.20.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:097c5d8a9808302fb0da7e20edf0b8d4703274d140fd25c5edabddcde43e081f"},
-    {file = "protobuf-3.20.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e250a42f15bf9d5b09fe1b293bdba2801cd520a9f5ea2d7fb7536d4441811d20"},
-    {file = "protobuf-3.20.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cdee09140e1cd184ba9324ec1df410e7147242b94b5f8b0c64fc89e38a8ba531"},
-    {file = "protobuf-3.20.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:af0ebadc74e281a517141daad9d0f2c5d93ab78e9d455113719a45a49da9db4e"},
-    {file = "protobuf-3.20.1-cp37-cp37m-win32.whl", hash = "sha256:755f3aee41354ae395e104d62119cb223339a8f3276a0cd009ffabfcdd46bb0c"},
-    {file = "protobuf-3.20.1-cp37-cp37m-win_amd64.whl", hash = "sha256:62f1b5c4cd6c5402b4e2d63804ba49a327e0c386c99b1675c8a0fefda23b2067"},
-    {file = "protobuf-3.20.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:06059eb6953ff01e56a25cd02cca1a9649a75a7e65397b5b9b4e929ed71d10cf"},
-    {file = "protobuf-3.20.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:cb29edb9eab15742d791e1025dd7b6a8f6fcb53802ad2f6e3adcb102051063ab"},
-    {file = "protobuf-3.20.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:69ccfdf3657ba59569c64295b7d51325f91af586f8d5793b734260dfe2e94e2c"},
-    {file = "protobuf-3.20.1-cp38-cp38-win32.whl", hash = "sha256:dd5789b2948ca702c17027c84c2accb552fc30f4622a98ab5c51fcfe8c50d3e7"},
-    {file = "protobuf-3.20.1-cp38-cp38-win_amd64.whl", hash = "sha256:77053d28427a29987ca9caf7b72ccafee011257561259faba8dd308fda9a8739"},
-    {file = "protobuf-3.20.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6f50601512a3d23625d8a85b1638d914a0970f17920ff39cec63aaef80a93fb7"},
-    {file = "protobuf-3.20.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:284f86a6207c897542d7e956eb243a36bb8f9564c1742b253462386e96c6b78f"},
-    {file = "protobuf-3.20.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7403941f6d0992d40161aa8bb23e12575637008a5a02283a930addc0508982f9"},
-    {file = "protobuf-3.20.1-cp39-cp39-win32.whl", hash = "sha256:db977c4ca738dd9ce508557d4fce0f5aebd105e158c725beec86feb1f6bc20d8"},
-    {file = "protobuf-3.20.1-cp39-cp39-win_amd64.whl", hash = "sha256:7e371f10abe57cee5021797126c93479f59fccc9693dafd6bd5633ab67808a91"},
-    {file = "protobuf-3.20.1-py2.py3-none-any.whl", hash = "sha256:adfc6cf69c7f8c50fd24c793964eef18f0ac321315439d94945820612849c388"},
-    {file = "protobuf-3.20.1.tar.gz", hash = "sha256:adc31566d027f45efe3f44eeb5b1f329da43891634d61c75a5944e9be6dd42c9"},
-]
+prompt-toolkit = []
+protobuf = []
 psycopg2-binary = [
     {file = "psycopg2-binary-2.9.3.tar.gz", hash = "sha256:761df5313dc15da1502b21453642d7599d26be88bff659382f8f9747c7ebea4e"},
     {file = "psycopg2_binary-2.9.3-cp310-cp310-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:539b28661b71da7c0e428692438efbcd048ca21ea81af618d845e06ebfd29478"},
@@ -2690,43 +2574,7 @@ pycryptodome = [
     {file = "pycryptodome-3.13.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:b3af53dddf848afb38b3ac2bae7159ddad1feb9bac14aa3acec6ef1797b82f8d"},
     {file = "pycryptodome-3.13.0.tar.gz", hash = "sha256:95bacf9ff7d1b90bba537d3f5f6c834efe6bfbb1a0195cb3573f29e6716ef08d"},
 ]
-pydantic = [
-    {file = "pydantic-1.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8098a724c2784bf03e8070993f6d46aa2eeca031f8d8a048dff277703e6e193"},
-    {file = "pydantic-1.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c320c64dd876e45254bdd350f0179da737463eea41c43bacbee9d8c9d1021f11"},
-    {file = "pydantic-1.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18f3e912f9ad1bdec27fb06b8198a2ccc32f201e24174cec1b3424dda605a310"},
-    {file = "pydantic-1.9.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c11951b404e08b01b151222a1cb1a9f0a860a8153ce8334149ab9199cd198131"},
-    {file = "pydantic-1.9.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8bc541a405423ce0e51c19f637050acdbdf8feca34150e0d17f675e72d119580"},
-    {file = "pydantic-1.9.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e565a785233c2d03724c4dc55464559639b1ba9ecf091288dd47ad9c629433bd"},
-    {file = "pydantic-1.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:a4a88dcd6ff8fd47c18b3a3709a89adb39a6373f4482e04c1b765045c7e282fd"},
-    {file = "pydantic-1.9.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:447d5521575f18e18240906beadc58551e97ec98142266e521c34968c76c8761"},
-    {file = "pydantic-1.9.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:985ceb5d0a86fcaa61e45781e567a59baa0da292d5ed2e490d612d0de5796918"},
-    {file = "pydantic-1.9.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059b6c1795170809103a1538255883e1983e5b831faea6558ef873d4955b4a74"},
-    {file = "pydantic-1.9.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:d12f96b5b64bec3f43c8e82b4aab7599d0157f11c798c9f9c528a72b9e0b339a"},
-    {file = "pydantic-1.9.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:ae72f8098acb368d877b210ebe02ba12585e77bd0db78ac04a1ee9b9f5dd2166"},
-    {file = "pydantic-1.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:79b485767c13788ee314669008d01f9ef3bc05db9ea3298f6a50d3ef596a154b"},
-    {file = "pydantic-1.9.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:494f7c8537f0c02b740c229af4cb47c0d39840b829ecdcfc93d91dcbb0779892"},
-    {file = "pydantic-1.9.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0f047e11febe5c3198ed346b507e1d010330d56ad615a7e0a89fae604065a0e"},
-    {file = "pydantic-1.9.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:969dd06110cb780da01336b281f53e2e7eb3a482831df441fb65dd30403f4608"},
-    {file = "pydantic-1.9.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:177071dfc0df6248fd22b43036f936cfe2508077a72af0933d0c1fa269b18537"},
-    {file = "pydantic-1.9.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9bcf8b6e011be08fb729d110f3e22e654a50f8a826b0575c7196616780683380"},
-    {file = "pydantic-1.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a955260d47f03df08acf45689bd163ed9df82c0e0124beb4251b1290fa7ae728"},
-    {file = "pydantic-1.9.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9ce157d979f742a915b75f792dbd6aa63b8eccaf46a1005ba03aa8a986bde34a"},
-    {file = "pydantic-1.9.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0bf07cab5b279859c253d26a9194a8906e6f4a210063b84b433cf90a569de0c1"},
-    {file = "pydantic-1.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d93d4e95eacd313d2c765ebe40d49ca9dd2ed90e5b37d0d421c597af830c195"},
-    {file = "pydantic-1.9.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1542636a39c4892c4f4fa6270696902acb186a9aaeac6f6cf92ce6ae2e88564b"},
-    {file = "pydantic-1.9.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a9af62e9b5b9bc67b2a195ebc2c2662fdf498a822d62f902bf27cccb52dbbf49"},
-    {file = "pydantic-1.9.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fe4670cb32ea98ffbf5a1262f14c3e102cccd92b1869df3bb09538158ba90fe6"},
-    {file = "pydantic-1.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:9f659a5ee95c8baa2436d392267988fd0f43eb774e5eb8739252e5a7e9cf07e0"},
-    {file = "pydantic-1.9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b83ba3825bc91dfa989d4eed76865e71aea3a6ca1388b59fc801ee04c4d8d0d6"},
-    {file = "pydantic-1.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1dd8fecbad028cd89d04a46688d2fcc14423e8a196d5b0a5c65105664901f810"},
-    {file = "pydantic-1.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02eefd7087268b711a3ff4db528e9916ac9aa18616da7bca69c1871d0b7a091f"},
-    {file = "pydantic-1.9.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7eb57ba90929bac0b6cc2af2373893d80ac559adda6933e562dcfb375029acee"},
-    {file = "pydantic-1.9.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4ce9ae9e91f46c344bec3b03d6ee9612802682c1551aaf627ad24045ce090761"},
-    {file = "pydantic-1.9.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:72ccb318bf0c9ab97fc04c10c37683d9eea952ed526707fabf9ac5ae59b701fd"},
-    {file = "pydantic-1.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:61b6760b08b7c395975d893e0b814a11cf011ebb24f7d869e7118f5a339a82e1"},
-    {file = "pydantic-1.9.1-py3-none-any.whl", hash = "sha256:4988c0f13c42bfa9ddd2fe2f569c9d54646ce84adc5de84228cfe83396f3bd58"},
-    {file = "pydantic-1.9.1.tar.gz", hash = "sha256:1ed987c3ff29fff7fd8c3ea3a3ea877ad310aae2ef9889a119e22d3f2db0691a"},
-]
+pydantic = []
 pydocstyle = [
     {file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"},
     {file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"},
@@ -2735,10 +2583,7 @@ pyflakes = [
     {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"},
     {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"},
 ]
-pyjwt = [
-    {file = "PyJWT-2.4.0-py3-none-any.whl", hash = "sha256:72d1d253f32dbd4f5c88eaf1fdc62f3a19f676ccbadb9dbc5d07e951b2b26daf"},
-    {file = "PyJWT-2.4.0.tar.gz", hash = "sha256:d42908208c699b3b973cbeb01a969ba6a96c821eefb1c5bfe4c390c01d67abba"},
-]
+pyjwt = []
 pylint = [
     {file = "pylint-2.13.9-py3-none-any.whl", hash = "sha256:705c620d388035bdd9ff8b44c5bcdd235bfb49d276d488dd2c8ff1736aa42526"},
     {file = "pylint-2.13.9.tar.gz", hash = "sha256:095567c96e19e6f57b5b907e67d265ff535e588fe26b12b5ebe1fc5645b2c731"},
@@ -2789,10 +2634,7 @@ python3-openid = [
     {file = "python3-openid-3.2.0.tar.gz", hash = "sha256:33fbf6928f401e0b790151ed2b5290b02545e8775f982485205a066f874aaeaf"},
     {file = "python3_openid-3.2.0-py3-none-any.whl", hash = "sha256:6626f771e0417486701e0b4daff762e7212e820ca5b29fcc0d05f6f8736dfa6b"},
 ]
-pytz = [
-    {file = "pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"},
-    {file = "pytz-2022.1.tar.gz", hash = "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7"},
-]
+pytz = []
 pyuwsgi = [
     {file = "pyuwsgi-2.0.20-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bd09a7328558728f6e38f77609f44d16f18d8b4dc5f8b8776f657c8892b70337"},
     {file = "pyuwsgi-2.0.20-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:210ad768b8af02bbfd3c0345a51c6d2c56a2333ac268d9a97ecf2d4209f78ef8"},
@@ -2932,10 +2774,7 @@ social-auth-core = [
     {file = "social-auth-core-4.3.0.tar.gz", hash = "sha256:4686f0e43cf12954216875a32e944847bb1dc69e7cd9573d16a9003bb05ca477"},
     {file = "social_auth_core-4.3.0-py3-none-any.whl", hash = "sha256:1e3440d104f743b02dfe258c9d4dba5b4065abf24b2f7eb362b47054d21797df"},
 ]
-sqlparse = [
-    {file = "sqlparse-0.4.2-py3-none-any.whl", hash = "sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d"},
-    {file = "sqlparse-0.4.2.tar.gz", hash = "sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae"},
-]
+sqlparse = []
 stevedore = [
     {file = "stevedore-3.5.0-py3-none-any.whl", hash = "sha256:a547de73308fd7e90075bb4d301405bebf705292fa90a90fc3bcf9133f58616c"},
     {file = "stevedore-3.5.0.tar.gz", hash = "sha256:f40253887d8712eaa2bb0ea3830374416736dc8ec0e22f5a65092c1174c44335"},
@@ -2945,10 +2784,7 @@ structlog = [
     {file = "structlog-21.5.0.tar.gz", hash = "sha256:68c4c29c003714fe86834f347cb107452847ba52414390a7ee583472bde00fc9"},
 ]
 svgwrite = []
-swagger-spec-validator = [
-    {file = "swagger-spec-validator-2.7.4.tar.gz", hash = "sha256:2aee5e1fc0503be9f8299378b10c92169572781573c6de3315e831fd0559ba73"},
-    {file = "swagger_spec_validator-2.7.4-py2.py3-none-any.whl", hash = "sha256:4e373a4db5262e7257fde17d84c5c0178327b8057985ab1be63f580bfa009855"},
-]
+swagger-spec-validator = []
 text-unidecode = [
     {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"},
     {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"},
@@ -2983,10 +2819,7 @@ typed-ast = [
     {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"},
     {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"},
 ]
-types-protobuf = [
-    {file = "types-protobuf-3.19.22.tar.gz", hash = "sha256:d2b26861b0cb46a3c8669b0df507b7ef72e487da66d61f9f3576aa76ce028a83"},
-    {file = "types_protobuf-3.19.22-py3-none-any.whl", hash = "sha256:d291388678af91bb045fafa864f142dc4ac22f5d4cdca097c7d8d8a32fa9b3ab"},
-]
+types-protobuf = []
 types-pyyaml = []
 types-requests = []
 types-urllib3 = []
diff --git a/pyproject.toml b/pyproject.toml
index 9f5241a..2bb48bb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -22,7 +22,7 @@ packages = [
 python = "^3.7"
 nautobot = "^1.2.0"
 nautobot-ssot = "^1.0.1"
-cloudvision = "1.4.4"
+cloudvision = "^1.5.0"
 
 [tool.poetry.dev-dependencies]
 invoke = "*"
@@ -37,6 +37,7 @@ flake8 = "*"
 coverage = "*"
 mkdocs = "*"
 markdown-include = "*"
+parameterized = "^0.8.1"
 
 [tool.black]
 line-length = 120


Test patch:
diff --git a/nautobot_ssot_aristacv/tests/fixtures/__init__.py b/nautobot_ssot_aristacv/tests/fixtures/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/nautobot_ssot_aristacv/tests/fixtures/fixtures.py b/nautobot_ssot_aristacv/tests/fixtures/fixtures.py
new file mode 100644
index 0000000..086d8ee
--- /dev/null
+++ b/nautobot_ssot_aristacv/tests/fixtures/fixtures.py
@@ -0,0 +1,13 @@
+"""Test fixtures to be used with unit tests."""
+import json
+
+
+def load_json(path):
+    """Load a json file."""
+    with open(path, encoding="utf-8") as file:
+        return json.loads(file.read())
+
+
+DEVICE_FIXTURE = load_json("./nautobot_ssot_aristacv/tests/fixtures/get_devices_response.json")
+FIXED_INTF_QUERY = load_json("./nautobot_ssot_aristacv/tests/fixtures/get_interfaces_fixed_client_query.json")
+INTERFACE_FIXTURE = load_json("./nautobot_ssot_aristacv/tests/fixtures/get_interfaces_fixed_response.json")
diff --git a/nautobot_ssot_aristacv/tests/fixtures/get_devices_response.json b/nautobot_ssot_aristacv/tests/fixtures/get_devices_response.json
new file mode 100644
index 0000000..08dabb6
--- /dev/null
+++ b/nautobot_ssot_aristacv/tests/fixtures/get_devices_response.json
@@ -0,0 +1,18 @@
+[
+    {
+        "device_id": "JPE12345678",
+        "hostname": "ams01-edge-01.ntc.com",
+        "fqdn": "ams01-edge-01.ntc.com",
+        "sw_ver": "4.26.5M",
+        "model": "DCS-7280CR2-60",
+        "system_mac_address": "12:34:56:78:ab:cd"
+    },
+    {
+        "device_id": "JPE12345679",
+        "hostname": "ams01-edge-02.ntc.com",
+        "fqdn": "ams01-edge-02.ntc.com",
+        "sw_ver": "4.26.5M",
+        "model": "DCS-7280CR2-60",
+        "system_mac_address": "12:34:56:78:ab:ce"
+    }
+]
\ No newline at end of file
diff --git a/nautobot_ssot_aristacv/tests/fixtures/get_interfaces_fixed_client_query.json b/nautobot_ssot_aristacv/tests/fixtures/get_interfaces_fixed_client_query.json
new file mode 100644
index 0000000..595b84c
--- /dev/null
+++ b/nautobot_ssot_aristacv/tests/fixtures/get_interfaces_fixed_client_query.json
@@ -0,0 +1,478 @@
+[
+    {
+        "notifications": [
+            {
+                "updates": {
+                    "mtu": 1500,
+                    "addr": "ab:cd:ef:12:34:56",
+                    "name": "",
+                    "genId": 0,
+                    "l2Mru": 0,
+                    "l2Mtu": 0,
+                    "duplex": {
+                        "Name": "duplexUnknown",
+                        "Value": 0
+                    },
+                    "intfId": "Ethernet1/1",
+                    "maxMtu": 9214,
+                    "portId": {
+                        "value": 210
+                    },
+                    "ifIndex": 1,
+                    "l2MruMax": 0,
+                    "l2MruMin": 0,
+                    "fecStatus": {
+                        "Name": "fecEncodingUnknown",
+                        "Value": 0
+                    },
+                    "macActive": true,
+                    "speedEnum": {
+                        "Name": "speedUnknown",
+                        "Value": 0
+                    },
+                    "speedMbps": 0,
+                    "counterVal": [
+                        "Sysdb",
+                        "interface",
+                        "status",
+                        "eth",
+                        "phy",
+                        "slice",
+                        "1",
+                        "intfStatus",
+                        "Ethernet1/1",
+                        "counterVal"
+                    ],
+                    "deviceName": "",
+                    "intfConfig": [
+                        "Sysdb",
+                        "interface",
+                        "config",
+                        "eth",
+                        "phy",
+                        "slice",
+                        "1",
+                        "intfConfig",
+                        "Ethernet1/1"
+                    ],
+                    "linkStatus": {
+                        "Name": "linkUp",
+                        "Value": 2
+                    },
+                    "operStatus": {
+                        "Name": "intfOperUp",
+                        "Value": 1
+                    },
+                    "routedAddr": "ab:cd:ef:12:34:56",
+                    "burnedInAddr": "ab:cd:ef:12:34:56",
+                    "enabledState": {
+                        "Name": "enabled",
+                        "Value": 1
+                    },
+                    "loopbackMode": {
+                        "Name": "loopbackNone",
+                        "Value": 0
+                    },
+                    "speedAutoAdj": true,
+                    "autonegStatus": {
+                        "mode": {
+                            "Name": "anegModeUnknown",
+                            "Value": 0
+                        },
+                        "speed": {
+                            "Name": "speedUnknown",
+                            "Value": 0
+                        },
+                        "state": {
+                            "Name": "anegStateUnknown",
+                            "Value": 0
+                        },
+                        "duplex": {
+                            "Name": "duplexUnknown",
+                            "Value": 0
+                        },
+                        "laneCount": {
+                            "Name": "laneCountUnknown",
+                            "Value": 0
+                        },
+                        "fecEncoding": {
+                            "Name": "fecEncodingUnknown",
+                            "Value": 0
+                        },
+                        "advertisement": {
+                            "pause": {
+                                "Name": "pauseUnknown",
+                                "Value": 0
+                            },
+                            "linkModes": {
+                                "mode1GbpsFull": false,
+                                "mode1GbpsHalf": false,
+                                "mode5GbpsFull": false,
+                                "mode10GbpsFull": false,
+                                "mode10MbpsFull": false,
+                                "mode10MbpsHalf": false,
+                                "mode25GbpsFull": false,
+                                "mode40GbpsFull": false,
+                                "mode50GbpsFull": false,
+                                "mode100GbpsFull": false,
+                                "mode100MbpsFull": false,
+                                "mode100MbpsHalf": false,
+                                "mode2p5GbpsFull": false,
+                                "mode50GbpsFull1Lane": false,
+                                "mode100GbpsFull1Lane": false,
+                                "mode100GbpsFull2Lane": false,
+                                "mode200GbpsFull2Lane": false,
+                                "mode200GbpsFull4Lane": false,
+                                "mode200GbpsFull8Lane": false,
+                                "mode400GbpsFull4Lane": false,
+                                "mode400GbpsFull8Lane": false,
+                                "mode800GbpsFull8Lane": false,
+                                "modeHg11GbpsFull1Lane": false,
+                                "modeHg21GbpsFull2Lane": false,
+                                "modeHg27GbpsFull1Lane": false,
+                                "modeHg42GbpsFull4Lane": false,
+                                "modeHg53GbpsFull2Lane": false,
+                                "modeHg106GbpsFull4Lane": false
+                            },
+                            "technologyAbilities": {
+                                "techAbility25gCr": false,
+                                "techAbility25gCrS": false,
+                                "techAbility40gCr4": false,
+                                "techAbility50gCr1": false,
+                                "techAbility100gCr1": false,
+                                "techAbility100gCr2": false,
+                                "techAbility100gCr4": false,
+                                "techAbility200gCr2": false,
+                                "techAbility200gCr4": false,
+                                "techAbility400gCr4": false,
+                                "techAbilityUnknown": false,
+                                "techAbilityEthCon25gCr": false,
+                                "techAbilityEthCon50gCr2": false,
+                                "techAbilityEthCon400gCr8": false
+                            }
+                        },
+                        "rxFlowcontrol": {
+                            "Name": "flowControlStatusUnknown",
+                            "Value": 0
+                        },
+                        "txFlowcontrol": {
+                            "Name": "flowControlStatusUnknown",
+                            "Value": 0
+                        },
+                        "technologyType": {
+                            "Name": "techAbilityUnknown",
+                            "Value": 0
+                        },
+                        "partnerAdvertisement": {
+                            "pause": {
+                                "Name": "pauseUnknown",
+                                "Value": 0
+                            },
+                            "linkModes": {
+                                "mode1GbpsFull": false,
+                                "mode1GbpsHalf": false,
+                                "mode5GbpsFull": false,
+                                "mode10GbpsFull": false,
+                                "mode10MbpsFull": false,
+                                "mode10MbpsHalf": false,
+                                "mode25GbpsFull": false,
+                                "mode40GbpsFull": false,
+                                "mode50GbpsFull": false,
+                                "mode100GbpsFull": false,
+                                "mode100MbpsFull": false,
+                                "mode100MbpsHalf": false,
+                                "mode2p5GbpsFull": false,
+                                "mode50GbpsFull1Lane": false,
+                                "mode100GbpsFull1Lane": false,
+                                "mode100GbpsFull2Lane": false,
+                                "mode200GbpsFull2Lane": false,
+                                "mode200GbpsFull4Lane": false,
+                                "mode200GbpsFull8Lane": false,
+                                "mode400GbpsFull4Lane": false,
+                                "mode400GbpsFull8Lane": false,
+                                "mode800GbpsFull8Lane": false,
+                                "modeHg11GbpsFull1Lane": false,
+                                "modeHg21GbpsFull2Lane": false,
+                                "modeHg27GbpsFull1Lane": false,
+                                "modeHg42GbpsFull4Lane": false,
+                                "modeHg53GbpsFull2Lane": false,
+                                "modeHg106GbpsFull4Lane": false
+                            },
+                            "technologyAbilities": {
+                                "techAbility25gCr": false,
+                                "techAbility25gCrS": false,
+                                "techAbility40gCr4": false,
+                                "techAbility50gCr1": false,
+                                "techAbility100gCr1": false,
+                                "techAbility100gCr2": false,
+                                "techAbility100gCr4": false,
+                                "techAbility200gCr2": false,
+                                "techAbility200gCr4": false,
+                                "techAbility400gCr4": false,
+                                "techAbilityUnknown": false,
+                                "techAbilityEthCon25gCr": false,
+                                "techAbilityEthCon50gCr2": false,
+                                "techAbilityEthCon400gCr8": false
+                            }
+                        }
+                    },
+                    "defaultConfig": [
+                        "Sysdb",
+                        "interface",
+                        "config",
+                        "eth",
+                        "phy",
+                        "default",
+                        "defaultIntfConfig",
+                        "DefaultEthSwitchedPort2"
+                    ],
+                    "macActivePrev": true,
+                    "rxFlowcontrol": {
+                        "Name": "flowControlStatusUnknown",
+                        "Value": 0
+                    },
+                    "timestampMode": {
+                        "Name": "timestampModeDisabled",
+                        "Value": 0
+                    },
+                    "txFlowcontrol": {
+                        "Name": "flowControlStatusUnknown",
+                        "Value": 0
+                    },
+                    "linkModeStatus": {
+                        "Name": "linkModeUnknown",
+                        "Value": 0
+                    },
+                    "aneg25gModeCaps": {
+                        "ieee25g": true,
+                        "consortium25g": true
+                    },
+                    "fecBypassStatus": {
+                        "Name": "fecBypassDisabled",
+                        "Value": 0
+                    },
+                    "macRxLocalFault": false,
+                    "phySignalStatus": {
+                        "Name": "phySignalStatusNotReady",
+                        "Value": 1
+                    },
+                    "relativeIfindex": {
+                        "value": 0
+                    },
+                    "autonegAdvConfig": [
+                        "Sysdb",
+                        "interface",
+                        "status",
+                        "eth",
+                        "phy",
+                        "slice",
+                        "1",
+                        "intfStatus",
+                        "Ethernet1/1",
+                        "autonegAdvConfig"
+                    ],
+                    "macActiveChanges": 0,
+                    "macRxRemoteFault": false,
+                    "operStatusChange": {
+                        "time": 0.0,
+                        "count": 0
+                    },
+                    "rawPhyLinkStatus": {
+                        "Name": "linkUnknown",
+                        "Value": 0
+                    },
+                    "linkStatusChanges": 0,
+                    "phyFecCapabilities": [
+                        "Sysdb",
+                        "interface",
+                        "status",
+                        "eth",
+                        "phy",
+                        "slice",
+                        "1",
+                        "intfStatus",
+                        "Ethernet1/1",
+                        "phyFecCapabilities"
+                    ],
+                    "activeStatusChanges": 0,
+                    "autonegCapabilities": {
+                        "mode": {
+                            "Name": "anegModeUnknown",
+                            "Value": 0
+                        },
+                        "rxPause": false,
+                        "txPause": false,
+                        "linkModes": {
+                            "mode1GbpsFull": false,
+                            "mode1GbpsHalf": false,
+                            "mode5GbpsFull": false,
+                            "mode10GbpsFull": false,
+                            "mode10MbpsFull": false,
+                            "mode10MbpsHalf": false,
+                            "mode25GbpsFull": false,
+                            "mode40GbpsFull": false,
+                            "mode50GbpsFull": false,
+                            "mode100GbpsFull": false,
+                            "mode100MbpsFull": false,
+                            "mode100MbpsHalf": false,
+                            "mode2p5GbpsFull": false,
+                            "mode50GbpsFull1Lane": false,
+                            "mode100GbpsFull1Lane": false,
+                            "mode100GbpsFull2Lane": false,
+                            "mode200GbpsFull2Lane": false,
+                            "mode200GbpsFull4Lane": false,
+                            "mode200GbpsFull8Lane": false,
+                            "mode400GbpsFull4Lane": false,
+                            "mode400GbpsFull8Lane": false,
+                            "mode800GbpsFull8Lane": false,
+                            "modeHg11GbpsFull1Lane": false,
+                            "modeHg21GbpsFull2Lane": false,
+                            "modeHg27GbpsFull1Lane": false,
+                            "modeHg42GbpsFull4Lane": false,
+                            "modeHg53GbpsFull2Lane": false,
+                            "modeHg106GbpsFull4Lane": false
+                        }
+                    },
+                    "debouncedLinkStatus": {
+                        "Name": "linkUnknown",
+                        "Value": 0
+                    },
+                    "lastMacActiveChange": 0.0,
+                    "limitedSpeedAutoAdj": {
+                        "mode1GbpsFull": false,
+                        "mode1GbpsHalf": false,
+                        "mode5GbpsFull": false,
+                        "mode10GbpsFull": false,
+                        "mode10MbpsFull": false,
+                        "mode10MbpsHalf": false,
+                        "mode25GbpsFull": false,
+                        "mode40GbpsFull": false,
+                        "mode50GbpsFull": false,
+                        "mode100GbpsFull": false,
+                        "mode100MbpsFull": false,
+                        "mode100MbpsHalf": false,
+                        "mode2p5GbpsFull": false,
+                        "mode50GbpsFull1Lane": false,
+                        "mode100GbpsFull1Lane": false,
+                        "mode100GbpsFull2Lane": false,
+                        "mode200GbpsFull2Lane": false,
+                        "mode200GbpsFull4Lane": false,
+                        "mode200GbpsFull8Lane": false,
+                        "mode400GbpsFull4Lane": false,
+                        "mode400GbpsFull8Lane": false,
+                        "mode800GbpsFull8Lane": false,
+                        "modeHg11GbpsFull1Lane": false,
+                        "modeHg21GbpsFull2Lane": false,
+                        "modeHg27GbpsFull1Lane": false,
+                        "modeHg42GbpsFull4Lane": false,
+                        "modeHg53GbpsFull2Lane": false,
+                        "modeHg106GbpsFull4Lane": false
+                    },
+                    "fecBypassCapabilities": {
+                        "fecBypassCorrection": false,
+                        "fecBypassIndication": false
+                    },
+                    "loopbackModeSupported": [
+                        "Sysdb",
+                        "interface",
+                        "status",
+                        "eth",
+                        "phy",
+                        "slice",
+                        "1",
+                        "intfStatus",
+                        "Ethernet1/1",
+                        "loopbackModeSupported"
+                    ],
+                    "macRxLocalFaultChanges": 0,
+                    "unidirectionalLinkMode": {
+                        "Name": "uniLinkModeDisabled",
+                        "Value": 0
+                    },
+                    "macRxRemoteFaultChanges": 0,
+                    "phyLinkModeCapabilities": {
+                        "mode1GbpsFull": false,
+                        "mode1GbpsHalf": false,
+                        "mode5GbpsFull": false,
+                        "mode10GbpsFull": false,
+                        "mode10MbpsFull": false,
+                        "mode10MbpsHalf": false,
+                        "mode25GbpsFull": false,
+                        "mode40GbpsFull": false,
+                        "mode50GbpsFull": false,
+                        "mode100GbpsFull": false,
+                        "mode100MbpsFull": false,
+                        "mode100MbpsHalf": false,
+                        "mode2p5GbpsFull": false,
+                        "mode50GbpsFull1Lane": false,
+                        "mode100GbpsFull1Lane": false,
+                        "mode100GbpsFull2Lane": false,
+                        "mode200GbpsFull2Lane": false,
+                        "mode200GbpsFull4Lane": false,
+                        "mode200GbpsFull8Lane": false,
+                        "mode400GbpsFull4Lane": false,
+                        "mode400GbpsFull8Lane": false,
+                        "mode800GbpsFull8Lane": false,
+                        "modeHg11GbpsFull1Lane": false,
+                        "modeHg21GbpsFull2Lane": false,
+                        "modeHg27GbpsFull1Lane": false,
+                        "modeHg42GbpsFull4Lane": false,
+                        "modeHg53GbpsFull2Lane": false,
+                        "modeHg106GbpsFull4Lane": false
+                    },
+                    "lastLinkStatusChangeTime": 0.0,
+                    "macLayerAdditionalStatus": [
+                        "Sysdb",
+                        "interface",
+                        "status",
+                        "eth",
+                        "phy",
+                        "slice",
+                        "1",
+                        "intfStatus",
+                        "Ethernet1/1",
+                        "macLayerAdditionalStatus"
+                    ],
+                    "negotiation25gModeStatus": {
+                        "ieee25g": false,
+                        "consortium25g": false
+                    },
+                    "lastMacRxLocalFaultChange": 0.0,
+                    "lastMacRxRemoteFaultChange": 0.0,
+                    "unidirectionalLinkSupported": false,
+                    "autonegAdvConfigGenerationId": 0,
+                    "modifiedLinkModeCapabilities": {
+                        "mode1GbpsFull": false,
+                        "mode1GbpsHalf": false,
+                        "mode5GbpsFull": false,
+                        "mode10GbpsFull": false,
+                        "mode10MbpsFull": false,
+                        "mode10MbpsHalf": false,
+                        "mode25GbpsFull": false,
+                        "mode40GbpsFull": false,
+                        "mode50GbpsFull": false,
+                        "mode100GbpsFull": false,
+                        "mode100MbpsFull": false,
+                        "mode100MbpsHalf": false,
+                        "mode2p5GbpsFull": false,
+                        "mode50GbpsFull1Lane": false,
+                        "mode100GbpsFull1Lane": false,
+                        "mode100GbpsFull2Lane": false,
+                        "mode200GbpsFull2Lane": false,
+                        "mode200GbpsFull4Lane": false,
+                        "mode200GbpsFull8Lane": false,
+                        "mode400GbpsFull4Lane": false,
+                        "mode400GbpsFull8Lane": false,
+                        "mode800GbpsFull8Lane": false,
+                        "modeHg11GbpsFull1Lane": false,
+                        "modeHg21GbpsFull2Lane": false,
+                        "modeHg27GbpsFull1Lane": false,
+                        "modeHg42GbpsFull4Lane": false,
+                        "modeHg53GbpsFull2Lane": false,
+                        "modeHg106GbpsFull4Lane": false
+                    }
+                }
+            }
+        ]
+    }
+]
\ No newline at end of file
diff --git a/nautobot_ssot_aristacv/tests/fixtures/get_interfaces_fixed_response.json b/nautobot_ssot_aristacv/tests/fixtures/get_interfaces_fixed_response.json
new file mode 100644
index 0000000..23adcb2
--- /dev/null
+++ b/nautobot_ssot_aristacv/tests/fixtures/get_interfaces_fixed_response.json
@@ -0,0 +1,10 @@
+[
+    {
+        "interface": "Ethernet1/1",
+        "link_status": "up",
+        "oper_status": "up",
+        "enabled": true,
+        "mac_addr": "ab:cd:ef:12:34:56",
+        "mtu": 1500
+    }
+]
\ No newline at end of file
diff --git a/nautobot_ssot_aristacv/tests/test_cloudvision_adapter.py b/nautobot_ssot_aristacv/tests/test_cloudvision_adapter.py
new file mode 100644
index 0000000..ce96f1a
--- /dev/null
+++ b/nautobot_ssot_aristacv/tests/test_cloudvision_adapter.py
@@ -0,0 +1,81 @@
+"""Unit tests for the Cloudvision DiffSync adapter class."""
+import uuid
+from unittest.mock import MagicMock, patch
+from django.contrib.contenttypes.models import ContentType
+
+from nautobot.extras.models import Job, JobResult
+from nautobot.utilities.testing import TransactionTestCase
+from nautobot_ssot_aristacv.diffsync.adapters.cloudvision import CloudvisionAdapter
+from nautobot_ssot_aristacv.jobs import CloudVisionDataSource
+from nautobot_ssot_aristacv.tests.fixtures import fixtures
+
+
+class CloudvisionAdapterTestCase(TransactionTestCase):
+    """Test the CloudvisionAdapter class."""
+
+    databases = ("default", "job_logs")
+
+    def setUp(self):
+        """Method to initialize test case."""
+        self.client = MagicMock()
+        self.client.comm_channel = MagicMock()
+
+        self.cloudvision = MagicMock()
+        self.cloudvision.get_devices = MagicMock()
+        self.cloudvision.get_devices.return_value = fixtures.DEVICE_FIXTURE
+        self.cloudvision.get_tags_by_type = MagicMock()
+        self.cloudvision.get_tags_by_type.return_value = []
+        self.cloudvision.get_device_type = MagicMock()
+        self.cloudvision.get_device_type.return_value = "fixedSystem"
+        self.cloudvision.get_interfaces_fixed = MagicMock()
+        self.cloudvision.get_interfaces_fixed.return_value = fixtures.INTERFACE_FIXTURE
+        self.cloudvision.get_interface_mode = MagicMock()
+        self.cloudvision.get_interface_mode.return_value = "access"
+        self.cloudvision.get_interface_transceiver = MagicMock()
+        self.cloudvision.get_interface_transceiver.return_value = "1000BASE-T"
+
+        self.job = CloudVisionDataSource()
+        self.job.job_result = JobResult.objects.create(
+            name=self.job.class_path, obj_type=ContentType.objects.get_for_model(Job), user=None, job_id=uuid.uuid4()
+        )
+        self.cvp = CloudvisionAdapter(job=self.job, conn=self.client)
+
+    def test_load_devices(self):
+        """Test the load_devices() adapter method."""
+        with patch("nautobot_ssot_aristacv.utils.cloudvision.get_devices", self.cloudvision.get_devices):
+            with patch("nautobot_ssot_aristacv.utils.cloudvision.get_device_type", self.cloudvision.get_device_type):
+                with patch(
+                    "nautobot_ssot_aristacv.utils.cloudvision.get_interfaces_fixed",
+                    self.cloudvision.get_interfaces_fixed,
+                ):
+                    self.cvp.load_devices()
+        self.assertEqual(
+            {dev["hostname"] for dev in fixtures.DEVICE_FIXTURE},
+            {dev.get_unique_id() for dev in self.cvp.get_all("device")},
+        )
+
+    def test_load_interfaces(self):
+        """Test the load_interfaces() adapter method."""
+        mock_device = MagicMock()
+        mock_device.name = "mock_device"
+        mock_device.serial = MagicMock()
+        mock_device.serial.return_value = "JPE12345678"
+        mock_device.device_model = MagicMock()
+        mock_device.device_model.return_value = "DCS-7280CR2-60"
+
+        with patch("nautobot_ssot_aristacv.utils.cloudvision.get_device_type", self.cloudvision.get_device_type):
+            with patch(
+                "nautobot_ssot_aristacv.utils.cloudvision.get_interfaces_fixed", self.cloudvision.get_interfaces_fixed
+            ):
+                with patch(
+                    "nautobot_ssot_aristacv.utils.cloudvision.get_interface_mode", self.cloudvision.get_interface_mode
+                ):
+                    with patch(
+                        "nautobot_ssot_aristacv.utils.cloudvision.get_interface_transceiver",
+                        self.cloudvision.get_interface_transceiver,
+                    ):
+                        self.cvp.load_interfaces(mock_device)
+        self.assertEqual(
+            {f"{port['interface']}__mock_device" for port in fixtures.INTERFACE_FIXTURE},
+            {port.get_unique_id() for port in self.cvp.get_all("port")},
+        )
diff --git a/nautobot_ssot_aristacv/tests/test_utils_cloudvision.py b/nautobot_ssot_aristacv/tests/test_utils_cloudvision.py
new file mode 100644
index 0000000..3021cd1
--- /dev/null
+++ b/nautobot_ssot_aristacv/tests/test_utils_cloudvision.py
@@ -0,0 +1,114 @@
+"""Tests of Cloudvision utility methods."""
+from unittest.mock import MagicMock, patch
+from parameterized import parameterized
+
+from nautobot.utilities.testing import TestCase
+from nautobot_ssot_aristacv.utils import cloudvision
+from nautobot_ssot_aristacv.tests.fixtures import fixtures
+
+
+class TestCloudvisionUtils(TestCase):
+    """Test Cloudvision utility methods."""
+
+    databases = ("default", "job_logs")
+
+    def setUp(self):
+        """Setup mock Cloudvision client."""
+        self.client = MagicMock()
+
+    def test_get_devices(self):
+        """Test get_devices function."""
+        device1 = MagicMock()
+        device1.value.key.device_id.value = "JPE12345678"
+        device1.value.hostname.value = "ams01-edge-01.ntc.com"
+        device1.value.fqdn.value = "ams01-edge-01.ntc.com"
+        device1.value.software_version.value = "4.26.5M"
+        device1.value.model_name.value = "DCS-7280CR2-60"
+        device1.value.system_mac_address.value = "12:34:56:78:ab:cd"
+
+        device2 = MagicMock()
+        device2.value.key.device_id.value = "JPE12345679"
+        device2.value.hostname.value = "ams01-edge-02.ntc.com"
+        device2.value.fqdn.value = "ams01-edge-02.ntc.com"
+        device2.value.software_version.value = "4.26.5M"
+        device2.value.model_name.value = "DCS-7280CR2-60"
+        device2.value.system_mac_address.value = "12:34:56:78:ab:ce"
+
+        device_list = [device1, device2]
+
+        device_svc_stub = MagicMock()
+        device_svc_stub.DeviceServiceStub.return_value.GetAll.return_value = device_list
+
+        with patch("nautobot_ssot_aristacv.utils.cloudvision.services", device_svc_stub):
+            results = cloudvision.get_devices(client=self.client)
+        expected = fixtures.DEVICE_FIXTURE
+        self.assertEqual(results, expected)
+
+    def test_get_tags(self):
+        """Test get_tags method."""
+
+        mock_tag = MagicMock()
+        mock_tag.value.key.label.value = "test"
+        mock_tag.value.key.value.value = "test"
+        mock_tag.value.creator_type = 1
+
+        device_tag_stub = MagicMock()
+        device_tag_stub.DeviceTagServiceStub.return_value.GetAll.return_value = [mock_tag]
+
+        with patch("nautobot_ssot_aristacv.utils.cloudvision.tag_services", device_tag_stub):
+            results = cloudvision.get_tags(client=self.client)
+        expected = [{"label": "test", "value": "test", "creator_type": 1}]
+        self.assertEqual(results, expected)
+
+    def test_get_interfaces_fixed(self):
+        """Test get_interfaces_fixed method."""
+        mock_query = MagicMock()
+        mock_query.dataset.type = "device"
+        mock_query.dataset.name = "JPE12345678"
+        mock_query.paths.path_elements = [
+            "\304\005Sysdb",
+            "\304\tinterface",
+            "\304\006status",
+            "\304\003eth",
+            "\304\003phy",
+            "\304\005slice",
+            "\304\0011",
+            "\304\nintfStatus",
+            "\304\00\001",
+        ]
+
+        with patch("cloudvision.Connector.grpc_client.grpcClient.create_query", mock_query):
+            self.client.get = MagicMock()
+            self.client.get.return_value = fixtures.FIXED_INTF_QUERY
+            results = cloudvision.get_interfaces_fixed(client=self.client, dId="JPE12345678")
+        expected = fixtures.INTERFACE_FIXTURE
+        self.assertEqual(results, expected)
+
+    port_types = [
+        ("built_in_gig", {"port_info": {}, "transceiver": "xcvr1000BaseT"}, "1000base-t"),
+        ("build_in_10g_sr", {"port_info": {}, "transceiver": "xcvr10GBaseSr"}, "10gbase-x-xfp"),
+        ("management_port", {"port_info": {"interface": "Management1"}, "transceiver": "Unknown"}, "1000base-t"),
+        ("vlan_port", {"port_info": {"interface": "Vlan100"}, "transceiver": "Unknown"}, "virtual"),
+        ("loopback_port", {"port_info": {"interface": "Loopback0"}, "transceiver": "Unknown"}, "virtual"),
+        ("port_channel_port", {"port_info": {"interface": "Port-Channel10"}, "transceiver": "Unknown"}, "lag"),
+        ("unknown_ethernet_port", {"port_info": {"interface": "Ethernet1"}, "transceiver": "Unknown"}, "other"),
+    ]
+
+    @parameterized.expand(port_types, skip_on_empty=True)
+    def test_get_port_type(self, name, sent, received):  # pylint: disable=unused-argument
+        """Test the get_port_type method."""
+        self.assertEqual(
+            cloudvision.get_port_type(port_info=sent["port_info"], transceiver=sent["transceiver"]), received
+        )
+
+    port_statuses = [
+        ("active_port", {"link_status": "up", "oper_status": "up"}, "active"),
+        ("planned_port", {"link_status": "down", "oper_status": "up"}, "planned"),
+        ("maintenance_port", {"link_status": "down", "oper_status": "down"}, "maintenance"),
+        ("decommissioning_port", {"link_status": "up", "oper_status": "down"}, "decommissioning"),
+    ]
+
+    @parameterized.expand(port_statuses, skip_on_empty=True)
+    def test_get_interface_status(self, name, sent, received):  # pylint: disable=unused-argument
+        """Test the get_interface_status method."""
+        self.assertEqual(cloudvision.get_interface_status(port_info=sent), received)