Repository: nautobot/nautobot-app-golden-config (language: python)

Issue:
Drift Manager docs updates
Automatic updates to docs made by Drift Manager, needs to be cleaned up


Discussion / hints:

Fix patch:
diff --git a/.bandit.yml b/.bandit.yml
index 16ab010a..78140801 100644
--- a/.bandit.yml
+++ b/.bandit.yml
@@ -2,6 +2,6 @@
 # No need to check for security issues in the test scripts!
 exclude_dirs:
   - "./nautobot_golden_config/tests/"
-
+  - "./.venv/"
 skips:
   - "B404"
diff --git a/.cookiecutter.json b/.cookiecutter.json
new file mode 100644
index 00000000..ea65bfeb
--- /dev/null
+++ b/.cookiecutter.json
@@ -0,0 +1,33 @@
+{
+    "cookiecutter": {
+        "codeowner_github_usernames": "@itdependsnetworks @jeffkala @nkallergis",
+        "full_name": "Network to Code, LLC",
+        "email": "opensource@networktocode.com",
+        "github_org": "nautobot",
+        "plugin_name": "nautobot_golden_config",
+        "verbose_name": "Golden Config",
+        "plugin_slug": "nautobot-golden-config",
+        "project_slug": "nautobot-plugin-golden-config",
+        "repo_url": "https://github.com/nautobot/nautobot-plugin-golden-config",
+        "base_url": "golden-config",
+        "min_nautobot_version": "1.4.0",
+        "max_nautobot_version": "1.9999",
+        "nautobot_version": "latest",
+        "camel_name": "NautobotGoldenConfig",
+        "project_short_description": "A plugin for configuration on nautobot",
+        "version": "1.0.0",
+        "model_class_name": "None",
+        "open_source_license": "Apache-2.0",
+        "docs_base_url": "https://docs.nautobot.com",
+        "docs_app_url": "https://docs.nautobot.com/projects/golden-config/en/latest",
+        "_drift_manager": {
+            "template": "https://github.com/nautobot/cookiecutter-nautobot-app.git",
+            "template_dir": "nautobot-app",
+            "template_ref": "develop",
+            "cookie_dir": "",
+            "branch_prefix": "drift-manager",
+            "pull_request_strategy": "create",
+            "post_actions": []
+        }
+    }
+}
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 00000000..2270f496
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,27 @@
+# Docker related
+development/Dockerfile
+development/docker-compose*.yml
+development/*.env
+*.env
+environments/
+
+# Python
+**/*.pyc
+**/*.pyo
+**/__pycache__/
+**/.pytest_cache/
+**/.venv/
+
+
+# Other
+docs/_build
+FAQ.md
+.git/
+.gitignore
+.github
+tasks.py
+LICENSE
+**/*.log
+**/.vscode/
+invoke*.yml
+tasks.py
diff --git a/.flake8 b/.flake8
index aaa63b60..c925648d 100644
--- a/.flake8
+++ b/.flake8
@@ -1,4 +1,15 @@
 [flake8]
-# E501: Line length is enforced by Black, so flake8 doesn't need to check it
-# W503: Black disagrees with this rule, as does PEP 8; Black wins
-ignore = E501, W503, F811, F401, F405, E203
+ignore =
+  E501,  # Line length is enforced by Black, so flake8 doesn't need to check it
+  W503,  # Black disagrees with this rule, as does PEP 8; Black wins
+  # TODO: Review the following rules, not in the template
+  F811,
+  F401,
+  F405,
+  E203,
+exclude = 
+  migrations,
+  __pycache__,
+  manage.py,
+  settings.py,
+  .venv
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 30cca39a..d1982711 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1,2 +1,2 @@
-# Default owners for all files in this repository
+# Default owner(s) of all files in this repository
 * @itdependsnetworks @jeffkala @nkallergis
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 8f26fcbf..cbb194ae 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -5,9 +5,16 @@ about: Report a reproducible bug in the current release of nautobot-golden-confi
 
 ### Environment
 * Python version:  <!-- Example: 3.7.7 -->
-* Nautobot version:  <!-- Example: 1.0.1 -->
+* Nautobot version:  <!-- Example: 1.4.0 -->
 * nautobot-golden-config version:  <!-- Example: 1.0.0 -->
 
+<!-- What did you expect to happen? -->
+### Expected Behavior
+
+
+<!-- What happened instead? -->
+### Observed Behavior
+
 <!--
     Describe in detail the exact steps that someone else can take to reproduce
     this bug using the current release.
@@ -16,10 +23,3 @@ about: Report a reproducible bug in the current release of nautobot-golden-confi
 1.
 2.
 3.
-
-<!-- What did you expect to happen? -->
-### Expected Behavior
-
-
-<!-- What happened instead? -->
-### Observed Behavior
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
index 924a52ad..a8d54617 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -1,11 +1,11 @@
 ---
 name: ✨ Feature Request
 about: Propose a new feature or enhancement
+
 ---
 
 ### Environment
-* Python version:  <!-- Example: 3.7.7 -->
-* Nautobot version:  <!-- Example: 1.0.1 -->
+* Nautobot version:  <!-- Example: 1.4.0 -->
 * nautobot-golden-config version:  <!-- Example: 1.0.0 -->
 
 <!--
diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md
new file mode 100644
index 00000000..2cf47647
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md
@@ -0,0 +1,10 @@
+## New Pull Request
+
+Have you:
+- [ ] Updated the README if necessary?
+- [ ] Updated any configuration settings?
+- [ ] Written a unit test?
+
+## Change Notes
+
+## Justification
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 9912258f..45e563c6 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,5 +1,8 @@
 ---
 name: "CI"
+concurrency:  # Cancel any existing runs of this workflow for this same PR
+  group: "${{ github.workflow }}-${{ github.ref }}"
+  cancel-in-progress: true
 on: # yamllint disable-line rule:truthy rule:comments
   push:
     branches:
@@ -10,7 +13,7 @@ on: # yamllint disable-line rule:truthy rule:comments
   pull_request: ~
 
 env:
-  PLUGIN_NAME: "nautobot-golden-config"
+  PLUGIN_NAME: "nautobot-plugin-golden-config"
 
 jobs:
   black:
@@ -19,7 +22,7 @@ jobs:
       INVOKE_NAUTOBOT_GOLDEN_CONFIG_LOCAL: "True"
     steps:
       - name: "Check out repository code"
-        uses: "actions/checkout@v2"
+        uses: "actions/checkout@v3"
       - name: "Setup environment"
         uses: "networktocode/gh-action-setup-poetry-environment@v4"
       - name: "Linting: black"
@@ -30,7 +33,7 @@ jobs:
       INVOKE_NAUTOBOT_GOLDEN_CONFIG_LOCAL: "True"
     steps:
       - name: "Check out repository code"
-        uses: "actions/checkout@v2"
+        uses: "actions/checkout@v3"
       - name: "Setup environment"
         uses: "networktocode/gh-action-setup-poetry-environment@v4"
       - name: "Linting: bandit"
@@ -41,7 +44,7 @@ jobs:
       INVOKE_NAUTOBOT_GOLDEN_CONFIG_LOCAL: "True"
     steps:
       - name: "Check out repository code"
-        uses: "actions/checkout@v2"
+        uses: "actions/checkout@v3"
       - name: "Setup environment"
         uses: "networktocode/gh-action-setup-poetry-environment@v4"
       - name: "Linting: pydocstyle"
@@ -52,7 +55,7 @@ jobs:
       INVOKE_NAUTOBOT_GOLDEN_CONFIG_LOCAL: "True"
     steps:
       - name: "Check out repository code"
-        uses: "actions/checkout@v2"
+        uses: "actions/checkout@v3"
       - name: "Setup environment"
         uses: "networktocode/gh-action-setup-poetry-environment@v4"
       - name: "Linting: flake8"
@@ -63,7 +66,7 @@ jobs:
       INVOKE_NAUTOBOT_GOLDEN_CONFIG_LOCAL: "True"
     steps:
       - name: "Check out repository code"
-        uses: "actions/checkout@v2"
+        uses: "actions/checkout@v3"
       - name: "Setup environment"
         uses: "networktocode/gh-action-setup-poetry-environment@v4"
       - name: "Linting: yamllint"
@@ -86,7 +89,7 @@ jobs:
       INVOKE_NAUTOBOT_GOLDEN_CONFIG_NAUTOBOT_VER: "${{ matrix.nautobot-version }}"
     steps:
       - name: "Check out repository code"
-        uses: "actions/checkout@v2"
+        uses: "actions/checkout@v3"
       - name: "Setup environment"
         uses: "networktocode/gh-action-setup-poetry-environment@v4"
       - name: "Set up Docker Buildx"
@@ -138,7 +141,7 @@ jobs:
       INVOKE_NAUTOBOT_GOLDEN_CONFIG_NAUTOBOT_VER: "${{ matrix.nautobot-version }}"
     steps:
       - name: "Check out repository code"
-        uses: "actions/checkout@v2"
+        uses: "actions/checkout@v3"
       - name: "Setup environment"
         uses: "networktocode/gh-action-setup-poetry-environment@v4"
       - name: "Set up Docker Buildx"
@@ -173,7 +176,7 @@ jobs:
     if: "startsWith(github.ref, 'refs/tags/v')"
     steps:
       - name: "Check out repository code"
-        uses: "actions/checkout@v2"
+        uses: "actions/checkout@v3"
       - name: "Set up Python"
         uses: "actions/setup-python@v2"
         with:
@@ -202,7 +205,7 @@ jobs:
     if: "startsWith(github.ref, 'refs/tags/v')"
     steps:
       - name: "Check out repository code"
-        uses: "actions/checkout@v2"
+        uses: "actions/checkout@v3"
       - name: "Set up Python"
         uses: "actions/setup-python@v2"
         with:
@@ -226,7 +229,7 @@ jobs:
       - "publish_pypi"
     runs-on: "ubuntu-20.04"
     env:
-      SLACK_WEBHOOK_URL: "${{ '{{ secrets.SLACK_WEBHOOK_URL }}' }}"
+      SLACK_WEBHOOK_URL: "${{ secrets.SLACK_WEBHOOK_URL }}"
       SLACK_MESSAGE: >-
         *NOTIFICATION: NEW-RELEASE-PUBLISHED*\n
         Repository: <${{ github.server_url }}/${{ github.repository }}|${{ github.repository }}>\n
@@ -236,11 +239,12 @@ jobs:
       - name: "Send a notification to Slack"
         # ENVs cannot be used directly in job.if. This is a workaround to check
         # if SLACK_WEBHOOK_URL is present.
-        if: "${{ env.SLACK_WEBHOOK_URL != '' }}"
+        if: "env.SLACK_WEBHOOK_URL != ''"
         uses: "slackapi/slack-github-action@v1.17.0"
         with:
           payload: |
             {
+              "text": "${{ env.SLACK_MESSAGE }}",
               "blocks": [
                 {
                   "type": "section",
diff --git a/.github/workflows/rebake.yaml b/.github/workflows/rebake.yaml
new file mode 100644
index 00000000..6b91c3fd
--- /dev/null
+++ b/.github/workflows/rebake.yaml
@@ -0,0 +1,34 @@
+---
+name: "Rebake Cookie"
+on:  # yamllint disable-line rule:truthy
+  schedule:
+    # Every Saturday at 5:00 am UTC
+    - cron: "0 5 * * 6"
+  workflow_dispatch:
+    inputs:
+      template-ref:
+        description: "The branch or tag to use for the template, using the value from .cookiecutter.json when empty"
+        default: ""
+jobs:
+  rebake:
+    runs-on: "ubuntu-22.04"
+    permissions:
+      actions: "write"
+      contents: "write"
+      packages: "read"
+      pull-requests: "write"
+    container: "ghcr.io/nautobot/cookiecutter-nautobot-app-drift-manager/prod:latest"
+    env:
+      GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
+    steps:
+      - name: "Configure"
+        id: "config"
+        run: |
+          export REBAKE_ARGS='--push'
+          if [[ ${{ github.event_name }} == 'workflow_dispatch' ]]; then
+            export REBAKE_ARGS="$REBAKE_ARGS --template-ref='${{ github.event.inputs.template-ref }}'"
+          fi
+          echo "rebake-args='$REBAKE_ARGS'" >> $GITHUB_OUTPUT
+      - name: "Rebake"
+        run: |
+          python -m ntc_cookie_drift_manager rebake ${{ steps.config.outputs.rebake-args }} '${{ github.repositoryUrl }}'
diff --git a/.gitignore b/.gitignore
index 2605d8cd..856602c2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,11 +1,4 @@
-# Specific ignores
-nautobot_backup.dump
-.creds.env
-creds.env
 nautobot_golden_config/transposer.py
-docker-compose.override.yml
-packages/
-invoke.yml
 
 # Ansible Retry Files
 *.retry
@@ -201,9 +194,6 @@ $RECYCLE.BIN/
 # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
 # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
 
-# Jetbrains IDE configs
-.idea/
-
 # User-specific stuff
 .idea/**/workspace.xml
 .idea/**/tasks.xml
@@ -305,4 +295,16 @@ fabric.properties
 
 ### vscode ###
 .vscode/*
-*.code-workspace
\ No newline at end of file
+*.code-workspace
+
+# Rando
+creds.env
+development/*.txt
+
+# Invoke overrides
+invoke.yml
+
+# Docs
+public
+/compose.yaml
+/dump.sql
diff --git a/.yamllint.yml b/.yamllint.yml
index 58324ed1..8cc3e9a9 100644
--- a/.yamllint.yml
+++ b/.yamllint.yml
@@ -2,9 +2,12 @@
 extends: "default"
 rules:
   comments: "enable"
-  empty-values: "enable"
+  empty-values: "disable"
   indentation:
     indent-sequences: "consistent"
   line-length: "disable"
   quoted-strings:
     quote-type: "double"
+ignore: |
+  .venv/
+  compose.yaml
diff --git a/LICENSE b/LICENSE
index 087f92f5..d46cc975 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 Apache Software License 2.0
 
-Copyright (c) 2021, Network to Code, LLC
+Copyright (c) 2023, Network to Code, LLC
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
diff --git a/development/Dockerfile b/development/Dockerfile
index 7b596e27..0b746de2 100644
--- a/development/Dockerfile
+++ b/development/Dockerfile
@@ -1,8 +1,8 @@
 # -------------------------------------------------------------------------------------
-# Nautobot Plugin Development Dockerfile Template
-# Version: 0.0.1
+# Nautobot App Developement Dockerfile Template
+# Version: 1.1.0
 #
-# Plugins that need to add additional steps or packages can do in the section below.
+# Apps that need to add additional steps or packages can do in the section below.
 # -------------------------------------------------------------------------------------
 # !!! USE CAUTION WHEN MODIFYING LINES BELOW
 
@@ -26,26 +26,25 @@ ENV NAUTOBOT_ROOT ${NAUTOBOT_ROOT}
 # and CI and local development may have a newer version of Poetry
 # Since this is only used for development and we don't ship this container, pinning Poetry back is not expressly necessary
 # We also don't need virtual environments in container
-RUN curl -sSL https://install.python-poetry.org -o /tmp/install-poetry.py && \
-    python /tmp/install-poetry.py && \
-    rm -f /tmp/install-poetry.py && \
-    poetry config virtualenvs.create false
+RUN curl -sSL https://install.python-poetry.org | python3 - && \
+  poetry config virtualenvs.create false
 
 # !!! USE CAUTION WHEN MODIFYING LINES ABOVE
 # -------------------------------------------------------------------------------------
-# Plugin-specific system build/test dependencies.
-#
+# App-specifc system build/test dependencies.
+# 
 # Example: LDAP requires `libldap2-dev` to be apt-installed before the Python package.
 # -------------------------------------------------------------------------------------
 # --> Start safe to modify section
 
-# Uncomment the line below if you are apt-installing any package.
-# RUN apt update
-# RUN apt install libldap2-dev
+# Uncomment the lines below if you are apt-installing any package.
+# RUN apt-get -y update && apt-get -y install \
+#     libldap2-dev \
+#     && rm -rf /var/lib/apt/lists/*
 
 # --> Stop safe to modify section
 # -------------------------------------------------------------------------------------
-# Install Nautobot Plugin
+# Install Nautobot App
 # -------------------------------------------------------------------------------------
 # !!! USE CAUTION WHEN MODIFYING LINES BELOW
 
@@ -54,7 +53,7 @@ WORKDIR /source
 COPY . /source
 
 # Get container's installed Nautobot version as a forced constraint
-# NAUTOBOT_VER may be a branch name and not a published release therefore we need to get the installed version
+# NAUTOBOT_VER may be a branch name and not a published release therefor we need to get the installed version
 #  so pip can use it to recognize local constraints.
 RUN pip show nautobot | grep "^Version: " | sed -e 's/Version: /nautobot==/' > constraints.txt
 
@@ -68,7 +67,7 @@ RUN poetry export -f requirements.txt --with dev --without-hashes --output poetr
 RUN sort poetry_freeze_base.txt poetry_freeze_all.txt | uniq -u > poetry_freeze_dev.txt
 
 # Install all local project as editable, constrained on Nautobot version, to get any additional
-#   direct dependencies of the plugin
+#   direct dependencies of the app
 RUN pip install -c constraints.txt -e .
 
 # Install any dev dependencies frozen from Poetry
diff --git a/development/development.env b/development/development.env
index 080a7c71..6b7d03e2 100644
--- a/development/development.env
+++ b/development/development.env
@@ -7,8 +7,6 @@ NAUTOBOT_BANNER_TOP="Local"
 NAUTOBOT_CHANGELOG_RETENTION=0
 
 NAUTOBOT_DEBUG=True
-NAUTOBOT_DJANGO_EXTENSIONS_ENABLED=True
-NAUTOBOT_DJANGO_TOOLBAR_ENABLED=True
 NAUTOBOT_LOG_LEVEL=DEBUG
 NAUTOBOT_METRICS_ENABLED=True
 NAUTOBOT_NAPALM_TIMEOUT=5
diff --git a/development/docker-compose.base.yml b/development/docker-compose.base.yml
index 26356204..6a0a4c72 100644
--- a/development/docker-compose.base.yml
+++ b/development/docker-compose.base.yml
@@ -22,13 +22,13 @@ services:
       db:
         condition: "service_healthy"
     <<:
-      - *nautobot-build
       - *nautobot-base
+      - *nautobot-build
   worker:
     entrypoint:
       - "sh"
       - "-c"  # this is to evaluate the $NAUTOBOT_LOG_LEVEL from the env
-      - "nautobot-server celery worker -l $$NAUTOBOT_LOG_LEVEL"  ## $$ because of docker-compose
+      - "nautobot-server celery worker -l $$NAUTOBOT_LOG_LEVEL --events"  ## $$ because of docker-compose
     depends_on:
       - "nautobot"
     healthcheck:
diff --git a/development/docker-compose.dev.yml b/development/docker-compose.dev.yml
index b95ecb81..8e8c15db 100644
--- a/development/docker-compose.dev.yml
+++ b/development/docker-compose.dev.yml
@@ -19,6 +19,8 @@ services:
       # - "../../nautobot-plugin-nornir/nautobot_plugin_nornir:/opt/nautobot/lib/python3.11/site-packages/nautobot_plugin_nornir"
       # - "../../nautobot/nautobot:/opt/nautobot/lib/python3.11/site-packages/nautobot"
 
+    healthcheck:
+      test: ["CMD", "true"]  # Due to layering, disable: true won't work. Instead, change the test
   docs:
     entrypoint: "mkdocs serve -v -a 0.0.0.0:8080"
     ports:
@@ -30,6 +32,10 @@ services:
       disable: true
     tty: true
   worker:
+    entrypoint:
+      - "sh"
+      - "-c"  # this is to evaluate the $NAUTOBOT_LOG_LEVEL from the env
+      - "watchmedo auto-restart --directory './' --pattern '*.py' --recursive -- nautobot-server celery worker -l $$NAUTOBOT_LOG_LEVEL --events"  ## $$ because of docker-compose
     volumes:
       - "./nautobot_config.py:/opt/nautobot/nautobot_config.py"
       - "../:/source"
@@ -39,6 +45,8 @@ services:
       # - "../../nornir-nautobot/nornir_nautobot:/opt/nautobot/lib/python3.11/site-packages/nornir_nautobot"
       # - "../../nautobot-plugin-nornir/nautobot_plugin_nornir:/opt/nautobot/lib/python3.11/site-packages/nautobot_plugin_nornir"
       # - "../../nautobot/nautobot:/opt/nautobot/lib/python3.11/site-packages/nautobot"
+    healthcheck:
+      test: ["CMD", "true"]  # Due to layering, disable: true won't work. Instead, change the test
 # To expose postgres or redis to the host uncomment the following
 # postgres:
 #   ports:
diff --git a/development/docker-compose.mysql.yml b/development/docker-compose.mysql.yml
index c7fa6a1f..062ada94 100644
--- a/development/docker-compose.mysql.yml
+++ b/development/docker-compose.mysql.yml
@@ -20,6 +20,7 @@ services:
     image: "mysql:8"
     command:
       - "--default-authentication-plugin=mysql_native_password"
+      - "--max_connections=1000"
     env_file:
       - "development.env"
       - "creds.env"
@@ -27,7 +28,12 @@ services:
     volumes:
       - "mysql_data:/var/lib/mysql"
     healthcheck:
-      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
+      test:
+        - "CMD"
+        - "mysqladmin"
+        - "ping"
+        - "-h"
+        - "localhost"
       timeout: "20s"
       retries: 10
 volumes:
diff --git a/development/docker-compose.postgres.yml b/development/docker-compose.postgres.yml
index 55afdb70..12d1de31 100644
--- a/development/docker-compose.postgres.yml
+++ b/development/docker-compose.postgres.yml
@@ -7,11 +7,13 @@ services:
       - "NAUTOBOT_DB_ENGINE=django.db.backends.postgresql"
   db:
     image: "postgres:13-alpine"
+    command:
+      - "-c"
+      - "max_connections=200"
     env_file:
       - "development.env"
       - "creds.env"
     volumes:
-      # - "./nautobot.sql:/tmp/nautobot.sql"
       - "postgres_data:/var/lib/postgresql/data"
     healthcheck:
       test: "pg_isready --username=$$POSTGRES_USER --dbname=$$POSTGRES_DB"
diff --git a/development/nautobot_config.py b/development/nautobot_config.py
index 546c4417..5599a096 100644
--- a/development/nautobot_config.py
+++ b/development/nautobot_config.py
@@ -1,13 +1,25 @@
 """Nautobot development configuration file."""
-# pylint: disable=invalid-envvar-default
 import os
 import sys
 
 from django.utils.module_loading import import_string
-
-from nautobot.core.settings import *  # noqa: F403
+from nautobot.core.settings import *  # noqa: F403  # pylint: disable=wildcard-import,unused-wildcard-import
 from nautobot.core.settings_funcs import is_truthy, parse_redis_connection
 
+#
+# Debug
+#
+
+DEBUG = is_truthy(os.getenv("NAUTOBOT_DEBUG", False))
+_TESTING = len(sys.argv) > 1 and sys.argv[1] == "test"
+
+if DEBUG and not _TESTING:
+    DEBUG_TOOLBAR_CONFIG = {"SHOW_TOOLBAR_CALLBACK": lambda _request: True}
+
+    if "debug_toolbar" not in INSTALLED_APPS:  # noqa: F405
+        INSTALLED_APPS.append("debug_toolbar")  # noqa: F405
+    if "debug_toolbar.middleware.DebugToolbarMiddleware" not in MIDDLEWARE:  # noqa: F405
+        MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware")  # noqa: F405
 
 #
 # Misc. settings
@@ -16,6 +28,9 @@
 ALLOWED_HOSTS = os.getenv("NAUTOBOT_ALLOWED_HOSTS", "").split(" ")
 SECRET_KEY = os.getenv("NAUTOBOT_SECRET_KEY", "")
 
+#
+# Database
+#
 
 nautobot_db_engine = os.getenv("NAUTOBOT_DB_ENGINE", "django.db.backends.postgresql")
 default_db_settings = {
@@ -45,18 +60,28 @@
     DATABASES["default"]["OPTIONS"] = {"charset": "utf8mb4"}
 
 #
-# Debug
+# Redis
 #
 
-DEBUG = True
+# The django-redis cache is used to establish concurrent locks using Redis.
+CACHES = {
+    "default": {
+        "BACKEND": "django_redis.cache.RedisCache",
+        "LOCATION": parse_redis_connection(redis_database=0),
+        "TIMEOUT": 300,
+        "OPTIONS": {
+            "CLIENT_CLASS": "django_redis.client.DefaultClient",
+        },
+    }
+}
 
-# Django Debug Toolbar
-DEBUG_TOOLBAR_CONFIG = {"SHOW_TOOLBAR_CALLBACK": lambda _request: DEBUG and not TESTING}
+# Redis Cacheops
+CACHEOPS_REDIS = parse_redis_connection(redis_database=1)
 
-if DEBUG and "debug_toolbar" not in INSTALLED_APPS:  # noqa: F405
-    INSTALLED_APPS.append("debug_toolbar")  # noqa: F405
-if DEBUG and "debug_toolbar.middleware.DebugToolbarMiddleware" not in MIDDLEWARE:  # noqa: F405
-    MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware")  # noqa: F405
+#
+# Celery settings are not defined here because they can be overloaded with
+# environment variables. By default they use `CACHES["default"]["LOCATION"]`.
+#
 
 #
 # Logging
@@ -64,10 +89,8 @@
 
 LOG_LEVEL = "DEBUG" if DEBUG else "INFO"
 
-TESTING = len(sys.argv) > 1 and sys.argv[1] == "test"
-
 # Verbose logging during normal development operation, but quiet logging during unit test execution
-if not TESTING:
+if not _TESTING:
     LOGGING = {
         "version": 1,
         "disable_existing_loggers": False,
@@ -101,41 +124,11 @@
             },
         },
     }
-else:
-    LOGGING = {}
 
 #
-# Redis
+# Apps
 #
 
-# The django-redis cache is used to establish concurrent locks using Redis. The
-# django-rq settings will use the same instance/database by default.
-#
-# This "default" server is now used by RQ_QUEUES.
-# >> See: nautobot.core.settings.RQ_QUEUES
-CACHES = {
-    "default": {
-        "BACKEND": "django_redis.cache.RedisCache",
-        "LOCATION": parse_redis_connection(redis_database=0),
-        "TIMEOUT": 300,
-        "OPTIONS": {
-            "CLIENT_CLASS": "django_redis.client.DefaultClient",
-        },
-    }
-}
-
-# RQ_QUEUES is not set here because it just uses the default that gets imported
-# up top via `from nautobot.core.settings import *`.
-
-# Redis Cacheops
-CACHEOPS_REDIS = parse_redis_connection(redis_database=1)
-
-#
-# Celery settings are not defined here because they can be overloaded with
-# environment variables. By default they use `CACHES["default"]["LOCATION"]`.
-#
-
-
 # Enable installed plugins. Add the name of each plugin to the list.
 PLUGINS = ["nautobot_plugin_nornir", "nautobot_golden_config"]
 
@@ -196,6 +189,7 @@
     },
 }
 
+# TODO:Verify this is still needed
 # Modify django_jinja Environment for test cases
 django_jinja_config = None
 for template in TEMPLATES:
diff --git a/invoke.example.yml b/invoke.example.yml
index 60d1ca37..16db4580 100644
--- a/invoke.example.yml
+++ b/invoke.example.yml
@@ -3,7 +3,7 @@ nautobot_golden_config:
   project_name: "nautobot-golden-config"
   nautobot_ver: "latest"
   local: false
-  python_ver: "3.11"
+  python_ver: "3.8"
   compose_dir: "development"
   compose_files:
     - "docker-compose.base.yml"
diff --git a/invoke.mysql.yml b/invoke.mysql.yml
index 10f73b14..5ee06af6 100644
--- a/invoke.mysql.yml
+++ b/invoke.mysql.yml
@@ -3,7 +3,7 @@ nautobot_golden_config:
   project_name: "nautobot-golden-config"
   nautobot_ver: "latest"
   local: false
-  python_ver: "3.11"
+  python_ver: "3.8"
   compose_dir: "development"
   compose_files:
     - "docker-compose.base.yml"
diff --git a/nautobot_golden_config/__init__.py b/nautobot_golden_config/__init__.py
index a652bce3..d16e5bdd 100644
--- a/nautobot_golden_config/__init__.py
+++ b/nautobot_golden_config/__init__.py
@@ -8,10 +8,10 @@
 from jinja2 import StrictUndefined
 from django.db.models.signals import post_migrate
 from nautobot.core.signals import nautobot_database_ready
-from nautobot.extras.plugins import PluginConfig
+from nautobot.extras.plugins import NautobotAppConfig
 
 
-class GoldenConfig(PluginConfig):
+class GoldenConfig(NautobotAppConfig):
     """Plugin configuration for the nautobot_golden_config plugin."""
 
     name = "nautobot_golden_config"
diff --git a/nautobot_golden_config/jobs.py b/nautobot_golden_config/jobs.py
index 09afa3c3..f17a6811 100644
--- a/nautobot_golden_config/jobs.py
+++ b/nautobot_golden_config/jobs.py
@@ -1,5 +1,7 @@
 """Jobs to run backups, intended config, and compliance."""
 # pylint: disable=too-many-function-args,logging-fstring-interpolation
+# TODO: Remove the following ignore, added to be able to pass pylint in CI.
+# pylint: disable=arguments-differ
 
 from datetime import datetime
 
@@ -163,9 +165,7 @@ def run(self, **data):
         # Instantiate a GitRepo object for each GitRepository in GoldenConfigSettings.
         intended_repos = get_refreshed_repos(job_obj=self, repo_type="intended_repository", data=data)
 
-        self.logger.debug(
-            "Building device settings mapping and running intended config nornir play."
-        )
+        self.logger.debug("Building device settings mapping and running intended config nornir play.")
         config_intended(self, data)
 
         # Commit / Push each repo after job is completed.
@@ -396,7 +396,7 @@ def run(self, **data):
         except NornirNautobotException as error:
             error_msg = str(error)
             self.logger.error(error_msg)
-            raise NornirNautobotException(error_msg)
+            raise NornirNautobotException(error_msg) from error
         if self._plan_type in ["intended", "missing", "remediation"]:
             self.logger.debug("Starting config plan generation for compliance features.")
             self._generate_config_plan_from_feature()
@@ -439,7 +439,8 @@ def receive_job_button(self, obj):
         """Run config plan deployment process."""
         self.logger.debug("Starting config plan deployment job.")
         data = {"debug": False, "config_plan": ConfigPlan.objects.filter(id=obj.id)}
-        config_deployment(self, **data=True)
+        # pylint: disable-next=unexpected-keyword-arg
+        config_deployment(self, **data)
 
 
 # Conditionally allow jobs based on whether or not turned on.
diff --git a/nautobot_golden_config/nornir_plays/config_compliance.py b/nautobot_golden_config/nornir_plays/config_compliance.py
index 66fdb420..07db01a3 100644
--- a/nautobot_golden_config/nornir_plays/config_compliance.py
+++ b/nautobot_golden_config/nornir_plays/config_compliance.py
@@ -143,7 +143,8 @@ def run_compliance(  # pylint: disable=too-many-arguments,too-many-locals
     platform = obj.platform.network_driver
     if not rules.get(platform):
         error_msg = (
-            f"E3007: There is no defined `Configuration Rule` for platform network_driver `{platform}`, preemptively failed."
+            f"E3007: There is no defined `Configuration Rule` for platform network_driver `{platform}`, "
+            "preemptively failed."
         )
         logger.log_error(error_msg, extra={"object": obj})
         raise NornirNautobotException(error_msg)
diff --git a/poetry.lock b/poetry.lock
index e6daf3a6..fb9944fb 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
 
 [[package]]
 name = "amqp"
@@ -46,6 +46,17 @@ doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)"]
 test = ["anyio[trio]", "coverage[toml] (>=7)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
 trio = ["trio (>=0.22)"]
 
+[[package]]
+name = "appnope"
+version = "0.1.3"
+description = "Disable App Nap on macOS >= 10.9"
+optional = false
+python-versions = "*"
+files = [
+    {file = "appnope-0.1.3-py2.py3-none-any.whl", hash = "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e"},
+    {file = "appnope-0.1.3.tar.gz", hash = "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24"},
+]
+
 [[package]]
 name = "asgiref"
 version = "3.7.2"
@@ -82,6 +93,23 @@ wrapt = [
     {version = ">=1.14,<2", markers = "python_version >= \"3.11\""},
 ]
 
+[[package]]
+name = "asttokens"
+version = "2.4.0"
+description = "Annotate AST trees with source code positions"
+optional = false
+python-versions = "*"
+files = [
+    {file = "asttokens-2.4.0-py2.py3-none-any.whl", hash = "sha256:cf8fc9e61a86461aa9fb161a14a0841a03c405fa829ac6b202670b3495d2ce69"},
+    {file = "asttokens-2.4.0.tar.gz", hash = "sha256:2e0171b991b2c959acc6c49318049236844a5da1d65ba2672c4880c1c894834e"},
+]
+
+[package.dependencies]
+six = ">=1.12.0"
+
+[package.extras]
+test = ["astroid", "pytest"]
+
 [[package]]
 name = "async-timeout"
 version = "4.0.3"
@@ -125,6 +153,17 @@ files = [
 [package.dependencies]
 pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""}
 
+[[package]]
+name = "backcall"
+version = "0.2.0"
+description = "Specifications for callback functions passed in to an API"
+optional = false
+python-versions = "*"
+files = [
+    {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"},
+    {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"},
+]
+
 [[package]]
 name = "backports-zoneinfo"
 version = "0.2.1"
@@ -627,6 +666,70 @@ mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.2.0)", "types-Pill
 test = ["Pillow", "contourpy[test-no-images]", "matplotlib"]
 test-no-images = ["pytest", "pytest-cov", "wurlitzer"]
 
+[[package]]
+name = "coverage"
+version = "7.3.1"
+description = "Code coverage measurement for Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "coverage-7.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cd0f7429ecfd1ff597389907045ff209c8fdb5b013d38cfa7c60728cb484b6e3"},
+    {file = "coverage-7.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:966f10df9b2b2115da87f50f6a248e313c72a668248be1b9060ce935c871f276"},
+    {file = "coverage-7.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0575c37e207bb9b98b6cf72fdaaa18ac909fb3d153083400c2d48e2e6d28bd8e"},
+    {file = "coverage-7.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:245c5a99254e83875c7fed8b8b2536f040997a9b76ac4c1da5bff398c06e860f"},
+    {file = "coverage-7.3.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c96dd7798d83b960afc6c1feb9e5af537fc4908852ef025600374ff1a017392"},
+    {file = "coverage-7.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:de30c1aa80f30af0f6b2058a91505ea6e36d6535d437520067f525f7df123887"},
+    {file = "coverage-7.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:50dd1e2dd13dbbd856ffef69196781edff26c800a74f070d3b3e3389cab2600d"},
+    {file = "coverage-7.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9c0c19f70d30219113b18fe07e372b244fb2a773d4afde29d5a2f7930765136"},
+    {file = "coverage-7.3.1-cp310-cp310-win32.whl", hash = "sha256:770f143980cc16eb601ccfd571846e89a5fe4c03b4193f2e485268f224ab602f"},
+    {file = "coverage-7.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:cdd088c00c39a27cfa5329349cc763a48761fdc785879220d54eb785c8a38520"},
+    {file = "coverage-7.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:74bb470399dc1989b535cb41f5ca7ab2af561e40def22d7e188e0a445e7639e3"},
+    {file = "coverage-7.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:025ded371f1ca280c035d91b43252adbb04d2aea4c7105252d3cbc227f03b375"},
+    {file = "coverage-7.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6191b3a6ad3e09b6cfd75b45c6aeeffe7e3b0ad46b268345d159b8df8d835f9"},
+    {file = "coverage-7.3.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7eb0b188f30e41ddd659a529e385470aa6782f3b412f860ce22b2491c89b8593"},
+    {file = "coverage-7.3.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75c8f0df9dfd8ff745bccff75867d63ef336e57cc22b2908ee725cc552689ec8"},
+    {file = "coverage-7.3.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7eb3cd48d54b9bd0e73026dedce44773214064be93611deab0b6a43158c3d5a0"},
+    {file = "coverage-7.3.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ac3c5b7e75acac31e490b7851595212ed951889918d398b7afa12736c85e13ce"},
+    {file = "coverage-7.3.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5b4ee7080878077af0afa7238df1b967f00dc10763f6e1b66f5cced4abebb0a3"},
+    {file = "coverage-7.3.1-cp311-cp311-win32.whl", hash = "sha256:229c0dd2ccf956bf5aeede7e3131ca48b65beacde2029f0361b54bf93d36f45a"},
+    {file = "coverage-7.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:c6f55d38818ca9596dc9019eae19a47410d5322408140d9a0076001a3dcb938c"},
+    {file = "coverage-7.3.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5289490dd1c3bb86de4730a92261ae66ea8d44b79ed3cc26464f4c2cde581fbc"},
+    {file = "coverage-7.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ca833941ec701fda15414be400c3259479bfde7ae6d806b69e63b3dc423b1832"},
+    {file = "coverage-7.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd694e19c031733e446c8024dedd12a00cda87e1c10bd7b8539a87963685e969"},
+    {file = "coverage-7.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aab8e9464c00da5cb9c536150b7fbcd8850d376d1151741dd0d16dfe1ba4fd26"},
+    {file = "coverage-7.3.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87d38444efffd5b056fcc026c1e8d862191881143c3aa80bb11fcf9dca9ae204"},
+    {file = "coverage-7.3.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8a07b692129b8a14ad7a37941a3029c291254feb7a4237f245cfae2de78de037"},
+    {file = "coverage-7.3.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:2829c65c8faaf55b868ed7af3c7477b76b1c6ebeee99a28f59a2cb5907a45760"},
+    {file = "coverage-7.3.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f111a7d85658ea52ffad7084088277135ec5f368457275fc57f11cebb15607f"},
+    {file = "coverage-7.3.1-cp312-cp312-win32.whl", hash = "sha256:c397c70cd20f6df7d2a52283857af622d5f23300c4ca8e5bd8c7a543825baa5a"},
+    {file = "coverage-7.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:5ae4c6da8b3d123500f9525b50bf0168023313963e0e2e814badf9000dd6ef92"},
+    {file = "coverage-7.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ca70466ca3a17460e8fc9cea7123c8cbef5ada4be3140a1ef8f7b63f2f37108f"},
+    {file = "coverage-7.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f2781fd3cabc28278dc982a352f50c81c09a1a500cc2086dc4249853ea96b981"},
+    {file = "coverage-7.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6407424621f40205bbe6325686417e5e552f6b2dba3535dd1f90afc88a61d465"},
+    {file = "coverage-7.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:04312b036580ec505f2b77cbbdfb15137d5efdfade09156961f5277149f5e344"},
+    {file = "coverage-7.3.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac9ad38204887349853d7c313f53a7b1c210ce138c73859e925bc4e5d8fc18e7"},
+    {file = "coverage-7.3.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:53669b79f3d599da95a0afbef039ac0fadbb236532feb042c534fbb81b1a4e40"},
+    {file = "coverage-7.3.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:614f1f98b84eb256e4f35e726bfe5ca82349f8dfa576faabf8a49ca09e630086"},
+    {file = "coverage-7.3.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f1a317fdf5c122ad642db8a97964733ab7c3cf6009e1a8ae8821089993f175ff"},
+    {file = "coverage-7.3.1-cp38-cp38-win32.whl", hash = "sha256:defbbb51121189722420a208957e26e49809feafca6afeef325df66c39c4fdb3"},
+    {file = "coverage-7.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:f4f456590eefb6e1b3c9ea6328c1e9fa0f1006e7481179d749b3376fc793478e"},
+    {file = "coverage-7.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f12d8b11a54f32688b165fd1a788c408f927b0960984b899be7e4c190ae758f1"},
+    {file = "coverage-7.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f09195dda68d94a53123883de75bb97b0e35f5f6f9f3aa5bf6e496da718f0cb6"},
+    {file = "coverage-7.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6601a60318f9c3945be6ea0f2a80571f4299b6801716f8a6e4846892737ebe4"},
+    {file = "coverage-7.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07d156269718670d00a3b06db2288b48527fc5f36859425ff7cec07c6b367745"},
+    {file = "coverage-7.3.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:636a8ac0b044cfeccae76a36f3b18264edcc810a76a49884b96dd744613ec0b7"},
+    {file = "coverage-7.3.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5d991e13ad2ed3aced177f524e4d670f304c8233edad3210e02c465351f785a0"},
+    {file = "coverage-7.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:586649ada7cf139445da386ab6f8ef00e6172f11a939fc3b2b7e7c9082052fa0"},
+    {file = "coverage-7.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4aba512a15a3e1e4fdbfed2f5392ec221434a614cc68100ca99dcad7af29f3f8"},
+    {file = "coverage-7.3.1-cp39-cp39-win32.whl", hash = "sha256:6bc6f3f4692d806831c136c5acad5ccedd0262aa44c087c46b7101c77e139140"},
+    {file = "coverage-7.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:553d7094cb27db58ea91332e8b5681bac107e7242c23f7629ab1316ee73c4981"},
+    {file = "coverage-7.3.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:220eb51f5fb38dfdb7e5d54284ca4d0cd70ddac047d750111a68ab1798945194"},
+    {file = "coverage-7.3.1.tar.gz", hash = "sha256:6cb7fe1581deb67b782c153136541e20901aa312ceedaf1467dcb35255787952"},
+]
+
+[package.extras]
+toml = ["tomli"]
+
 [[package]]
 name = "cron-descriptor"
 version = "1.4.0"
@@ -707,6 +810,17 @@ files = [
     {file = "cycler-0.11.0.tar.gz", hash = "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f"},
 ]
 
+[[package]]
+name = "decorator"
+version = "5.1.1"
+description = "Decorators for Humans"
+optional = false
+python-versions = ">=3.5"
+files = [
+    {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"},
+    {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"},
+]
+
 [[package]]
 name = "deepdiff"
 version = "6.4.1"
@@ -1162,6 +1276,20 @@ files = [
 [package.extras]
 test = ["pytest (>=6)"]
 
+[[package]]
+name = "executing"
+version = "1.2.0"
+description = "Get the currently executing AST node of a frame, and other information"
+optional = false
+python-versions = "*"
+files = [
+    {file = "executing-1.2.0-py2.py3-none-any.whl", hash = "sha256:0314a69e37426e3608aada02473b4161d4caf5a4b244d1d0c48072b8fee7bacc"},
+    {file = "executing-1.2.0.tar.gz", hash = "sha256:19da64c18d2d851112f09c287f8d3dbbdf725ab0e569077efb6cdcbd3497c107"},
+]
+
+[package.extras]
+tests = ["asttokens", "littleutils", "pytest", "rich"]
+
 [[package]]
 name = "flake8"
 version = "3.9.2"
@@ -1535,6 +1663,45 @@ files = [
     {file = "invoke-2.2.0.tar.gz", hash = "sha256:ee6cbb101af1a859c7fe84f2a264c059020b0cb7fe3535f9424300ab568f6bd5"},
 ]
 
+[[package]]
+name = "ipython"
+version = "8.12.2"
+description = "IPython: Productive Interactive Computing"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "ipython-8.12.2-py3-none-any.whl", hash = "sha256:ea8801f15dfe4ffb76dea1b09b847430ffd70d827b41735c64a0638a04103bfc"},
+    {file = "ipython-8.12.2.tar.gz", hash = "sha256:c7b80eb7f5a855a88efc971fda506ff7a91c280b42cdae26643e0f601ea281ea"},
+]
+
+[package.dependencies]
+appnope = {version = "*", markers = "sys_platform == \"darwin\""}
+backcall = "*"
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+decorator = "*"
+jedi = ">=0.16"
+matplotlib-inline = "*"
+pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""}
+pickleshare = "*"
+prompt-toolkit = ">=3.0.30,<3.0.37 || >3.0.37,<3.1.0"
+pygments = ">=2.4.0"
+stack-data = "*"
+traitlets = ">=5"
+typing-extensions = {version = "*", markers = "python_version < \"3.10\""}
+
+[package.extras]
+all = ["black", "curio", "docrepr", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.21)", "pandas", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"]
+black = ["black"]
+doc = ["docrepr", "ipykernel", "matplotlib", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"]
+kernel = ["ipykernel"]
+nbconvert = ["nbconvert"]
+nbformat = ["nbformat"]
+notebook = ["ipywidgets", "notebook"]
+parallel = ["ipyparallel"]
+qtconsole = ["qtconsole"]
+test = ["pytest (<7.1)", "pytest-asyncio", "testpath"]
+test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.21)", "pandas", "pytest (<7.1)", "pytest-asyncio", "testpath", "trio"]
+
 [[package]]
 name = "isort"
 version = "5.12.0"
@@ -1552,6 +1719,25 @@ pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"
 plugins = ["setuptools"]
 requirements-deprecated-finder = ["pip-api", "pipreqs"]
 
+[[package]]
+name = "jedi"
+version = "0.19.0"
+description = "An autocompletion tool for Python that can be used for text editors."
+optional = false
+python-versions = ">=3.6"
+files = [
+    {file = "jedi-0.19.0-py2.py3-none-any.whl", hash = "sha256:cb8ce23fbccff0025e9386b5cf85e892f94c9b822378f8da49970471335ac64e"},
+    {file = "jedi-0.19.0.tar.gz", hash = "sha256:bcf9894f1753969cbac8022a8c2eaee06bfa3724e4192470aaffe7eb6272b0c4"},
+]
+
+[package.dependencies]
+parso = ">=0.8.3,<0.9.0"
+
+[package.extras]
+docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"]
+qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"]
+testing = ["Django (<3.1)", "attrs", "colorama", "docopt", "pytest (<7.0.0)"]
+
 [[package]]
 name = "jinja2"
 version = "3.1.2"
@@ -2015,16 +2201,6 @@ files = [
     {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"},
     {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"},
     {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"},
-    {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"},
-    {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"},
-    {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"},
-    {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"},
-    {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"},
-    {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"},
-    {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"},
-    {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"},
-    {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"},
-    {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"},
     {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"},
     {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"},
     {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"},
@@ -2119,6 +2295,20 @@ pillow = ">=6.2.0"
 pyparsing = ">=2.3.1,<3.1"
 python-dateutil = ">=2.7"
 
+[[package]]
+name = "matplotlib-inline"
+version = "0.1.6"
+description = "Inline Matplotlib backend for Jupyter"
+optional = false
+python-versions = ">=3.5"
+files = [
+    {file = "matplotlib-inline-0.1.6.tar.gz", hash = "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"},
+    {file = "matplotlib_inline-0.1.6-py3-none-any.whl", hash = "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311"},
+]
+
+[package.dependencies]
+traitlets = "*"
+
 [[package]]
 name = "mccabe"
 version = "0.6.1"
@@ -2706,6 +2896,21 @@ all = ["gssapi (>=1.4.1)", "invoke (>=2.0)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1
 gssapi = ["gssapi (>=1.4.1)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8)"]
 invoke = ["invoke (>=2.0)"]
 
+[[package]]
+name = "parso"
+version = "0.8.3"
+description = "A Python Parser"
+optional = false
+python-versions = ">=3.6"
+files = [
+    {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"},
+    {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"},
+]
+
+[package.extras]
+qa = ["flake8 (==3.8.3)", "mypy (==0.782)"]
+testing = ["docopt", "pytest (<6.0.0)"]
+
 [[package]]
 name = "pathspec"
 version = "0.11.2"
@@ -2728,6 +2933,31 @@ files = [
     {file = "pbr-5.11.1.tar.gz", hash = "sha256:aefc51675b0b533d56bb5fd1c8c6c0522fe31896679882e1c4c63d5e4a0fccb3"},
 ]
 
+[[package]]
+name = "pexpect"
+version = "4.8.0"
+description = "Pexpect allows easy control of interactive console applications."
+optional = false
+python-versions = "*"
+files = [
+    {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"},
+    {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"},
+]
+
+[package.dependencies]
+ptyprocess = ">=0.5"
+
+[[package]]
+name = "pickleshare"
+version = "0.7.5"
+description = "Tiny 'shelve'-like database with concurrency support"
+optional = false
+python-versions = "*"
+files = [
+    {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"},
+    {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"},
+]
+
 [[package]]
 name = "pillow"
 version = "10.0.0"
@@ -2936,6 +3166,31 @@ files = [
     {file = "psycopg2_binary-2.9.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb3b8d55924a6058a26db69fb1d3e7e32695ff8b491835ba9f479537e14dcf9f"},
 ]
 
+[[package]]
+name = "ptyprocess"
+version = "0.7.0"
+description = "Run a subprocess in a pseudo terminal"
+optional = false
+python-versions = "*"
+files = [
+    {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"},
+    {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"},
+]
+
+[[package]]
+name = "pure-eval"
+version = "0.2.2"
+description = "Safely evaluate AST nodes without side effects"
+optional = false
+python-versions = "*"
+files = [
+    {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"},
+    {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"},
+]
+
+[package.extras]
+tests = ["pytest"]
+
 [[package]]
 name = "pycodestyle"
 version = "2.7.0"
@@ -3254,6 +3509,20 @@ files = [
 [package.dependencies]
 six = ">=1.5"
 
+[[package]]
+name = "python-dotenv"
+version = "1.0.0"
+description = "Read key-value pairs from a .env file and set them as environment variables"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"},
+    {file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"},
+]
+
+[package.extras]
+cli = ["click (>=5.0)"]
+
 [[package]]
 name = "python3-openid"
 version = "3.2.0"
@@ -3347,7 +3616,6 @@ files = [
     {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
     {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
     {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
-    {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
     {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
     {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
     {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
@@ -3355,15 +3623,8 @@ files = [
     {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
     {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
     {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
-    {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
     {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
     {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
-    {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
-    {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
-    {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
-    {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
-    {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
-    {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
     {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
     {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
     {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
@@ -3380,7 +3641,6 @@ files = [
     {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
     {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
     {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
-    {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
     {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
     {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
     {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
@@ -3388,7 +3648,6 @@ files = [
     {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
     {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
     {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
-    {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
     {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
     {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
     {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
@@ -3949,6 +4208,25 @@ dev = ["build", "flake8"]
 doc = ["sphinx"]
 test = ["pytest", "pytest-cov"]
 
+[[package]]
+name = "stack-data"
+version = "0.6.2"
+description = "Extract data from python stack frames and tracebacks for informative displays"
+optional = false
+python-versions = "*"
+files = [
+    {file = "stack_data-0.6.2-py3-none-any.whl", hash = "sha256:cbb2a53eb64e5785878201a97ed7c7b94883f48b87bfb0bbe8b623c74679e4a8"},
+    {file = "stack_data-0.6.2.tar.gz", hash = "sha256:32d2dd0376772d01b6cb9fc996f3c8b57a357089dec328ed4b6553d037eaf815"},
+]
+
+[package.dependencies]
+asttokens = ">=2.1.0"
+executing = ">=1.2.0"
+pure-eval = "*"
+
+[package.extras]
+tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"]
+
 [[package]]
 name = "stevedore"
 version = "5.1.0"
@@ -4047,6 +4325,21 @@ files = [
     {file = "tomlkit-0.12.1.tar.gz", hash = "sha256:38e1ff8edb991273ec9f6181244a6a391ac30e9f5098e7535640ea6be97a7c86"},
 ]
 
+[[package]]
+name = "traitlets"
+version = "5.9.0"
+description = "Traitlets Python configuration system"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "traitlets-5.9.0-py3-none-any.whl", hash = "sha256:9e6ec080259b9a5940c797d58b613b5e31441c2257b87c2e795c5228ae80d2d8"},
+    {file = "traitlets-5.9.0.tar.gz", hash = "sha256:f6cde21a9c68cf756af02035f72d5a723bf607e862e7be33ece505abf4a3bad9"},
+]
+
+[package.extras]
+docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"]
+test = ["argcomplete (>=2.0)", "pre-commit", "pytest", "pytest-mock"]
+
 [[package]]
 name = "transitions"
 version = "0.9.0"
@@ -4339,5 +4632,5 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
 
 [metadata]
 lock-version = "2.0"
-python-versions = "^3.8,<3.12"
-content-hash = "78f0fbd14f3ec37f0d450c6506629a900ec58816ec3b219a938f54c80db1059f"
+python-versions = ">=3.8,<3.12"
+content-hash = "e4fcff9be10ff04cb47ad77598e2ed2525993cc94f232b0dd98aed79a420d418"
diff --git a/pyproject.toml b/pyproject.toml
index f9155520..3ca67bd0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -2,19 +2,13 @@
 name = "nautobot-golden-config"
 version = "1.5.0"
 description = "A plugin for configuration on nautobot"
-authors = ["Network to Code, LLC", "<opensource@networktocode.com>"]
-
+authors = ["Network to Code, LLC <opensource@networktocode.com>"]
 license = "Apache-2.0"
-
 readme = "README.md"
 homepage = "https://docs.nautobot.com/projects/golden-config/en/latest/"
 repository = "https://github.com/nautobot/nautobot-plugin-golden-config"
 documentation = "https://docs.nautobot.com/projects/golden-config/en/latest/"
 keywords = ["nautobot", "nautobot-plugin"]
-include = [
-    "LICENSE",
-    "README.md",
-]
 classifiers = [
     "Intended Audience :: Developers",
     "Development Status :: 5 - Production/Stable",
@@ -24,17 +18,20 @@ classifiers = [
     "Programming Language :: Python :: 3.10",
     "Programming Language :: Python :: 3.11",
 ]
+include = [
+    "LICENSE",
+    "README.md",
+]
 packages = [
     { include = "nautobot_golden_config" },
 ]
 
-
 [tool.poetry.urls]
 "Changelog" = "https://docs.nautobot.com/projects/golden-config/en/latest/admin/release_notes/"
 "Bug Tracker" = "https://github.com/nautobot/nautobot-plugin-golden-config/issues"
 
 [tool.poetry.dependencies]
-python = "^3.8,<3.12"
+python = ">=3.8,<3.12"
 deepdiff = ">=5.5.0,!=6.0,!=6.1,<7"
 django-pivot = "^1.8.1"
 matplotlib = "^3.3.2"
@@ -49,18 +46,19 @@ hier-config = "^2.2.2"
 
 [tool.poetry.group.dev.dependencies]
 bandit = "*"
-# Black 23.x.x configuration changes and migration files are taken into account
 black = "*"
+coverage = "*"
 django-debug-toolbar = "*"
-# we need to pin flake8 because of package dependencies that cause it to downgrade and
-# therefore cause issues with linting since older versions do not take .flake8 as config
-flake8 = "^3.9.2"
+flake8 = "*"
 invoke = "*"
+ipython = "*"
 pydocstyle = "*"
 pylint = "*"
 pylint-django = "*"
 pylint-nautobot = { git = "https://github.com/nautobot/pylint-nautobot.git", branch = "develop" }  # TODO: 2.0: Point to pypi
+python-dotenv = "^1.0.0"
 yamllint = "*"
+toml = "*"
 Markdown = "*"
 # Rendering docs to HTML
 mkdocs = "1.5.2"
@@ -72,7 +70,6 @@ mkdocs-version-annotations = "1.0.0"
 mkdocstrings = "0.22.0"
 mkdocstrings-python = "1.5.2"
 
-
 [tool.black]
 line-length = 120
 target-version = ['py38', 'py39', 'py310', 'py311']
@@ -98,8 +95,11 @@ exclude = '''
 
 [tool.pylint.master]
 # Include the pylint_django plugin to avoid spurious warnings about Django patterns
-load-plugins="pylint_django,pylint_nautobot"
-ignore=["jinja_filters.py", ".venv"]
+load-plugins="pylint_django, pylint_nautobot"
+ignore = [
+  "jinja_filters.py",
+  ".venv",
+]
 
 [tool.pylint.basic]
 # No docstrings required for private methods (Pylint default), or for test_ functions, or for inner Meta classes.
@@ -131,6 +131,7 @@ match-dir = "(?!tests|migrations|development)[^\\.].*"
 # My docstring is on the line after the opening quotes instead of on the same line as them.
 # """
 # We've discussed and concluded that we consider this to be a valid style choice.
+# TODO D417 is not in the template, verify this
 add_ignore = "D212,D417"
 
 [build-system]
@@ -144,4 +145,4 @@ testpaths = [
 addopts = "-vv --doctest-modules"
 
 [tool.pylint-nautobot]
-supported_nautobot_versions = ["2"]
\ No newline at end of file
+supported_nautobot_versions = ["2"]
diff --git a/tasks.py b/tasks.py
index f2e22f03..ce99d5a0 100644
--- a/tasks.py
+++ b/tasks.py
@@ -1,6 +1,6 @@
 """Tasks for use with Invoke.
 
-(c) 2020-2023 Network To Code
+Copyright (c) 2023, Network to Code, LLC
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
@@ -12,11 +12,16 @@
 limitations under the License.
 """
 
-import os
 from distutils.util import strtobool
+from invoke import Collection, task as invoke_task
+import os
+
+from dotenv import load_dotenv
 
-from invoke import Collection
-from invoke import task as invoke_task
+
+def _load_dotenv():
+    load_dotenv("./development/development.env")
+    load_dotenv("./development/creds.env")
 
 
 def is_truthy(arg):
@@ -41,7 +46,7 @@ def is_truthy(arg):
     {
         "nautobot_golden_config": {
             "nautobot_ver": "2.0.0-rc.2",
-            "project_name": "nautobot_golden_config",
+            "project_name": "nautobot-golden-config",
             "python_ver": "3.11",
             "local": False,
             "compose_dir": os.path.join(os.path.dirname(__file__), "development"),
@@ -77,22 +82,23 @@ def task_wrapper(function=None):
 
 
 def docker_compose(context, command, **kwargs):
-    """Helper function for running a specific docker-compose command with all appropriate parameters and environment.
+    """Helper function for running a specific docker compose command with all appropriate parameters and environment.
 
     Args:
         context (obj): Used to run specific commands
-        command (str): Command string to append to the "docker-compose ..." command, such as "build", "up", etc.
+        command (str): Command string to append to the "docker compose ..." command, such as "build", "up", etc.
         **kwargs: Passed through to the context.run() call.
     """
     build_env = {
-        # Note: 'docker-compose logs' will stop following after 60 seconds by default,
+        # Note: 'docker compose logs' will stop following after 60 seconds by default,
         # so we are overriding that by setting this environment variable.
         "COMPOSE_HTTP_TIMEOUT": context.nautobot_golden_config.compose_http_timeout,
         "NAUTOBOT_VER": context.nautobot_golden_config.nautobot_ver,
         "PYTHON_VER": context.nautobot_golden_config.python_ver,
+        **kwargs.pop("env", {}),
     }
     compose_command_tokens = [
-        "docker-compose",
+        "docker compose",
         f"--project-name {context.nautobot_golden_config.project_name}",
         f'--project-directory "{context.nautobot_golden_config.compose_dir}"',
     ]
@@ -108,7 +114,7 @@ def docker_compose(context, command, **kwargs):
     if service is not None:
         compose_command_tokens.append(service)
 
-    print(f'Running docker-compose command "{command}"')
+    print(f'Running docker compose command "{command}"')
     compose_command = " ".join(compose_command_tokens)
 
     return context.run(compose_command, env=build_env, **kwargs)
@@ -125,9 +131,11 @@ def run_command(context, command, **kwargs):
         if "nautobot" in results.stdout:
             compose_command = f"exec nautobot {command}"
         else:
-            compose_command = f"run --entrypoint '{command}' nautobot"
+            compose_command = f"run --rm --entrypoint '{command}' nautobot"
+
+        pty = kwargs.pop("pty", True)
 
-        docker_compose(context, compose_command, pty=True)
+        docker_compose(context, compose_command, pty=pty, **kwargs)
 
 
 # ------------------------------------------------------------------------------
@@ -159,42 +167,66 @@ def generate_packages(context):
     run_command(context, command)
 
 
+@task
+def lock(context):
+    """Generate poetry.lock inside the Nautobot container."""
+    run_command(context, "poetry lock --no-update")
+
+
 # ------------------------------------------------------------------------------
 # START / STOP / DEBUG
 # ------------------------------------------------------------------------------
-@task
-def debug(context):
-    """Start Nautobot and its dependencies in debug mode."""
-    print("Starting Nautobot in debug mode...")
-    docker_compose(context, "up")
+@task(help={"service": "If specified, only affect this service."})
+def debug(context, service=""):
+    """Start specified or all services and its dependencies in debug mode."""
+    print(f"Starting {service} in debug mode...")
+    docker_compose(context, "up", service=service)
 
 
 @task(help={"service": "If specified, only affect this service."})
-def start(context, service=None):
-    """Start Nautobot and its dependencies in detached mode."""
+def start(context, service=""):
+    """Start specified or all services and its dependencies in detached mode."""
     print("Starting Nautobot in detached mode...")
     docker_compose(context, "up --detach", service=service)
 
 
-@task
-def restart(context):
-    """Gracefully restart all containers."""
+@task(help={"service": "If specified, only affect this service."})
+def restart(context, service=""):
+    """Gracefully restart specified or all services."""
     print("Restarting Nautobot...")
-    docker_compose(context, "restart")
+    docker_compose(context, "restart", service=service)
 
 
-@task
-def stop(context):
-    """Stop Nautobot and its dependencies."""
+@task(help={"service": "If specified, only affect this service."})
+def stop(context, service=""):
+    """Stop specified or all services, if service is not specified, remove all containers."""
     print("Stopping Nautobot...")
-    docker_compose(context, "down")
+    docker_compose(context, "stop" if service else "down --remove-orphans", service=service)
 
 
 @task
 def destroy(context):
     """Destroy all containers and volumes."""
     print("Destroying Nautobot...")
-    docker_compose(context, "down --volumes")
+    docker_compose(context, "down --remove-orphans --volumes")
+
+
+@task
+def export(context):
+    """Export docker compose configuration to `compose.yaml` file.
+
+    Useful to:
+
+    - Debug docker compose configuration.
+    - Allow using `docker compose` command directly without invoke.
+    """
+    docker_compose(context, "convert > compose.yaml")
+
+
+@task(name="ps", help={"all": "Show all, including stopped containers"})
+def ps_task(context, all=False):
+    """List containers."""
+    docker_compose(context, f"ps {'--all' if all else ''}")
 
 
 @task
@@ -207,13 +239,13 @@ def vscode(context):
 
 @task(
     help={
-        "service": "Docker-compose service name to view (default: nautobot)",
-        "follow": "Follow logs",
-        "tail": "Tail N number of lines or 'all'",
+        "service": "If specified, only display logs for this service (default: all)",
+        "follow": "Flag to follow logs (default: False)",
+        "tail": "Tail N number of lines (default: all)",
     }
 )
-def logs(context, service="nautobot", follow=False, tail=None):
-    """View the logs of a docker-compose service."""
+def logs(context, service="", follow=False, tail=0):
+    """View the logs of a docker compose service."""
     command = "logs "
 
     if follow:
@@ -221,18 +253,21 @@ def logs(context, service="nautobot", follow=False, tail=None):
     if tail:
         command += f"--tail={tail} "
 
-    command += service
-    docker_compose(context, command)
+    docker_compose(context, command, service=service)
 
 
 # ------------------------------------------------------------------------------
 # ACTIONS
 # ------------------------------------------------------------------------------
-@task
-def nbshell(context):
+@task(help={"file": "Python file to execute"})
+def nbshell(context, file=""):
     """Launch an interactive nbshell session."""
-    command = "nautobot-server nbshell"
-    run_command(context, command)
+    command = [
+        "nautobot-server",
+        "nbshell",
+        f"< '{file}'" if file else "",
+    ]
+    run_command(context, " ".join(command), pty=not bool(file))
 
 
 @task
@@ -244,7 +279,7 @@ def shell_plus(context):
 
 @task
 def cli(context):
-    """Launch a bash shell inside the running Nautobot container."""
+    """Launch a bash shell inside the Nautobot container."""
     run_command(context, "bash")
 
 
@@ -302,6 +337,181 @@ def post_upgrade(context):
     run_command(context, command)
 
 
+@task(
+    help={
+        "service": "Docker compose service name to run command in (default: nautobot).",
+        "command": "Command to run (default: bash).",
+        "file": "File to run command with (default: empty)",
+    },
+)
+def exec(context, service="nautobot", command="bash", file=""):
+    """Launch a command inside the running container (defaults to bash shell inside nautobot container)."""
+    command = [
+        "exec",
+        "--",
+        service,
+        command,
+        f"< '{file}'" if file else "",
+    ]
+    docker_compose(context, " ".join(command), pty=not bool(file))
+
+
+@task(
+    help={
+        "query": "SQL command to execute and quit (default: empty)",
+        "input": "SQL file to execute and quit (default: empty)",
+        "output": "Ouput file, overwrite if exists (default: empty)",
+    }
+)
+def dbshell(context, query="", input="", output=""):
+    """Start database CLI inside the running `db` container.
+
+    Doesn't use `nautobot-server dbshell`, using started `db` service container only.
+    """
+    if input and query:
+        raise ValueError("Cannot specify both, `input` and `query` arguments")
+    if output and not (input or query):
+        raise ValueError("`output` argument requires `input` or `query` argument")
+
+    _load_dotenv()
+
+    service = "db"
+    env_vars = {}
+    command = ["exec"]
+
+    if "docker-compose.mysql.yml" in context.nautobot_golden_config.compose_files:
+        env_vars["MYSQL_PWD"] = os.getenv("MYSQL_PASSWORD")
+        command += [
+            "--env=MYSQL_PWD",
+            "--",
+            service,
+            "mysql",
+            f"--user='{os.getenv('MYSQL_USER')}'",
+            f"--database='{os.getenv('MYSQL_DATABASE')}'",
+        ]
+        if query:
+            command += [f"--execute='{query}'"]
+    elif "docker-compose.postgres.yml" in context.nautobot_golden_config.compose_files:
+        command += [
+            "--",
+            service,
+            "psql",
+            f"--username='{os.getenv('POSTGRES_USER')}'",
+            f"--dbname='{os.getenv('POSTGRES_DB')}'",
+        ]
+        if query:
+            command += [f"--command='{query}'"]
+    else:
+        raise ValueError("Unsupported database backend.")
+
+    if input:
+        command += [f"< '{input}'"]
+    if output:
+        command += [f"> '{output}'"]
+
+    docker_compose(context, " ".join(command), env=env_vars, pty=not (input or output or query))
+
+
+@task(
+    help={
+        "input": "SQL dump file to replace the existing database with. This can be generated using `invoke backup-db` (default: `dump.sql`).",
+    }
+)
+def import_db(context, input="dump.sql"):
+    """Stop Nautobot containers and replace the current database with the dump into the running `db` container."""
+    docker_compose(context, "stop -- nautobot worker")
+
+    _load_dotenv()
+
+    service = "db"
+    env_vars = {}
+    command = ["exec"]
+
+    if "docker-compose.mysql.yml" in context.nautobot_golden_config.compose_files:
+        env_vars["MYSQL_PWD"] = os.getenv("MYSQL_PASSWORD")
+        command += [
+            "--env=MYSQL_PWD",
+            "--",
+            service,
+            "mysql",
+            f"--user='{os.getenv('MYSQL_USER')}'",
+            f"--database='{os.getenv('MYSQL_DATABASE')}'",
+        ]
+    elif "docker-compose.postgres.yml" in context.nautobot_golden_config.compose_files:
+        command += [
+            "--",
+            service,
+            "psql",
+            f"--username='{os.getenv('POSTGRES_USER')}'",
+            "postgres",
+        ]
+    else:
+        raise ValueError("Unsupported database backend.")
+
+    command += [f"< '{input}'"]
+
+    docker_compose(context, " ".join(command), env=env_vars, pty=False)
+
+    print("Database import complete, you can start Nautobot now: `invoke start`")
+
+
+@task(
+    help={
+        "output": "Ouput file, overwrite if exists (default: `dump.sql`)",
+        "readable": "Flag to dump database data in more readable format (default: `True`)",
+    }
+)
+def backup_db(context, output="dump.sql", readable=True):
+    """Dump database into `output` file from running `db` container."""
+    _load_dotenv()
+
+    service = "db"
+    env_vars = {}
+    command = ["exec"]
+
+    if "docker-compose.mysql.yml" in context.nautobot_golden_config.compose_files:
+        env_vars["MYSQL_PWD"] = os.getenv("MYSQL_ROOT_PASSWORD")
+        command += [
+            "--env=MYSQL_PWD",
+            "--",
+            service,
+            "mysqldump",
+            "--user=root",
+            "--add-drop-database",
+            "--skip-extended-insert" if readable else "",
+            "--databases",
+            os.getenv("MYSQL_DATABASE", ""),
+        ]
+    elif "docker-compose.postgres.yml" in context.nautobot_golden_config.compose_files:
+        command += [
+            "--",
+            service,
+            "pg_dump",
+            "--clean",
+            "--create",
+            "--if-exists",
+            f"--username='{os.getenv('POSTGRES_USER')}'",
+            f"--dbname='{os.getenv('POSTGRES_DB')}'",
+        ]
+
+        if readable:
+            command += ["--inserts"]
+    else:
+        raise ValueError("Unsupported database backend.")
+
+    if output:
+        command += [f"> '{output}'"]
+
+    docker_compose(context, " ".join(command), env=env_vars, pty=False)
+
+    print(50 * "=")
+    print("The database backup has been successfully completed and saved to the file:")
+    print(output)
+    print("If you want to import this database backup, please execute the following command:")
+    print(f"invoke import-db --input '{output}'")
+    print(50 * "=")
+
+
 # ------------------------------------------------------------------------------
 # DOCS
 # ------------------------------------------------------------------------------
@@ -317,6 +527,25 @@ def docs(context):
         start(context, service="docs")
 
 
+@task
+def build_and_check_docs(context):
+    """Build documentation to be available within Nautobot."""
+    command = "mkdocs build --no-directory-urls --strict"
+    run_command(context, command)
+
+
+@task(name="help")
+def help_task(context):
+    """Print the help of available tasks."""
+    import tasks  # pylint: disable=all
+
+    root = Collection.from_module(tasks)
+    for task_name in sorted(root.task_names):
+        print(50 * "-")
+        print(f"invoke {task_name} --help")
+        context.run(f"invoke {task_name} --help")
+
+
 # ------------------------------------------------------------------------------
 # TESTS
 # ------------------------------------------------------------------------------
@@ -387,7 +616,7 @@ def yamllint(context):
 @task
 def check_migrations(context):
     """Check for missing migrations."""
-    command = "nautobot-server --config=nautobot/core/tests/nautobot_config.py makemigrations --dry-run --check"
+    command = "nautobot-server makemigrations --dry-run --check"
 
     run_command(context, command)
 
@@ -398,9 +627,19 @@ def check_migrations(context):
         "label": "specify a directory or module to test instead of running all Nautobot tests",
         "failfast": "fail as soon as a single test fails don't run the entire test suite",
         "buffer": "Discard output from passing tests",
+        "pattern": "Run specific test methods, classes, or modules instead of all tests",
+        "verbose": "Enable verbose test output.",
     }
 )
-def unittest(context, keepdb=False, label="nautobot_golden_config", failfast=False, buffer=True):
+def unittest(
+    context,
+    keepdb=False,
+    label="nautobot_golden_config",
+    failfast=False,
+    buffer=True,
+    pattern="",
+    verbose=False,
+):
     """Run Nautobot unit tests."""
     command = f"coverage run --module nautobot.core.cli test {label}"
 
@@ -410,6 +649,11 @@ def unittest(context, keepdb=False, label="nautobot_golden_config", failfast=Fal
         command += " --failfast"
     if buffer:
         command += " --buffer"
+    if pattern:
+        command += f" -k='{pattern}'"
+    if verbose:
+        command += " --verbosity 2"
+
     run_command(context, command)
 
 
@@ -423,10 +667,12 @@ def unittest_coverage(context):
 
 @task(
     help={
-        "failfast": "fail as soon as a single test fails don't run the entire test suite",
+        "failfast": "fail as soon as a single test fails don't run the entire test suite. (default: False)",
+        "keepdb": "Save and re-use test database between test runs for faster re-testing. (default: False)",
+        "lint-only": "Only run linters; unit tests will be excluded. (default: False)",
     }
 )
-def tests(context, failfast=False):
+def tests(context, failfast=False, keepdb=False, lint_only=False):
     """Run all tests for this plugin."""
     # If we are not running locally, start the docker containers so we don't have to for each test
     if not is_truthy(context.nautobot_golden_config.local):
@@ -445,7 +691,10 @@ def tests(context, failfast=False):
     yamllint(context)
     print("Running pylint...")
     pylint(context)
-    print("Running unit tests...")
-    unittest(context, failfast=failfast)
+    print("Running mkdocs...")
+    build_and_check_docs(context)
+    if not lint_only:
+        print("Running unit tests...")
+        unittest(context, failfast=failfast, keepdb=keepdb)
     print("All tests have passed!")
     unittest_coverage(context)


Test patch:
diff --git a/.github/workflows/upstream_testing.yml b/.github/workflows/upstream_testing.yml
index 4b6c5000..0deb8033 100644
--- a/.github/workflows/upstream_testing.yml
+++ b/.github/workflows/upstream_testing.yml
@@ -1,7 +1,7 @@
 ---
-name: "Nautobot Upstream Testing"
+name: "Nautobot Upstream Monitor"
 
-on: # yamllint disable-line rule:truthy rule:comments
+on:  # yamllint disable-line rule:truthy rule:comments
   schedule:
     - cron: "0 4 */2 * *"  # every other day at midnight
 
@@ -10,4 +10,4 @@ jobs:
     uses: "nautobot/nautobot/.github/workflows/plugin_upstream_testing_base.yml@develop"
     with:  # Below could potentially be collapsed into a single argument if a concrete relationship between both is enforced
       invoke_context_name: "NAUTOBOT_GOLDEN_CONFIG"
-      plugin_name: "nautobot-golden-config"
+      plugin_name: "nautobot-plugin-golden-config"
diff --git a/nautobot_golden_config/tests/test_basic.py b/nautobot_golden_config/tests/test_basic.py
index e25dc149..4bb988c9 100644
--- a/nautobot_golden_config/tests/test_basic.py
+++ b/nautobot_golden_config/tests/test_basic.py
@@ -1,9 +1,20 @@
 """Basic tests that do not require Django."""
-import os
 import unittest
-
+import os
 import toml
 
+from nautobot_golden_config import __version__ as project_version
+
+
+class TestVersion(unittest.TestCase):
+    """Test Version is the same."""
+
+    def test_version(self):
+        """Verify that pyproject.toml version is same as version specified in the package."""
+        parent_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
+        poetry_version = toml.load(os.path.join(parent_path, "pyproject.toml"))["tool"]["poetry"]["version"]
+        self.assertEqual(project_version, poetry_version)
+
 
 class TestDocsPackaging(unittest.TestCase):
     """Test Version in doc requirements is the same pyproject."""