Repository: fullctl/fullctl (language: python)

Issue:
external service logo broken in service dropdown



Discussion / hints:

Fix patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index ac127da5..b0e2d584 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,4 +1,14 @@
+fail_fast: false
+exclude: |
+    (?x)^(
+        tests/data/.*
+    )$
 repos:
+  - repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v2.3.0
+    hooks:
+      - id: check-yaml
+      - id: trailing-whitespace
   - repo: local
     hooks:
       - id: system
diff --git a/Ctl/VERSION b/Ctl/VERSION
index aac619f9..42372ba0 100644
--- a/Ctl/VERSION
+++ b/Ctl/VERSION
@@ -1,1 +1,1 @@
-1.0.0-rc0
\ No newline at end of file
+1.0.0-rc10
\ No newline at end of file
diff --git a/Ctl/config.yaml b/Ctl/config.yaml
index a1f22eac..00179d26 100644
--- a/Ctl/config.yaml
+++ b/Ctl/config.yaml
@@ -40,7 +40,7 @@ ctl:
           - Ctl/tmplvars.yaml
         ignore:
           - \.git/
-    
+
     # command to remove temporary files
 
     - name: clean
@@ -68,7 +68,7 @@ ctl:
 
           - stage: python_template
             plugin: python_template
-    
+
 
     - type: changelog
       name: changelog
diff --git a/Ctl/dev/docker-compose.yml b/Ctl/dev/docker-compose.yml
index 230ba5cc..da710340 100644
--- a/Ctl/dev/docker-compose.yml
+++ b/Ctl/dev/docker-compose.yml
@@ -18,6 +18,7 @@ services:
 
 
   aaactl_web:
+    user: "0:0"
     build:
       context: ../../../aaactl
       dockerfile: Dockerfile
@@ -115,27 +116,27 @@ services:
       - ../../../peerctl/src:/srv/service/main
 
 
-#  devicectl_web:
-#    build:
-#      context: ../../../devicectl
-#      dockerfile: Dockerfile
-#    command: runserver 0.0.0.0:6001
-#    env_file:
-#      - .env
-#      - ../../../devicectl/Ctl/dev/.env
-#    environment:
-#      DATABASE_PASSWORD: devPASSWORD
-#      DATABASE_HOST: postgres
-#    logging:
-#      options:
-#        max-size: 100m
-#        max-file: "3"
-#    ports:
-#      # this needs to be set in the shell, compose env vars aren't read yet
-#      - "${DEVICECTL_PORT:-6001}:6001"
-#    volumes:
-#      - ../../src/fullctl:/srv/service/main/fullctl
-#      - ../../../devicectl/src:/srv/service/main
+  devicectl_web:
+    build:
+      context: ../../../devicectl
+      dockerfile: Dockerfile
+    command: runserver 0.0.0.0:6001
+    env_file:
+      - .env
+      - ../../../devicectl/Ctl/dev/.env
+    environment:
+      DATABASE_PASSWORD: devPASSWORD
+      DATABASE_HOST: postgres
+    logging:
+      options:
+        max-size: 100m
+        max-file: "3"
+    ports:
+      # this needs to be set in the shell, compose env vars aren't read yet
+      - "${DEVICECTL_PORT:-6001}:6001"
+    volumes:
+      - ../../src/fullctl:/srv/service/main/fullctl
+      - ../../../devicectl/src:/srv/service/main
 
 
   pdbctl_web:
@@ -190,6 +191,32 @@ services:
       - ../../../prefixctl-reputation/src/reputation_check:/srv/service/venv/lib/python3.9/site-packages/reputation_check:Z
       - ../../../prefixctl-meta/src/prefix_meta:/srv/service/venv/lib/python3.9/site-packages/prefix_meta:Z
 
+  aclctl_web:
+    user: "0:0"
+    build:
+      context: ../../../aclctl
+      dockerfile: Dockerfile
+    command: runserver 0.0.0.0:6001
+    env_file:
+      - .env
+      - ../../../aclctl/Ctl/dev/.env
+    environment:
+      DATABASE_PASSWORD: devPASSWORD
+      DATABASE_HOST: postgres
+      # defaults
+      # DATABASE_USER: account_service
+      # DATABASE_NAME: account_service
+      # DATABASE_PORT: 5432
+    logging:
+      options:
+        max-size: 100m
+        max-file: "3"
+    ports:
+      # this needs to be set in the shell, compose env vars aren't read yet
+      - "${ACLCTL_PORT:-6001}:6001"
+    volumes:
+      - ../../src/fullctl:/srv/service/main/fullctl
+      - ../../../aclctl/src:/srv/service/main:Z
 
 volumes:
   postgres_data:
diff --git a/Dockerfile b/Dockerfile
index c7f39b7c..b28c6288 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -20,18 +20,16 @@ ARG run_deps=" \
     postgresql-libs \
     "
 # env to pass to sub images
-ENV BUILD_DEPS=$build_deps
 ENV RUN_DEPS=$run_deps
 ENV SERVICE_HOME=$install_to
 ENV VIRTUAL_ENV=$virtual_env
 ENV PATH="$VIRTUAL_ENV/bin:$PATH"
-ENV POETRY_VERSION=1.1.11
-
+ENV POETRY_VERSION=1.1.13
 
 # build container
 FROM base as builder
 
-RUN apk --update --no-cache add $BUILD_DEPS
+RUN apk upgrade --nocache --available && apk --no-cache add $build_deps
 
 # Use Pip to install Poetry
 RUN pip install "poetry==$POETRY_VERSION"
@@ -41,9 +39,9 @@ RUN python3 -m venv "$VIRTUAL_ENV"
 
 WORKDIR /build
 
-
 # individual files here instead of COPY . . for caching
 COPY pyproject.toml poetry.lock ./
 RUN poetry install --no-root
+
 # Need to upgrade pip and wheel within Poetry for all its installs
 RUN poetry run pip install --upgrade pip wheel
\ No newline at end of file
diff --git a/docs/api_docs.md b/docs/api_docs.md
new file mode 100644
index 00000000..80c966b7
--- /dev/null
+++ b/docs/api_docs.md
@@ -0,0 +1,28 @@
+# Settings
+
+### Include service-bridge endpoints
+
+By default `service-bridge` endpoints will not be included in the documentation that is generated.
+
+This behaviour is controlled by the `API_DOCS_GENERATE_SERVICE_BRIDGE` setting. Set to `True` or `1` to include service-bridge
+endpoints in the api documenation.
+
+## Autogeneration
+
+api docs will be available at the following paths:
+
+-`/apidocs/schema.json` the schema
+-`/apidocs/redoc` ReDoc instance
+-`/apidocs/swagger` Swagger instance
+
+## Generate static file
+
+You *do not* need to manually generate a static schema file, but you can.
+
+`src` needs to be mounted as a volume to `main` for this to work
+
+```
+export SVC=devicectl
+Ctl/dev/run.sh generateschema --generator_class fullctl.django.rest.api_schema.SchemaGenerator --file main/openapi.yaml
+```
+
diff --git a/docs/authentication.md b/docs/authentication.md
index 3f82b35a..0d9df71b 100644
--- a/docs/authentication.md
+++ b/docs/authentication.md
@@ -2,6 +2,6 @@
 
 Authentication for fullctl based services and applications should be facilitated through [aaactl](https://github.com/fullctl/aaactl).
 
-aaactl is an authentication and account management hub that can also handle service billing to the users. 
+aaactl is an authentication and account management hub that can also handle service billing to the users.
 
 ![authentication flowchart](img/auth-flow.png)
diff --git a/docs/develop.md b/docs/develop.md
index b6ed8640..62c71038 100644
--- a/docs/develop.md
+++ b/docs/develop.md
@@ -107,7 +107,7 @@ At this point you will want to go through each service in order and follow its i
 
 **CentOS7** sometimes seems to have an issue allowing one container to connect to another, see [this](https://stackoverflow.com/questions/39134551/centos-vm-with-docker-getting-host-unreachable-when-trying-to-connect-to-itself/39211891#39211891) and [this](https://github.com/moby/moby/issues/32138) for possible workarounds.
 
-### aaactl 
+### aaactl
 
 Follow [instructions](https://github.com/fullctl/aaactl/blob/prep-release/docs/deploy.md) for the points listed below:
 
@@ -155,7 +155,7 @@ Follow [instructions](https://github.com/fullctl/ixctl/blob/prep-release/docs/qu
 **required setup**
 
 - Authentication and accounts
-- add `PDBCTL_HOST` to pdbctl/Ctl/dev/.env - pointing at your pdbctl instance (e.g., https://localhost:7002)
+- add `PDBCTL_URL` to pdbctl/Ctl/dev/.env - pointing at your pdbctl instance (e.g., https://localhost:7002)
 
 ### run ixctl
 
@@ -173,8 +173,8 @@ Follow [instructions](https://github.com/fullctl/peerctl/blob/prep-release/docs/
 **required setup**
 
 - Authentication and accounts
-- add `PDBCTL_HOST` to pdbctl/Ctl/dev/.env - pointing at your pdbctl instance (e.g., https://localhost:7002)
-- add `IXCTL_HOST` to pdbctl/Ctl/dev/.env - pointing at your pdbctl instance (e.g., https://localhost:7003)
+- add `PDBCTL_URL` to pdbctl/Ctl/dev/.env - pointing at your pdbctl instance (e.g., https://localhost:7002)
+- add `IXCTL_URL` to pdbctl/Ctl/dev/.env - pointing at your pdbctl instance (e.g., https://localhost:7003)
 
 ### run peerctl
 
diff --git a/docs/service_bridge.md b/docs/service_bridge.md
new file mode 100644
index 00000000..ef5fd29b
--- /dev/null
+++ b/docs/service_bridge.md
@@ -0,0 +1,26 @@
+# Service Bridge
+
+The fullctl service bridge is a way for fullctl services to exchange information seamlessly using the RESTful api.
+
+## ServiceBridgeModel
+
+The `ServiceBridgeModel` base class allows a django model to have a main reference at another service.
+
+This reference can then be used to populate data, either once, or continuously if the reference is flagged as the
+source for truth.
+
+### Use case(s)
+
+- **deviceCtl** pulling in device data from **nautobot**
+
+## ServiceBridgeAction
+
+`ServiceBridgeAction` allows one to add database controlled additional pull or push actions for a model through django admin.
+
+These can be used to push data from the model to its references, or pull in additional data from secondary references.
+
+### Use case(s)
+
+- **deviceCtl** pushing facility data and device locations to **nautobot**
+
+
diff --git a/docs/settings.md b/docs/settings.md
index 6ff69f39..dc4195f2 100644
--- a/docs/settings.md
+++ b/docs/settings.md
@@ -2,19 +2,19 @@
 
 ### OAuth
 
-- `OAUTH_TWENTYC_HOST` - "https://account.20c.com"
+- `OAUTH_TWENTYC_URL` - "https://account.20c.com"
 - `OAUTH_TWENTYC_KEY` - oauth application client id
 - `OAUTH_TWENTYC_SECRET` - oauth application secret
 
 ### Service Bridge
 
-- `SERVICE_KEY` - api key to use for fullctl service bridge communications 
-- `AAACTL_HOST` - host url for aaactl service (e.g., "https://account.fullctl.com"). This will automatically be defaulted to `OAUTH_TWENTYC_HOST`
-- `PDBCTL_HOST` - host url for pdbctl service (e.g., "https://pdb.fullctl.com")
-- `IXCTL_HOST` - host url for ixctl service (e.g., "https://ix.fullctl.com")
-- `PEERCTL_HOST` - host url for peerctl service (e.g., "https://peer.fullctl.com")
-- `PREFIXCTL_HOST` - host url for prefixctl service (e.g., "https://prefix.fullctl.com")
-- `DEVICECTL_HOST` - host url for devicectl service (e.g., "https://device.fullctl.com")
+- `SERVICE_KEY` - api key to use for fullctl service bridge communications
+- `AAACTL_URL` - host url for aaactl service (e.g., "https://account.fullctl.com"). This will automatically be defaulted to `OAUTH_TWENTYC_URL`
+- `PDBCTL_URL` - host url for pdbctl service (e.g., "https://pdb.fullctl.com")
+- `IXCTL_URL` - host url for ixctl service (e.g., "https://ix.fullctl.com")
+- `PEERCTL_URL` - host url for peerctl service (e.g., "https://peer.fullctl.com")
+- `PREFIXCTL_URL` - host url for prefixctl service (e.g., "https://prefix.fullctl.com")
+- `DEVICECTL_URL` - host url for devicectl service (e.g., "https://device.fullctl.com")
 
 ### Operational
 
@@ -34,6 +34,6 @@
 
 ### Development and Testing
 
-- `BILLING_INTEGRATIOON` - if False will disable all billing checks 
+- `BILLING_INTEGRATIOON` - if False will disable all billing checks
 - `SERVICE_TAG` - should be the service reference tag (e.g., 'ixctl')
 - `USE_LOCAL_PERMISSIONS` - if `True` permissions will be handled locally instead of through aaactl.
diff --git a/docs/tasks.md b/docs/tasks.md
index f00e8f41..8445ab96 100644
--- a/docs/tasks.md
+++ b/docs/tasks.md
@@ -27,10 +27,10 @@ from fullctl.django.tasks import register
 @register
 class SumTask(Task):
 
-    # required 
+    # required
     class Meta:
         proxy = True
-    
+
     # required
     class HandleRef:
         tag = "task_sum"
@@ -68,7 +68,7 @@ class MyTask(Task):
 
     class Meta:
         proxy = True
-    
+
     class HandleRef:
         tag = "task_my"
 
@@ -99,7 +99,7 @@ Allow a maximum of 1 pending instances to be up for the task.
 class MyTask(Task):
     class Meta:
         proxy = True
-    
+
     class HandleRef:
         tag = "task_my"
 
@@ -118,12 +118,12 @@ by the first task argument
 class MyTask(Task):
     class Meta:
         proxy = True
-    
+
     class HandleRef:
         tag = "task_my"
 
     class TaskMeta:
-        limit = 1 
+        limit = 1
 
     @property
     def generate_limit_id(self):
diff --git a/poetry.lock b/poetry.lock
index ae5b7927..9aaaa4a3 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -12,61 +12,34 @@ typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
 [package.extras]
 tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"]
 
-[[package]]
-name = "atomicwrites"
-version = "1.4.0"
-description = "Atomic file writes."
-category = "dev"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-
 [[package]]
 name = "attrs"
-version = "21.4.0"
+version = "22.1.0"
 description = "Classes Without Boilerplate"
 category = "dev"
 optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+python-versions = ">=3.5"
 
 [package.extras]
-dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"]
+dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"]
 docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
-tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"]
-tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"]
-
-[[package]]
-name = "bandit"
-version = "1.7.4"
-description = "Security oriented static analyser for python code."
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[package.dependencies]
-colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""}
-GitPython = ">=1.0.1"
-PyYAML = ">=5.3.1"
-stevedore = ">=1.20.0"
-
-[package.extras]
-test = ["coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "stestr (>=2.5.0)", "testscenarios (>=0.5.0)", "testtools (>=2.3.0)", "toml", "beautifulsoup4 (>=4.8.0)", "pylint (==1.9.4)"]
-toml = ["toml"]
-yaml = ["pyyaml"]
+tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"]
+tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"]
 
 [[package]]
 name = "black"
-version = "22.3.0"
+version = "22.10.0"
 description = "The uncompromising code formatter."
 category = "dev"
 optional = false
-python-versions = ">=3.6.2"
+python-versions = ">=3.7"
 
 [package.dependencies]
 click = ">=8.0.0"
 mypy-extensions = ">=0.4.3"
 pathspec = ">=0.9.0"
 platformdirs = ">=2"
-tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
+tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""}
 typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""}
 typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}
 
@@ -78,7 +51,7 @@ uvloop = ["uvloop (>=0.15.2)"]
 
 [[package]]
 name = "certifi"
-version = "2022.5.18.1"
+version = "2022.9.24"
 description = "Python package for providing Mozilla's CA Bundle."
 category = "main"
 optional = false
@@ -86,7 +59,7 @@ python-versions = ">=3.6"
 
 [[package]]
 name = "cffi"
-version = "1.15.0"
+version = "1.15.1"
 description = "Foreign Function Interface for Python calling C code."
 category = "main"
 optional = false
@@ -105,11 +78,11 @@ python-versions = ">=3.6.1"
 
 [[package]]
 name = "charset-normalizer"
-version = "2.0.12"
+version = "2.1.1"
 description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
 category = "main"
 optional = false
-python-versions = ">=3.5.0"
+python-versions = ">=3.6.0"
 
 [package.extras]
 unicode_backport = ["unicodedata2"]
@@ -140,7 +113,7 @@ requests = ">=2.7.9"
 
 [[package]]
 name = "colorama"
-version = "0.4.4"
+version = "0.4.5"
 description = "Cross-platform colored terminal text."
 category = "main"
 optional = false
@@ -156,7 +129,7 @@ python-versions = ">=3.6.2,<4.0.0"
 
 [[package]]
 name = "coverage"
-version = "6.4.1"
+version = "6.5.0"
 description = "Code coverage measurement for Python"
 category = "dev"
 optional = false
@@ -170,7 +143,7 @@ toml = ["tomli"]
 
 [[package]]
 name = "cryptography"
-version = "37.0.2"
+version = "38.0.2"
 description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
 category = "main"
 optional = false
@@ -183,7 +156,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)"]
 
@@ -212,7 +185,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
 
 [[package]]
 name = "distlib"
-version = "0.3.4"
+version = "0.3.6"
 description = "Distribution utilities"
 category = "dev"
 optional = false
@@ -220,7 +193,7 @@ python-versions = "*"
 
 [[package]]
 name = "django"
-version = "3.2.13"
+version = "3.2.16"
 description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design."
 category = "main"
 optional = false
@@ -254,7 +227,7 @@ tags = ["django-taggit"]
 
 [[package]]
 name = "django-countries"
-version = "7.3.2"
+version = "7.4.2"
 description = "Provides a country field for Django models."
 category = "main"
 optional = false
@@ -328,7 +301,7 @@ six = "*"
 
 [[package]]
 name = "django-peeringdb"
-version = "2.13.0"
+version = "2.14.1"
 description = "PeeringDB Django models"
 category = "main"
 optional = false
@@ -352,14 +325,14 @@ django = ">1.11,<4.0"
 
 [[package]]
 name = "django-reversion"
-version = "5.0.0"
+version = "5.0.3"
 description = "An extension to the Django web framework that provides version control for model instances."
 category = "main"
 optional = false
 python-versions = ">=3.7"
 
 [package.dependencies]
-django = ">=2.0"
+django = ">=3.2"
 
 [[package]]
 name = "django-structlog"
@@ -376,27 +349,27 @@ structlog = "*"
 
 [[package]]
 name = "djangorestframework"
-version = "3.13.1"
+version = "3.14.0"
 description = "Web APIs for Django, made easy."
 category = "main"
 optional = false
 python-versions = ">=3.6"
 
 [package.dependencies]
-django = ">=2.2"
+django = ">=3.0"
 pytz = "*"
 
 [[package]]
 name = "filelock"
-version = "3.7.1"
+version = "3.8.0"
 description = "A platform independent file lock."
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 
 [package.extras]
-docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"]
-testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"]
+testing = ["pytest-timeout (>=2.1)", "pytest-cov (>=3)", "pytest (>=7.1.2)", "coverage (>=6.4.2)", "covdefaults (>=2.2)"]
+docs = ["sphinx-autodoc-typehints (>=1.19.1)", "sphinx (>=5.1.1)", "furo (>=2022.6.21)"]
 
 [[package]]
 name = "flake8"
@@ -437,29 +410,6 @@ python-versions = "*"
 [package.dependencies]
 pbr = "*"
 
-[[package]]
-name = "gitdb"
-version = "4.0.9"
-description = "Git Object Database"
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-
-[package.dependencies]
-smmap = ">=3.0.1,<6"
-
-[[package]]
-name = "gitpython"
-version = "3.1.27"
-description = "GitPython is a python library used to interact with Git repositories"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[package.dependencies]
-gitdb = ">=4.0.1,<5"
-typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.8\""}
-
 [[package]]
 name = "grainy"
 version = "1.8.1"
@@ -470,7 +420,7 @@ python-versions = ">=3.6,<4.0"
 
 [[package]]
 name = "identify"
-version = "2.5.1"
+version = "2.5.6"
 description = "File identification library for Python"
 category = "dev"
 optional = false
@@ -481,7 +431,7 @@ license = ["ukkonen"]
 
 [[package]]
 name = "idna"
-version = "3.3"
+version = "3.4"
 description = "Internationalized Domain Names in Applications (IDNA)"
 category = "main"
 optional = false
@@ -489,7 +439,7 @@ python-versions = ">=3.5"
 
 [[package]]
 name = "importlib-metadata"
-version = "4.11.4"
+version = "5.0.0"
 description = "Read metadata from Python packages"
 category = "main"
 optional = false
@@ -500,9 +450,9 @@ typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
 zipp = ">=0.5"
 
 [package.extras]
-docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"]
+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", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"]
+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 = "iniconfig"
@@ -528,17 +478,17 @@ plugins = ["setuptools"]
 
 [[package]]
 name = "jinja2"
-version = "2.11.3"
+version = "3.1.2"
 description = "A very fast and expressive template engine."
 category = "dev"
 optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+python-versions = ">=3.7"
 
 [package.dependencies]
-MarkupSafe = ">=0.23"
+MarkupSafe = ">=2.0"
 
 [package.extras]
-i18n = ["Babel (>=0.8)"]
+i18n = ["Babel (>=2.7)"]
 
 [[package]]
 name = "markdown"
@@ -556,14 +506,14 @@ testing = ["coverage", "pyyaml"]
 
 [[package]]
 name = "markdown-include"
-version = "0.6.0"
+version = "0.7.0"
 description = "This is an extension to Python-Markdown which provides an \"include\" function, similar to that found in LaTeX (and also the C pre-processor and Fortran). I originally wrote it for my FORD Fortran auto-documentation generator."
 category = "dev"
 optional = false
 python-versions = "*"
 
 [package.dependencies]
-markdown = "*"
+markdown = ">=3.0"
 
 [[package]]
 name = "markupsafe"
@@ -591,22 +541,23 @@ python-versions = ">=3.6"
 
 [[package]]
 name = "mkdocs"
-version = "1.3.0"
+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"
-Markdown = ">=3.2.1"
+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]
@@ -649,15 +600,15 @@ python-versions = "*"
 
 [[package]]
 name = "nodeenv"
-version = "1.6.0"
+version = "1.7.0"
 description = "Node.js virtual environment builder"
 category = "dev"
 optional = false
-python-versions = "*"
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
 
 [[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
@@ -681,15 +632,15 @@ pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
 
 [[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
@@ -748,7 +699,7 @@ munge = ">=1.0.0,<2.0.0"
 
 [[package]]
 name = "pre-commit"
-version = "2.19.0"
+version = "2.20.0"
 description = "A framework for managing and maintaining multi-language pre-commit hooks."
 category = "dev"
 optional = false
@@ -765,7 +716,7 @@ virtualenv = ">=20.0.8"
 
 [[package]]
 name = "psycopg2-binary"
-version = "2.9.3"
+version = "2.9.4"
 description = "psycopg2 - Python-PostgreSQL Database Adapter"
 category = "main"
 optional = false
@@ -805,16 +756,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]]
@@ -830,14 +781,13 @@ diagrams = ["railroad-diagrams", "jinja2"]
 
 [[package]]
 name = "pytest"
-version = "6.2.5"
+version = "7.1.3"
 description = "pytest: simple powerful testing with Python"
 category = "dev"
 optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
 
 [package.dependencies]
-atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
 attrs = ">=19.2.0"
 colorama = {version = "*", markers = "sys_platform == \"win32\""}
 importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
@@ -845,14 +795,14 @@ iniconfig = "*"
 packaging = "*"
 pluggy = ">=0.12,<2.0"
 py = ">=1.8.2"
-toml = "*"
+tomli = ">=1.0.0"
 
 [package.extras]
-testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
+testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
 
 [[package]]
 name = "pytest-cov"
-version = "3.0.0"
+version = "4.0.0"
 description = "Pytest plugin for measuring coverage."
 category = "dev"
 optional = false
@@ -880,17 +830,6 @@ pytest = ">=5.4.0"
 docs = ["sphinx", "sphinx-rtd-theme"]
 testing = ["django", "django-configurations (>=2.0)"]
 
-[[package]]
-name = "pytest-pythonpath"
-version = "0.7.4"
-description = "pytest plugin for adding to the PYTHONPATH from command line or configs."
-category = "dev"
-optional = false
-python-versions = ">=2.6, <4"
-
-[package.dependencies]
-pytest = ">=2.5.2,<7"
-
 [[package]]
 name = "python-dateutil"
 version = "2.8.2"
@@ -919,7 +858,7 @@ postgresql = ["psycopg2"]
 
 [[package]]
 name = "pytz"
-version = "2022.1"
+version = "2022.4"
 description = "World timezone definitions, modern and historical"
 category = "main"
 optional = false
@@ -927,7 +866,7 @@ python-versions = "*"
 
 [[package]]
 name = "pyupgrade"
-version = "2.34.0"
+version = "3.1.0"
 description = "A tool to automatically upgrade syntax for newer versions."
 category = "dev"
 optional = false
@@ -936,6 +875,14 @@ python-versions = ">=3.7"
 [package.dependencies]
 tokenize-rt = ">=3.2.0"
 
+[[package]]
+name = "pyuwsgi"
+version = "2.0.20"
+description = "The uWSGI server"
+category = "main"
+optional = false
+python-versions = "*"
+
 [[package]]
 name = "pyyaml"
 version = "5.4.1"
@@ -957,7 +904,7 @@ pyyaml = "*"
 
 [[package]]
 name = "requests"
-version = "2.28.0"
+version = "2.28.1"
 description = "Python HTTP for Humans."
 category = "main"
 optional = false
@@ -965,13 +912,13 @@ python-versions = ">=3.7, <4"
 
 [package.dependencies]
 certifi = ">=2017.4.17"
-charset-normalizer = ">=2.0.0,<2.1.0"
+charset-normalizer = ">=2,<3"
 idna = ">=2.5,<4"
 urllib3 = ">=1.21.1,<1.27"
 
 [package.extras]
 socks = ["PySocks (>=1.5.6,!=1.5.7)"]
-use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"]
+use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"]
 
 [[package]]
 name = "requests-oauthlib"
@@ -996,14 +943,6 @@ category = "main"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
 
-[[package]]
-name = "smmap"
-version = "5.0.0"
-description = "A pure Python implementation of a sliding window memory map manager"
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-
 [[package]]
 name = "social-auth-app-django"
 version = "3.4.0"
@@ -1042,50 +981,27 @@ 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
 python-versions = ">=3.5"
 
-[[package]]
-name = "stevedore"
-version = "3.5.0"
-description = "Manage dynamic plugins for Python applications"
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-
-[package.dependencies]
-importlib-metadata = {version = ">=1.7.0", markers = "python_version < \"3.8\""}
-pbr = ">=2.0.0,<2.1.0 || >2.1.0"
-
-[[package]]
-name = "stripe"
-version = "2.76.0"
-description = "Python bindings for the Stripe API"
-category = "main"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-
-[package.dependencies]
-requests = {version = ">=2.20", markers = "python_version >= \"3.0\""}
-
 [[package]]
 name = "structlog"
-version = "21.5.0"
+version = "22.1.0"
 description = "Structured Logging for Python"
 category = "main"
 optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
 
 [package.dependencies]
 typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
 
 [package.extras]
-dev = ["pre-commit", "rich", "cogapp", "tomli", "coverage", "freezegun (>=0.2.8)", "pretend", "pytest-asyncio", "pytest (>=6.0)", "simplejson", "furo", "sphinx", "sphinx-notfound-page", "sphinxcontrib-mermaid", "twisted"]
-docs = ["furo", "sphinx", "sphinx-notfound-page", "sphinxcontrib-mermaid", "twisted"]
-tests = ["coverage", "freezegun (>=0.2.8)", "pretend", "pytest-asyncio", "pytest (>=6.0)", "simplejson"]
+dev = ["pre-commit", "rich", "cogapp", "tomli", "coverage", "freezegun (>=0.2.8)", "pretend", "pytest-asyncio (>=0.17)", "pytest (>=6.0)", "simplejson", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-mermaid", "twisted"]
+docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-mermaid", "twisted"]
+tests = ["coverage", "freezegun (>=0.2.8)", "pretend", "pytest-asyncio (>=0.17)", "pytest (>=6.0)", "simplejson"]
 
 [[package]]
 name = "tmpl"
@@ -1097,11 +1013,11 @@ python-versions = ">=3.6.1,<4.0"
 
 [[package]]
 name = "tokenize-rt"
-version = "4.2.1"
+version = "5.0.0"
 description = "A wrapper around the stdlib `tokenize` which roundtrips."
 category = "dev"
 optional = false
-python-versions = ">=3.6.1"
+python-versions = ">=3.7"
 
 [[package]]
 name = "toml"
@@ -1148,51 +1064,50 @@ python-versions = ">=3.6"
 
 [[package]]
 name = "typing-extensions"
-version = "4.2.0"
+version = "4.4.0"
 description = "Backported and Experimental Type Hints for Python 3.7+"
 category = "main"
 optional = false
 python-versions = ">=3.7"
 
+[[package]]
+name = "uritemplate"
+version = "4.1.1"
+description = "Implementation of RFC 6570 URI Templates"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
 [[package]]
 name = "urllib3"
-version = "1.26.9"
+version = "1.26.12"
 description = "HTTP library with thread-safe connection pooling, file post, and more."
 category = "main"
 optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4"
 
 [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]]
-name = "uwsgi"
-version = "2.0.20"
-description = "The uWSGI server"
-category = "main"
-optional = false
-python-versions = "*"
-
 [[package]]
 name = "virtualenv"
-version = "20.14.1"
+version = "20.16.5"
 description = "Virtual Python Environment builder"
 category = "dev"
 optional = false
-python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
+python-versions = ">=3.6"
 
 [package.dependencies]
-distlib = ">=0.3.1,<1"
-filelock = ">=3.2,<4"
-importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
-platformdirs = ">=2,<3"
-six = ">=1.9.0,<2"
+distlib = ">=0.3.5,<1"
+filelock = ">=3.4.1,<4"
+importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.8\""}
+platformdirs = ">=2.4,<3"
 
 [package.extras]
-docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"]
-testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"]
+docs = ["proselint (>=0.13)", "sphinx (>=5.1.1)", "sphinx-argparse (>=0.3.1)", "sphinx-rtd-theme (>=1)", "towncrier (>=21.9)"]
+testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"]
 
 [[package]]
 name = "watchdog"
@@ -1207,222 +1122,56 @@ watchmedo = ["PyYAML (>=3.10)"]
 
 [[package]]
 name = "whitenoise"
-version = "5.3.0"
+version = "6.2.0"
 description = "Radically simplified static file serving for WSGI applications"
 category = "main"
 optional = false
-python-versions = ">=3.5, <4"
+python-versions = ">=3.7"
 
 [package.extras]
 brotli = ["brotli"]
 
 [[package]]
 name = "zipp"
-version = "3.8.0"
+version = "3.9.0"
 description = "Backport of pathlib-compatible object wrapper for zip files"
 category = "main"
 optional = false
 python-versions = ">=3.7"
 
 [package.extras]
-docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"]
-testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"]
+docs = ["sphinx (>=3.5)", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "furo", "jaraco.tidelift (>=1.4)"]
+testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "flake8 (<5)", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "func-timeout", "jaraco.functools", "more-itertools", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"]
 
 [metadata]
 lock-version = "1.1"
 python-versions = "^3.7"
-content-hash = "f6108012cfc76ed33d6eb7fc721649437fed704fa0525a8d37565c3feca5d8fd"
+content-hash = "730bd57b2c0d4727db8d19aa2f714dad8fdaa4ec8932eaa3cc9606dfcec8b1d1"
 
 [metadata.files]
-asgiref = [
-    {file = "asgiref-3.5.2-py3-none-any.whl", hash = "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4"},
-    {file = "asgiref-3.5.2.tar.gz", hash = "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424"},
-]
-atomicwrites = [
-    {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
-    {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
-]
-attrs = [
-    {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"},
-    {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"},
-]
-bandit = [
-    {file = "bandit-1.7.4-py3-none-any.whl", hash = "sha256:412d3f259dab4077d0e7f0c11f50f650cc7d10db905d98f6520a95a18049658a"},
-    {file = "bandit-1.7.4.tar.gz", hash = "sha256:2d63a8c573417bae338962d4b9b06fbc6080f74ecd955a092849e1e65c717bd2"},
-]
-black = [
-    {file = "black-22.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09"},
-    {file = "black-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb"},
-    {file = "black-22.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a"},
-    {file = "black-22.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968"},
-    {file = "black-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d"},
-    {file = "black-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce"},
-    {file = "black-22.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82"},
-    {file = "black-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b"},
-    {file = "black-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015"},
-    {file = "black-22.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b"},
-    {file = "black-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a"},
-    {file = "black-22.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163"},
-    {file = "black-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464"},
-    {file = "black-22.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0"},
-    {file = "black-22.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176"},
-    {file = "black-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0"},
-    {file = "black-22.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20"},
-    {file = "black-22.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a"},
-    {file = "black-22.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad"},
-    {file = "black-22.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21"},
-    {file = "black-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265"},
-    {file = "black-22.3.0-py3-none-any.whl", hash = "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72"},
-    {file = "black-22.3.0.tar.gz", hash = "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79"},
-]
-certifi = [
-    {file = "certifi-2022.5.18.1-py3-none-any.whl", hash = "sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a"},
-    {file = "certifi-2022.5.18.1.tar.gz", hash = "sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7"},
-]
-cffi = [
-    {file = "cffi-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962"},
-    {file = "cffi-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0"},
-    {file = "cffi-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14"},
-    {file = "cffi-1.15.0-cp27-cp27m-win32.whl", hash = "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474"},
-    {file = "cffi-1.15.0-cp27-cp27m-win_amd64.whl", hash = "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6"},
-    {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27"},
-    {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023"},
-    {file = "cffi-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2"},
-    {file = "cffi-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e"},
-    {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7"},
-    {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3"},
-    {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c"},
-    {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962"},
-    {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382"},
-    {file = "cffi-1.15.0-cp310-cp310-win32.whl", hash = "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55"},
-    {file = "cffi-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0"},
-    {file = "cffi-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e"},
-    {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39"},
-    {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc"},
-    {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032"},
-    {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8"},
-    {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605"},
-    {file = "cffi-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e"},
-    {file = "cffi-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc"},
-    {file = "cffi-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636"},
-    {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4"},
-    {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997"},
-    {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b"},
-    {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2"},
-    {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7"},
-    {file = "cffi-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66"},
-    {file = "cffi-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029"},
-    {file = "cffi-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880"},
-    {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20"},
-    {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024"},
-    {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e"},
-    {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728"},
-    {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6"},
-    {file = "cffi-1.15.0-cp38-cp38-win32.whl", hash = "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c"},
-    {file = "cffi-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443"},
-    {file = "cffi-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a"},
-    {file = "cffi-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37"},
-    {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a"},
-    {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e"},
-    {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796"},
-    {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df"},
-    {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8"},
-    {file = "cffi-1.15.0-cp39-cp39-win32.whl", hash = "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a"},
-    {file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"},
-    {file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"},
-]
+asgiref = []
+attrs = []
+black = []
+certifi = []
+cffi = []
 cfgv = [
     {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"},
     {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
 ]
-charset-normalizer = [
-    {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"},
-    {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"},
-]
-click = [
-    {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
-    {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
-]
+charset-normalizer = []
+click = []
 codecov = [
     {file = "codecov-2.1.12-py2.py3-none-any.whl", hash = "sha256:585dc217dc3d8185198ceb402f85d5cb5dbfa0c5f350a5abcdf9e347776a5b47"},
     {file = "codecov-2.1.12-py3.8.egg", hash = "sha256:782a8e5352f22593cbc5427a35320b99490eb24d9dcfa2155fd99d2b75cfb635"},
     {file = "codecov-2.1.12.tar.gz", hash = "sha256:a0da46bb5025426da895af90938def8ee12d37fcbcbbbc15b6dc64cf7ebc51c1"},
 ]
-colorama = [
-    {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
-    {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
-]
+colorama = []
 confu = [
     {file = "confu-1.8.0-py3-none-any.whl", hash = "sha256:109cbf7342e90abe84d6f15b3152a1433a953fb34d9242df6de1b205cd60685e"},
     {file = "confu-1.8.0.tar.gz", hash = "sha256:fc5c99c14135dd8d396c9ff89e40ddac63261aea4bc84784d7b54b9c3b45ba68"},
 ]
-coverage = [
-    {file = "coverage-6.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f1d5aa2703e1dab4ae6cf416eb0095304f49d004c39e9db1d86f57924f43006b"},
-    {file = "coverage-6.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ce1b258493cbf8aec43e9b50d89982346b98e9ffdfaae8ae5793bc112fb0068"},
-    {file = "coverage-6.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83c4e737f60c6936460c5be330d296dd5b48b3963f48634c53b3f7deb0f34ec4"},
-    {file = "coverage-6.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84e65ef149028516c6d64461b95a8dbcfce95cfd5b9eb634320596173332ea84"},
-    {file = "coverage-6.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f69718750eaae75efe506406c490d6fc5a6161d047206cc63ce25527e8a3adad"},
-    {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e57816f8ffe46b1df8f12e1b348f06d164fd5219beba7d9433ba79608ef011cc"},
-    {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:01c5615d13f3dd3aa8543afc069e5319cfa0c7d712f6e04b920431e5c564a749"},
-    {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:75ab269400706fab15981fd4bd5080c56bd5cc07c3bccb86aab5e1d5a88dc8f4"},
-    {file = "coverage-6.4.1-cp310-cp310-win32.whl", hash = "sha256:a7f3049243783df2e6cc6deafc49ea123522b59f464831476d3d1448e30d72df"},
-    {file = "coverage-6.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:ee2ddcac99b2d2aec413e36d7a429ae9ebcadf912946b13ffa88e7d4c9b712d6"},
-    {file = "coverage-6.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fb73e0011b8793c053bfa85e53129ba5f0250fdc0392c1591fd35d915ec75c46"},
-    {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106c16dfe494de3193ec55cac9640dd039b66e196e4641fa8ac396181578b982"},
-    {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87f4f3df85aa39da00fd3ec4b5abeb7407e82b68c7c5ad181308b0e2526da5d4"},
-    {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:961e2fb0680b4f5ad63234e0bf55dfb90d302740ae9c7ed0120677a94a1590cb"},
-    {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cec3a0f75c8f1031825e19cd86ee787e87cf03e4fd2865c79c057092e69e3a3b"},
-    {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:129cd05ba6f0d08a766d942a9ed4b29283aff7b2cccf5b7ce279d50796860bb3"},
-    {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bf5601c33213d3cb19d17a796f8a14a9eaa5e87629a53979a5981e3e3ae166f6"},
-    {file = "coverage-6.4.1-cp37-cp37m-win32.whl", hash = "sha256:269eaa2c20a13a5bf17558d4dc91a8d078c4fa1872f25303dddcbba3a813085e"},
-    {file = "coverage-6.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f02cbbf8119db68455b9d763f2f8737bb7db7e43720afa07d8eb1604e5c5ae28"},
-    {file = "coverage-6.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ffa9297c3a453fba4717d06df579af42ab9a28022444cae7fa605af4df612d54"},
-    {file = "coverage-6.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:145f296d00441ca703a659e8f3eb48ae39fb083baba2d7ce4482fb2723e050d9"},
-    {file = "coverage-6.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d44996140af8b84284e5e7d398e589574b376fb4de8ccd28d82ad8e3bea13"},
-    {file = "coverage-6.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2bd9a6fc18aab8d2e18f89b7ff91c0f34ff4d5e0ba0b33e989b3cd4194c81fd9"},
-    {file = "coverage-6.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3384f2a3652cef289e38100f2d037956194a837221edd520a7ee5b42d00cc605"},
-    {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9b3e07152b4563722be523e8cd0b209e0d1a373022cfbde395ebb6575bf6790d"},
-    {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1480ff858b4113db2718848d7b2d1b75bc79895a9c22e76a221b9d8d62496428"},
-    {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:865d69ae811a392f4d06bde506d531f6a28a00af36f5c8649684a9e5e4a85c83"},
-    {file = "coverage-6.4.1-cp38-cp38-win32.whl", hash = "sha256:664a47ce62fe4bef9e2d2c430306e1428ecea207ffd68649e3b942fa8ea83b0b"},
-    {file = "coverage-6.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:26dff09fb0d82693ba9e6231248641d60ba606150d02ed45110f9ec26404ed1c"},
-    {file = "coverage-6.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d9c80df769f5ec05ad21ea34be7458d1dc51ff1fb4b2219e77fe24edf462d6df"},
-    {file = "coverage-6.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:39ee53946bf009788108b4dd2894bf1349b4e0ca18c2016ffa7d26ce46b8f10d"},
-    {file = "coverage-6.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5b66caa62922531059bc5ac04f836860412f7f88d38a476eda0a6f11d4724f4"},
-    {file = "coverage-6.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd180ed867e289964404051a958f7cccabdeed423f91a899829264bb7974d3d3"},
-    {file = "coverage-6.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84631e81dd053e8a0d4967cedab6db94345f1c36107c71698f746cb2636c63e3"},
-    {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8c08da0bd238f2970230c2a0d28ff0e99961598cb2e810245d7fc5afcf1254e8"},
-    {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d42c549a8f41dc103a8004b9f0c433e2086add8a719da00e246e17cbe4056f72"},
-    {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:309ce4a522ed5fca432af4ebe0f32b21d6d7ccbb0f5fcc99290e71feba67c264"},
-    {file = "coverage-6.4.1-cp39-cp39-win32.whl", hash = "sha256:fdb6f7bd51c2d1714cea40718f6149ad9be6a2ee7d93b19e9f00934c0f2a74d9"},
-    {file = "coverage-6.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:342d4aefd1c3e7f620a13f4fe563154d808b69cccef415415aece4c786665397"},
-    {file = "coverage-6.4.1-pp36.pp37.pp38-none-any.whl", hash = "sha256:4803e7ccf93230accb928f3a68f00ffa80a88213af98ed338a57ad021ef06815"},
-    {file = "coverage-6.4.1.tar.gz", hash = "sha256:4321f075095a096e70aff1d002030ee612b65a205a0a0f5b815280d5dc58100c"},
-]
-cryptography = [
-    {file = "cryptography-37.0.2-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:ef15c2df7656763b4ff20a9bc4381d8352e6640cfeb95c2972c38ef508e75181"},
-    {file = "cryptography-37.0.2-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3c81599befb4d4f3d7648ed3217e00d21a9341a9a688ecdd615ff72ffbed7336"},
-    {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2bd1096476aaac820426239ab534b636c77d71af66c547b9ddcd76eb9c79e004"},
-    {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:31fe38d14d2e5f787e0aecef831457da6cec68e0bb09a35835b0b44ae8b988fe"},
-    {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:093cb351031656d3ee2f4fa1be579a8c69c754cf874206be1d4cf3b542042804"},
-    {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59b281eab51e1b6b6afa525af2bd93c16d49358404f814fe2c2410058623928c"},
-    {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:0cc20f655157d4cfc7bada909dc5cc228211b075ba8407c46467f63597c78178"},
-    {file = "cryptography-37.0.2-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f8ec91983e638a9bcd75b39f1396e5c0dc2330cbd9ce4accefe68717e6779e0a"},
-    {file = "cryptography-37.0.2-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:46f4c544f6557a2fefa7ac8ac7d1b17bf9b647bd20b16decc8fbcab7117fbc15"},
-    {file = "cryptography-37.0.2-cp36-abi3-win32.whl", hash = "sha256:731c8abd27693323b348518ed0e0705713a36d79fdbd969ad968fbef0979a7e0"},
-    {file = "cryptography-37.0.2-cp36-abi3-win_amd64.whl", hash = "sha256:471e0d70201c069f74c837983189949aa0d24bb2d751b57e26e3761f2f782b8d"},
-    {file = "cryptography-37.0.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a68254dd88021f24a68b613d8c51d5c5e74d735878b9e32cc0adf19d1f10aaf9"},
-    {file = "cryptography-37.0.2-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:a7d5137e556cc0ea418dca6186deabe9129cee318618eb1ffecbd35bee55ddc1"},
-    {file = "cryptography-37.0.2-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aeaba7b5e756ea52c8861c133c596afe93dd716cbcacae23b80bc238202dc023"},
-    {file = "cryptography-37.0.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95e590dd70642eb2079d280420a888190aa040ad20f19ec8c6e097e38aa29e06"},
-    {file = "cryptography-37.0.2-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:1b9362d34363f2c71b7853f6251219298124aa4cc2075ae2932e64c91a3e2717"},
-    {file = "cryptography-37.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e53258e69874a306fcecb88b7534d61820db8a98655662a3dd2ec7f1afd9132f"},
-    {file = "cryptography-37.0.2-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:1f3bfbd611db5cb58ca82f3deb35e83af34bb8cf06043fa61500157d50a70982"},
-    {file = "cryptography-37.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:419c57d7b63f5ec38b1199a9521d77d7d1754eb97827bbb773162073ccd8c8d4"},
-    {file = "cryptography-37.0.2-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:dc26bb134452081859aa21d4990474ddb7e863aa39e60d1592800a8865a702de"},
-    {file = "cryptography-37.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3b8398b3d0efc420e777c40c16764d6870bcef2eb383df9c6dbb9ffe12c64452"},
-    {file = "cryptography-37.0.2.tar.gz", hash = "sha256:f224ad253cc9cea7568f49077007d2263efa57396a2f2f78114066fd54b5c68e"},
-]
+coverage = []
+cryptography = []
 ctl = [
     {file = "ctl-1.0.0-py3-none-any.whl", hash = "sha256:776d490062c35dd413c929e7f7b4829142b4bf2cc03ddd9002899c0f7da1efaa"},
     {file = "ctl-1.0.0.tar.gz", hash = "sha256:5e39e9e9505db18e1cd1ecf1f64530e7e900f625e4542545d66b3ad954e003b6"},
@@ -1431,102 +1180,47 @@ defusedxml = [
     {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"},
     {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"},
 ]
-distlib = [
-    {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"},
-    {file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"},
-]
-django = [
-    {file = "Django-3.2.13-py3-none-any.whl", hash = "sha256:b896ca61edc079eb6bbaa15cf6071eb69d6aac08cce5211583cfb41515644fdf"},
-    {file = "Django-3.2.13.tar.gz", hash = "sha256:6d93497a0a9bf6ba0e0b1a29cccdc40efbfc76297255b1309b3a884a688ec4b6"},
-]
+distlib = []
+django = []
 django-autocomplete-light = [
     {file = "django-autocomplete-light-3.5.1.tar.gz", hash = "sha256:52e8d468060a5911c44300a374d74058c90e65bfdf87ace151abac67279c4ae5"},
 ]
-django-countries = [
-    {file = "django-countries-7.3.2.tar.gz", hash = "sha256:0df6d34193667c2343da8935cbfb8a2bd4fb0c97baf01ac10db4628ba1557a82"},
-    {file = "django_countries-7.3.2-py3-none-any.whl", hash = "sha256:27fc8a0f66a87c9d839493f3926b4e0f4dd873ef66465aa8cd3e953f99758cc9"},
-]
-django-crispy-forms = [
-    {file = "django-crispy-forms-1.14.0.tar.gz", hash = "sha256:35887b8851a931374dd697207a8f56c57a9c5cb9dbf0b9fa54314da5666cea5b"},
-    {file = "django_crispy_forms-1.14.0-py3-none-any.whl", hash = "sha256:bc4d2037f6de602d39c0bc452ac3029d1f5d65e88458872cc4dbc01c3a400604"},
-]
+django-countries = []
+django-crispy-forms = []
 django-grainy = [
     {file = "django-grainy-1.9.1.tar.gz", hash = "sha256:a2ed568a185fdce27bc4765c732fa67a10627d0e392f7a41d645ddfcd74763f0"},
 ]
-django-handleref = [
-    {file = "django-handleref-1.0.2.tar.gz", hash = "sha256:7e23d4697cece9b85d081aee4cff69c12273da0bb9f9c1dbcc5480064c825e31"},
-    {file = "django_handleref-1.0.2-py3-none-any.whl", hash = "sha256:c6128fcf7e1863d5135e319947fcc8f113bb7a20026341fa5f106cb25ce93055"},
-]
-django-inet = [
-    {file = "django-inet-1.1.1.tar.gz", hash = "sha256:3df96eb1f9ce936122391123434d69520d773661bfcad7835a2ba276fd4bec3f"},
-    {file = "django_inet-1.1.1-py3-none-any.whl", hash = "sha256:3ae9683a4a9ea9be635d2926904b54328230eac58a79a8f266d79f06dd4bed28"},
-]
-django-ipware = [
-    {file = "django-ipware-4.0.2.tar.gz", hash = "sha256:602a58325a4808bd19197fef2676a0b2da2df40d0ecf21be414b2ff48c72ad05"},
-    {file = "django_ipware-4.0.2-py2.py3-none-any.whl", hash = "sha256:878dbb06a87e25550798e9ef3204ed70a200dd8b15e47dcef848cf08244f04c9"},
-]
-django-netfields = [
-    {file = "django-netfields-1.3.0.tar.gz", hash = "sha256:39171bad19870269732dd5b7988594135f6b97b31e9e963f6740101756034d88"},
-]
-django-peeringdb = [
-    {file = "django-peeringdb-2.13.0.tar.gz", hash = "sha256:78c8aca930afb8a70847ad5fc3778b2eab4b1100e445fc1ded6f02cc96e13b28"},
-    {file = "django_peeringdb-2.13.0-py3-none-any.whl", hash = "sha256:816d5aff0bc3d73eaf859d98586ce275d0ae63717f3c5843f97898a4354bf813"},
-]
+django-handleref = []
+django-inet = []
+django-ipware = []
+django-netfields = []
+django-peeringdb = []
 django-recaptcha = [
     {file = "django_recaptcha-2.0.6-py2.py3-none-any.whl", hash = "sha256:567784963fd5400feaf92e8951d8dbbbdb4b4c48a76e225d4baa63a2c9d2cd8c"},
 ]
-django-reversion = [
-    {file = "django-reversion-5.0.0.tar.gz", hash = "sha256:0bade3c399387451087f0c67835e0d3d186d767de6a5c5ba53a88eafc3f271c8"},
-    {file = "django_reversion-5.0.0-py3-none-any.whl", hash = "sha256:7bea725de7f56769d89a5a712cf7b7f1b02abc27655432f2eb1a703264986d99"},
-]
+django-reversion = []
 django-structlog = [
     {file = "django-structlog-2.2.0.tar.gz", hash = "sha256:0443d39d27bf34258a375a2dfd1a0ae51f24bedce88beae55c1b69f3c09af922"},
     {file = "django_structlog-2.2.0-py3-none-any.whl", hash = "sha256:d6eb180963c7baf381ad7035e9afc7ecb7031246cb640f16925fe72f7712a2a1"},
 ]
-djangorestframework = [
-    {file = "djangorestframework-3.13.1-py3-none-any.whl", hash = "sha256:24c4bf58ed7e85d1fe4ba250ab2da926d263cd57d64b03e8dcef0ac683f8b1aa"},
-    {file = "djangorestframework-3.13.1.tar.gz", hash = "sha256:0c33407ce23acc68eca2a6e46424b008c9c02eceb8cf18581921d0092bc1f2ee"},
-]
-filelock = [
-    {file = "filelock-3.7.1-py3-none-any.whl", hash = "sha256:37def7b658813cda163b56fc564cdc75e86d338246458c4c28ae84cabefa2404"},
-    {file = "filelock-3.7.1.tar.gz", hash = "sha256:3a0fd85166ad9dbab54c9aec96737b744106dc5f15c0b09a6744a445299fcf04"},
-]
+djangorestframework = []
+filelock = []
 flake8 = [
     {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"},
     {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"},
 ]
-ghp-import = [
-    {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"},
-    {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"},
-]
+ghp-import = []
 git-url-parse = [
     {file = "git-url-parse-1.2.2.tar.gz", hash = "sha256:7b5f4e3aeb1d693afeee67a3bd4ac063f7206c2e8e46e559f0da0da98445f117"},
     {file = "git_url_parse-1.2.2-py2-none-any.whl", hash = "sha256:9353ff40d69488ff2299b27f40e0350ad87bd5348ea6ea09a1895eda9e5733de"},
     {file = "git_url_parse-1.2.2-py3-none-any.whl", hash = "sha256:4655ee22f1d8bf7a1eb1066c1da16529b186966c6d8331f7f55686a76a9f7aef"},
 ]
-gitdb = [
-    {file = "gitdb-4.0.9-py3-none-any.whl", hash = "sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd"},
-    {file = "gitdb-4.0.9.tar.gz", hash = "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa"},
-]
-gitpython = [
-    {file = "GitPython-3.1.27-py3-none-any.whl", hash = "sha256:5b68b000463593e05ff2b261acff0ff0972df8ab1b70d3cdbd41b546c8b8fc3d"},
-    {file = "GitPython-3.1.27.tar.gz", hash = "sha256:1c885ce809e8ba2d88a29befeb385fcea06338d3640712b59ca623c220bb5704"},
-]
 grainy = [
     {file = "grainy-1.8.1.tar.gz", hash = "sha256:2cfd8d50b3f5cce3c463f3c5e86324442f61a7cd46dfe7b134ee926559e56556"},
 ]
-identify = [
-    {file = "identify-2.5.1-py2.py3-none-any.whl", hash = "sha256:0dca2ea3e4381c435ef9c33ba100a78a9b40c0bab11189c7cf121f75815efeaa"},
-    {file = "identify-2.5.1.tar.gz", hash = "sha256:3d11b16f3fe19f52039fb7e39c9c884b21cb1b586988114fbe42671f03de3e82"},
-]
-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.11.4-py3-none-any.whl", hash = "sha256:c58c8eb8a762858f49e18436ff552e83914778e50e9d2f1660535ffb364552ec"},
-    {file = "importlib_metadata-4.11.4.tar.gz", hash = "sha256:5d26852efe48c0a32b0509ffbc583fda1a2266545a78d104a6f4aff3db17d700"},
-]
+identify = []
+idna = []
+importlib-metadata = []
 iniconfig = [
     {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
     {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
@@ -1535,17 +1229,9 @@ isort = [
     {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"},
     {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"},
 ]
-jinja2 = [
-    {file = "Jinja2-2.11.3-py2.py3-none-any.whl", hash = "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419"},
-    {file = "Jinja2-2.11.3.tar.gz", hash = "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"},
-]
-markdown = [
-    {file = "Markdown-3.3.7-py3-none-any.whl", hash = "sha256:f5da449a6e1c989a4cea2631aa8ee67caa5a2ef855d551c88f9e309f4634c621"},
-    {file = "Markdown-3.3.7.tar.gz", hash = "sha256:cbb516f16218e643d8e0a95b309f77eb118cb138d39a4f27851e6a63581db874"},
-]
-markdown-include = [
-    {file = "markdown-include-0.6.0.tar.gz", hash = "sha256:6f5d680e36f7780c7f0f61dca53ca581bd50d1b56137ddcd6353efafa0c3e4a2"},
-]
+jinja2 = []
+markdown = []
+markdown-include = []
 markupsafe = [
     {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"},
     {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"},
@@ -1596,10 +1282,7 @@ mergedeep = [
     {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"},
     {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"},
 ]
-mkdocs = [
-    {file = "mkdocs-1.3.0-py3-none-any.whl", hash = "sha256:26bd2b03d739ac57a3e6eed0b7bcc86168703b719c27b99ad6ca91dc439aacde"},
-    {file = "mkdocs-1.3.0.tar.gz", hash = "sha256:b504405b04da38795fec9b2e5e28f6aa3a73bb0960cb6d5d27ead28952bd35ea"},
-]
+mkdocs = []
 munge = [
     {file = "munge-1.2.1-py3-none-any.whl", hash = "sha256:65fb76389b54970beeef6a80b9e1b45cb230d34743cf11565af926ebc8af2627"},
     {file = "munge-1.2.1.tar.gz", hash = "sha256:3daeb20c2964aa4136c57ebfd422ee7e059eb92fbb0ca1e0c3e392ee9a923853"},
@@ -1612,34 +1295,16 @@ netaddr = [
     {file = "netaddr-0.8.0-py2.py3-none-any.whl", hash = "sha256:9666d0232c32d2656e5e5f8d735f58fd6c7457ce52fc21c98d45f2af78f990ac"},
     {file = "netaddr-0.8.0.tar.gz", hash = "sha256:d6cc57c7a07b1d9d2e917aa8b36ae8ce61c35ba3fcd1b83ca31c5a0ee2b5a243"},
 ]
-nodeenv = [
-    {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"},
-    {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"},
-]
-oauthlib = [
-    {file = "oauthlib-3.2.0-py3-none-any.whl", hash = "sha256:6db33440354787f9b7f3a6dbd4febf5d0f93758354060e802f6c06cb493022fe"},
-    {file = "oauthlib-3.2.0.tar.gz", hash = "sha256:23a8208d75b902797ea29fd31fa80a15ed9dc2c6c16fe73f5d346f83f6fa27a2"},
-]
+nodeenv = []
+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"},
-]
-peeringdb = [
-    {file = "peeringdb-1.3.0-py3-none-any.whl", hash = "sha256:7a4ffe968d2953e494009ab185497c657e9ea8caeae06f5e3a081620453a518b"},
-    {file = "peeringdb-1.3.0.tar.gz", hash = "sha256:46e2fc3ca66848ccee71bfc7c3bbb02a7d3e4a03c706c17ee30bf3a6476b086f"},
-]
-platformdirs = [
-    {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
-    {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
-]
+pathspec = []
+pbr = []
+peeringdb = []
+platformdirs = []
 pluggy = [
     {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
     {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
@@ -1648,68 +1313,8 @@ pluginmgr = [
     {file = "pluginmgr-1.1.0-py3-none-any.whl", hash = "sha256:1737de45f3055a3a16b544f1177dd4e2c018c2fcac913e1d2603769513d60ece"},
     {file = "pluginmgr-1.1.0.tar.gz", hash = "sha256:473eae4056a3d8f33036287d19ea09ca3f82ba50330f8d187cdd279839e8e2d7"},
 ]
-pre-commit = [
-    {file = "pre_commit-2.19.0-py2.py3-none-any.whl", hash = "sha256:10c62741aa5704faea2ad69cb550ca78082efe5697d6f04e5710c3c229afdd10"},
-    {file = "pre_commit-2.19.0.tar.gz", hash = "sha256:4233a1e38621c87d9dda9808c6606d7e7ba0e087cd56d3fe03202a01d2919615"},
-]
-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"},
-    {file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e82d38390a03da28c7985b394ec3f56873174e2c88130e6966cb1c946508e65"},
-    {file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57804fc02ca3ce0dbfbef35c4b3a4a774da66d66ea20f4bda601294ad2ea6092"},
-    {file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:083a55275f09a62b8ca4902dd11f4b33075b743cf0d360419e2051a8a5d5ff76"},
-    {file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_24_ppc64le.whl", hash = "sha256:0a29729145aaaf1ad8bafe663131890e2111f13416b60e460dae0a96af5905c9"},
-    {file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a79d622f5206d695d7824cbf609a4f5b88ea6d6dab5f7c147fc6d333a8787e4"},
-    {file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:090f3348c0ab2cceb6dfbe6bf721ef61262ddf518cd6cc6ecc7d334996d64efa"},
-    {file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:a9e1f75f96ea388fbcef36c70640c4efbe4650658f3d6a2967b4cc70e907352e"},
-    {file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c3ae8e75eb7160851e59adc77b3a19a976e50622e44fd4fd47b8b18208189d42"},
-    {file = "psycopg2_binary-2.9.3-cp310-cp310-win32.whl", hash = "sha256:7b1e9b80afca7b7a386ef087db614faebbf8839b7f4db5eb107d0f1a53225029"},
-    {file = "psycopg2_binary-2.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:8b344adbb9a862de0c635f4f0425b7958bf5a4b927c8594e6e8d261775796d53"},
-    {file = "psycopg2_binary-2.9.3-cp36-cp36m-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:e847774f8ffd5b398a75bc1c18fbb56564cda3d629fe68fd81971fece2d3c67e"},
-    {file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68641a34023d306be959101b345732360fc2ea4938982309b786f7be1b43a4a1"},
-    {file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3303f8807f342641851578ee7ed1f3efc9802d00a6f83c101d21c608cb864460"},
-    {file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_24_aarch64.whl", hash = "sha256:e3699852e22aa68c10de06524a3721ade969abf382da95884e6a10ff798f9281"},
-    {file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_24_ppc64le.whl", hash = "sha256:526ea0378246d9b080148f2d6681229f4b5964543c170dd10bf4faaab6e0d27f"},
-    {file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:b1c8068513f5b158cf7e29c43a77eb34b407db29aca749d3eb9293ee0d3103ca"},
-    {file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:15803fa813ea05bef089fa78835118b5434204f3a17cb9f1e5dbfd0b9deea5af"},
-    {file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:152f09f57417b831418304c7f30d727dc83a12761627bb826951692cc6491e57"},
-    {file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:404224e5fef3b193f892abdbf8961ce20e0b6642886cfe1fe1923f41aaa75c9d"},
-    {file = "psycopg2_binary-2.9.3-cp36-cp36m-win32.whl", hash = "sha256:1f6b813106a3abdf7b03640d36e24669234120c72e91d5cbaeb87c5f7c36c65b"},
-    {file = "psycopg2_binary-2.9.3-cp36-cp36m-win_amd64.whl", hash = "sha256:2d872e3c9d5d075a2e104540965a1cf898b52274a5923936e5bfddb58c59c7c2"},
-    {file = "psycopg2_binary-2.9.3-cp37-cp37m-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:10bb90fb4d523a2aa67773d4ff2b833ec00857f5912bafcfd5f5414e45280fb1"},
-    {file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:874a52ecab70af13e899f7847b3e074eeb16ebac5615665db33bce8a1009cf33"},
-    {file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a29b3ca4ec9defec6d42bf5feb36bb5817ba3c0230dd83b4edf4bf02684cd0ae"},
-    {file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:12b11322ea00ad8db8c46f18b7dfc47ae215e4df55b46c67a94b4effbaec7094"},
-    {file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_24_ppc64le.whl", hash = "sha256:53293533fcbb94c202b7c800a12c873cfe24599656b341f56e71dd2b557be063"},
-    {file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c381bda330ddf2fccbafab789d83ebc6c53db126e4383e73794c74eedce855ef"},
-    {file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9d29409b625a143649d03d0fd7b57e4b92e0ecad9726ba682244b73be91d2fdb"},
-    {file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:183a517a3a63503f70f808b58bfbf962f23d73b6dccddae5aa56152ef2bcb232"},
-    {file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:15c4e4cfa45f5a60599d9cec5f46cd7b1b29d86a6390ec23e8eebaae84e64554"},
-    {file = "psycopg2_binary-2.9.3-cp37-cp37m-win32.whl", hash = "sha256:adf20d9a67e0b6393eac162eb81fb10bc9130a80540f4df7e7355c2dd4af9fba"},
-    {file = "psycopg2_binary-2.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:2f9ffd643bc7349eeb664eba8864d9e01f057880f510e4681ba40a6532f93c71"},
-    {file = "psycopg2_binary-2.9.3-cp38-cp38-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:def68d7c21984b0f8218e8a15d514f714d96904265164f75f8d3a70f9c295667"},
-    {file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dffc08ca91c9ac09008870c9eb77b00a46b3378719584059c034b8945e26b272"},
-    {file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:280b0bb5cbfe8039205c7981cceb006156a675362a00fe29b16fbc264e242834"},
-    {file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:af9813db73395fb1fc211bac696faea4ca9ef53f32dc0cfa27e4e7cf766dcf24"},
-    {file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_24_ppc64le.whl", hash = "sha256:63638d875be8c2784cfc952c9ac34e2b50e43f9f0a0660b65e2a87d656b3116c"},
-    {file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ffb7a888a047696e7f8240d649b43fb3644f14f0ee229077e7f6b9f9081635bd"},
-    {file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0c9d5450c566c80c396b7402895c4369a410cab5a82707b11aee1e624da7d004"},
-    {file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:d1c1b569ecafe3a69380a94e6ae09a4789bbb23666f3d3a08d06bbd2451f5ef1"},
-    {file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8fc53f9af09426a61db9ba357865c77f26076d48669f2e1bb24d85a22fb52307"},
-    {file = "psycopg2_binary-2.9.3-cp38-cp38-win32.whl", hash = "sha256:6472a178e291b59e7f16ab49ec8b4f3bdada0a879c68d3817ff0963e722a82ce"},
-    {file = "psycopg2_binary-2.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:35168209c9d51b145e459e05c31a9eaeffa9a6b0fd61689b48e07464ffd1a83e"},
-    {file = "psycopg2_binary-2.9.3-cp39-cp39-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:47133f3f872faf28c1e87d4357220e809dfd3fa7c64295a4a148bcd1e6e34ec9"},
-    {file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91920527dea30175cc02a1099f331aa8c1ba39bf8b7762b7b56cbf54bc5cce42"},
-    {file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:887dd9aac71765ac0d0bac1d0d4b4f2c99d5f5c1382d8b770404f0f3d0ce8a39"},
-    {file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:1f14c8b0942714eb3c74e1e71700cbbcb415acbc311c730370e70c578a44a25c"},
-    {file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_24_ppc64le.whl", hash = "sha256:7af0dd86ddb2f8af5da57a976d27cd2cd15510518d582b478fbb2292428710b4"},
-    {file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:93cd1967a18aa0edd4b95b1dfd554cf15af657cb606280996d393dadc88c3c35"},
-    {file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bda845b664bb6c91446ca9609fc69f7db6c334ec5e4adc87571c34e4f47b7ddb"},
-    {file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:01310cf4cf26db9aea5158c217caa92d291f0500051a6469ac52166e1a16f5b7"},
-    {file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:99485cab9ba0fa9b84f1f9e1fef106f44a46ef6afdeec8885e0b88d0772b49e8"},
-    {file = "psycopg2_binary-2.9.3-cp39-cp39-win32.whl", hash = "sha256:46f0e0a6b5fa5851bbd9ab1bc805eef362d3a230fbdfbc209f4a236d0a7a990d"},
-    {file = "psycopg2_binary-2.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:accfe7e982411da3178ec690baaceaad3c278652998b2c45828aaac66cd8285f"},
-]
+pre-commit = []
+psycopg2-binary = []
 py = [
     {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
     {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
@@ -1726,30 +1331,11 @@ 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"},
-]
-pyparsing = [
-    {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
-    {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
-]
-pytest = [
-    {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"},
-    {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"},
-]
-pytest-cov = [
-    {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"},
-    {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"},
-]
-pytest-django = [
-    {file = "pytest-django-4.5.2.tar.gz", hash = "sha256:d9076f759bb7c36939dbdd5ae6633c18edfc2902d1a69fdbefd2426b970ce6c2"},
-    {file = "pytest_django-4.5.2-py3-none-any.whl", hash = "sha256:c60834861933773109334fe5a53e83d1ef4828f2203a1d6a0fa9972f4f75ab3e"},
-]
-pytest-pythonpath = [
-    {file = "pytest-pythonpath-0.7.4.tar.gz", hash = "sha256:64e195b23a8f8c0c631fb16882d9ad6fa4137ed1f2961ddd15d52065cd435db6"},
-    {file = "pytest_pythonpath-0.7.4-py3-none-any.whl", hash = "sha256:e73e11dab2f0b83e73229e261242b251f0a369d7f527dbfec068822fd26a6ce5"},
-]
+pyjwt = []
+pyparsing = []
+pytest = []
+pytest-cov = []
+pytest-django = []
 python-dateutil = [
     {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
     {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
@@ -1758,14 +1344,9 @@ 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"},
-]
-pyupgrade = [
-    {file = "pyupgrade-2.34.0-py2.py3-none-any.whl", hash = "sha256:16617805b8d1138f96a1ff9cdb4b302b39b51804b3fc5474dc36c0326387445a"},
-    {file = "pyupgrade-2.34.0.tar.gz", hash = "sha256:ed0d9f282fa6282d0ca3bc0596a022aeb95435edab9daf3fc04c2ba118687b68"},
-]
+pytz = []
+pyupgrade = []
+pyuwsgi = []
 pyyaml = [
     {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"},
     {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"},
@@ -1773,18 +1354,26 @@ pyyaml = [
     {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"},
     {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"},
     {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"},
+    {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347"},
+    {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541"},
     {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"},
     {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"},
     {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"},
     {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"},
+    {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa"},
+    {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"},
     {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"},
     {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"},
     {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"},
     {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"},
+    {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247"},
+    {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc"},
     {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"},
     {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"},
     {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"},
     {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"},
+    {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122"},
+    {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6"},
     {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"},
     {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"},
     {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"},
@@ -1793,55 +1382,25 @@ pyyaml-env-tag = [
     {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"},
     {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"},
 ]
-requests = [
-    {file = "requests-2.28.0-py3-none-any.whl", hash = "sha256:bc7861137fbce630f17b03d3ad02ad0bf978c844f3536d0edda6499dafce2b6f"},
-    {file = "requests-2.28.0.tar.gz", hash = "sha256:d568723a7ebd25875d8d1eaf5dfa068cd2fc8194b2e483d7b1f7c81918dbec6b"},
-]
-requests-oauthlib = [
-    {file = "requests-oauthlib-1.3.1.tar.gz", hash = "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a"},
-    {file = "requests_oauthlib-1.3.1-py2.py3-none-any.whl", hash = "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5"},
-]
+requests = []
+requests-oauthlib = []
 six = [
     {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
     {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
 ]
-smmap = [
-    {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"},
-    {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"},
-]
 social-auth-app-django = [
     {file = "social-auth-app-django-3.4.0.tar.gz", hash = "sha256:09575f5c7dd91465df3a898c58e7c4ae1e78f31edba36b8b7be47ab0aeef2789"},
     {file = "social_auth_app_django-3.4.0-py2-none-any.whl", hash = "sha256:02b561e175d4a93896e4436b591586b61e647bd8eeef14c99a26344eb3b48d0e"},
     {file = "social_auth_app_django-3.4.0-py3-none-any.whl", hash = "sha256:47d1720115a9eaad78a67e99987d556abaa01222b9c2b9538182bbdbb10304ba"},
 ]
-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"},
-]
-stevedore = [
-    {file = "stevedore-3.5.0-py3-none-any.whl", hash = "sha256:a547de73308fd7e90075bb4d301405bebf705292fa90a90fc3bcf9133f58616c"},
-    {file = "stevedore-3.5.0.tar.gz", hash = "sha256:f40253887d8712eaa2bb0ea3830374416736dc8ec0e22f5a65092c1174c44335"},
-]
-stripe = [
-    {file = "stripe-2.76.0-py2.py3-none-any.whl", hash = "sha256:756bf6c1206f438d1fa23bb90cdf1233c9383478f854f2720a8a3e1eaf1f715b"},
-    {file = "stripe-2.76.0.tar.gz", hash = "sha256:fd3fc6935c3b6189967191607b6f38ebe490005a590b4d0d43fbe3aba45deca8"},
-]
-structlog = [
-    {file = "structlog-21.5.0-py3-none-any.whl", hash = "sha256:fd7922e195262b337da85c2a91c84be94ccab1f8fd1957bd6986f6904e3761c8"},
-    {file = "structlog-21.5.0.tar.gz", hash = "sha256:68c4c29c003714fe86834f347cb107452847ba52414390a7ee583472bde00fc9"},
-]
+social-auth-core = []
+sqlparse = []
+structlog = []
 tmpl = [
     {file = "tmpl-1.0.0-py3-none-any.whl", hash = "sha256:9932ac5fb2d3177948858f425b794904a5b86c786e8afa8e4a4471976fd283f7"},
     {file = "tmpl-1.0.0.tar.gz", hash = "sha256:71673a2c084de675ce010736f2fd9c83b844e93eaeece6e821a7a868a181e855"},
 ]
-tokenize-rt = [
-    {file = "tokenize_rt-4.2.1-py2.py3-none-any.whl", hash = "sha256:08a27fa032a81cf45e8858d0ac706004fcd523e8463415ddf1442be38e204ea8"},
-    {file = "tokenize_rt-4.2.1.tar.gz", hash = "sha256:0d4f69026fed520f8a1e0103aa36c406ef4661417f20ca643f913e33531b3b94"},
-]
+tokenize-rt = []
 toml = [
     {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
     {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
@@ -1858,79 +1417,11 @@ tomlkit = [
     {file = "twentyc.rpc-1.0.0-py3-none-any.whl", hash = "sha256:508ef696ada37f873a64bb814508d61e72680519dc90ef856f2b4deb7941a691"},
     {file = "twentyc.rpc-1.0.0.tar.gz", hash = "sha256:b11d02ebada57ddaaecabd617ba686acf4bdfccb01efb2f02fa9d7ae73614eb1"},
 ]
-typed-ast = [
-    {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"},
-    {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"},
-    {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"},
-    {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"},
-    {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"},
-    {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"},
-    {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"},
-    {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"},
-    {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"},
-    {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"},
-    {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"},
-    {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"},
-    {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"},
-    {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"},
-    {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"},
-    {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"},
-    {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"},
-    {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"},
-    {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"},
-    {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"},
-    {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"},
-    {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"},
-    {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"},
-]
-typing-extensions = [
-    {file = "typing_extensions-4.2.0-py3-none-any.whl", hash = "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"},
-    {file = "typing_extensions-4.2.0.tar.gz", hash = "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"},
-]
-urllib3 = [
-    {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"},
-    {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"},
-]
-uwsgi = [
-    {file = "uwsgi-2.0.20.tar.gz", hash = "sha256:88ab9867d8973d8ae84719cf233b7dafc54326fcaec89683c3f9f77c002cdff9"},
-]
-virtualenv = [
-    {file = "virtualenv-20.14.1-py2.py3-none-any.whl", hash = "sha256:e617f16e25b42eb4f6e74096b9c9e37713cf10bf30168fb4a739f3fa8f898a3a"},
-    {file = "virtualenv-20.14.1.tar.gz", hash = "sha256:ef589a79795589aada0c1c5b319486797c03b67ac3984c48c669c0e4f50df3a5"},
-]
-watchdog = [
-    {file = "watchdog-2.1.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a735a990a1095f75ca4f36ea2ef2752c99e6ee997c46b0de507ba40a09bf7330"},
-    {file = "watchdog-2.1.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b17d302850c8d412784d9246cfe8d7e3af6bcd45f958abb2d08a6f8bedf695d"},
-    {file = "watchdog-2.1.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ee3e38a6cc050a8830089f79cbec8a3878ec2fe5160cdb2dc8ccb6def8552658"},
-    {file = "watchdog-2.1.9-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64a27aed691408a6abd83394b38503e8176f69031ca25d64131d8d640a307591"},
-    {file = "watchdog-2.1.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:195fc70c6e41237362ba720e9aaf394f8178bfc7fa68207f112d108edef1af33"},
-    {file = "watchdog-2.1.9-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:bfc4d351e6348d6ec51df007432e6fe80adb53fd41183716017026af03427846"},
-    {file = "watchdog-2.1.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8250546a98388cbc00c3ee3cc5cf96799b5a595270dfcfa855491a64b86ef8c3"},
-    {file = "watchdog-2.1.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:117ffc6ec261639a0209a3252546b12800670d4bf5f84fbd355957a0595fe654"},
-    {file = "watchdog-2.1.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:97f9752208f5154e9e7b76acc8c4f5a58801b338de2af14e7e181ee3b28a5d39"},
-    {file = "watchdog-2.1.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:247dcf1df956daa24828bfea5a138d0e7a7c98b1a47cf1fa5b0c3c16241fcbb7"},
-    {file = "watchdog-2.1.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:226b3c6c468ce72051a4c15a4cc2ef317c32590d82ba0b330403cafd98a62cfd"},
-    {file = "watchdog-2.1.9-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d9820fe47c20c13e3c9dd544d3706a2a26c02b2b43c993b62fcd8011bcc0adb3"},
-    {file = "watchdog-2.1.9-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:70af927aa1613ded6a68089a9262a009fbdf819f46d09c1a908d4b36e1ba2b2d"},
-    {file = "watchdog-2.1.9-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ed80a1628cee19f5cfc6bb74e173f1b4189eb532e705e2a13e3250312a62e0c9"},
-    {file = "watchdog-2.1.9-py3-none-manylinux2014_aarch64.whl", hash = "sha256:9f05a5f7c12452f6a27203f76779ae3f46fa30f1dd833037ea8cbc2887c60213"},
-    {file = "watchdog-2.1.9-py3-none-manylinux2014_armv7l.whl", hash = "sha256:255bb5758f7e89b1a13c05a5bceccec2219f8995a3a4c4d6968fe1de6a3b2892"},
-    {file = "watchdog-2.1.9-py3-none-manylinux2014_i686.whl", hash = "sha256:d3dda00aca282b26194bdd0adec21e4c21e916956d972369359ba63ade616153"},
-    {file = "watchdog-2.1.9-py3-none-manylinux2014_ppc64.whl", hash = "sha256:186f6c55abc5e03872ae14c2f294a153ec7292f807af99f57611acc8caa75306"},
-    {file = "watchdog-2.1.9-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:083171652584e1b8829581f965b9b7723ca5f9a2cd7e20271edf264cfd7c1412"},
-    {file = "watchdog-2.1.9-py3-none-manylinux2014_s390x.whl", hash = "sha256:b530ae007a5f5d50b7fbba96634c7ee21abec70dc3e7f0233339c81943848dc1"},
-    {file = "watchdog-2.1.9-py3-none-manylinux2014_x86_64.whl", hash = "sha256:4f4e1c4aa54fb86316a62a87b3378c025e228178d55481d30d857c6c438897d6"},
-    {file = "watchdog-2.1.9-py3-none-win32.whl", hash = "sha256:5952135968519e2447a01875a6f5fc8c03190b24d14ee52b0f4b1682259520b1"},
-    {file = "watchdog-2.1.9-py3-none-win_amd64.whl", hash = "sha256:7a833211f49143c3d336729b0020ffd1274078e94b0ae42e22f596999f50279c"},
-    {file = "watchdog-2.1.9-py3-none-win_ia64.whl", hash = "sha256:ad576a565260d8f99d97f2e64b0f97a48228317095908568a9d5c786c829d428"},
-    {file = "watchdog-2.1.9.tar.gz", hash = "sha256:43ce20ebb36a51f21fa376f76d1d4692452b2527ccd601950d69ed36b9e21609"},
-]
-whitenoise = [
-    {file = "whitenoise-5.3.0-py2.py3-none-any.whl", hash = "sha256:d963ef25639d1417e8a247be36e6aedd8c7c6f0a08adcb5a89146980a96b577c"},
-    {file = "whitenoise-5.3.0.tar.gz", hash = "sha256:d234b871b52271ae7ed6d9da47ffe857c76568f11dd30e28e18c5869dbd11e12"},
-]
-zipp = [
-    {file = "zipp-3.8.0-py3-none-any.whl", hash = "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"},
-    {file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"},
-]
+typed-ast = []
+typing-extensions = []
+uritemplate = []
+urllib3 = []
+virtualenv = []
+watchdog = []
+whitenoise = []
+zipp = []
diff --git a/pyproject.toml b/pyproject.toml
index 30c7416e..02fc6a35 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,33 +1,25 @@
-[build-system]
-requires = [ "poetry>=0.12",]
-build-backend = "poetry.masonry.api"
-
 [tool.poetry]
 name = "fullctl"
 repository = "https://github.com/fullctl/fullctl"
-version = "0.3.0.1"
+version = "1.0.0-rc10"
 description = "Core classes and functions for service applications"
-authors = [ "20C <code@20c.com>",]
+authors = ["20C <code@20c.com>"]
 license = "Apache-2.0"
-classifiers = [ 
-  "Development Status :: 4 - Beta",
-  "Environment :: Console",
+classifiers = [
+  "Development Status :: 5 - Production/Stable",
   "Intended Audience :: Developers",
   "Topic :: Software Development",
   "License :: OSI Approved :: Apache Software License",
   "Programming Language :: Python :: 3.7",
   "Programming Language :: Python :: 3.8",
-  "Programming Language :: Python :: 3.9"
+  "Programming Language :: Python :: 3.9",
+  "Programming Language :: Python :: 3.10",
 ]
 
 #packages = [
 #    { include = "fullctl", from = "src" },
 #]
 
-[tool.isort]
-profile = "black"
-multi_line_output = 3
-
 [tool.poetry.dependencies]
 python = "^3.7"
 
@@ -49,53 +41,52 @@ social-auth-app-django = "^3"
 django-netfields = "<2"
 
 # prod serving
-whitenoise = "^5.2.0"
-uWSGI = "^2.0.19"
+whitenoise = "^6"
+pyuwsgi = "^2.0.19"
 
 confu = "^1.5"
 grainy = "^1.8.1"
 psycopg2-binary = "^2.8"
+# psycopg = {extras = ["binary"], version = "^3.0.14"}
 pyyaml = "^5.4"
 
-# move to aaactl?
-stripe = "^2.55.2"
-
 # move to pdbctl
 django-peeringdb = "^2.8"
 peeringdb = "^1"
 
-# XXX still used?
-# pip = "*"
-# wheel = "*"
-# celery = "<6,>=5"
-
-
 [tool.poetry.dev-dependencies]
 # tests
 codecov = ">=2"
 coverage = ">=5"
-pytest = ">=6.0.1"
-pytest-django = ">=3.8.0"
+pytest = ">=6"
+pytest-django = ">=3.8"
 pytest-cov = "*"
-pytest-pythonpath = "*"
 
 # linting
-bandit = "^1.6.2"
-black = {version = ">=20", allow-prereleases = true}
-isort = "^5.7.0"
-flake8 = "^3.8.4"
-pre-commit = "^2.13.0"
-pyupgrade = "^2.19.4"
+black = ">=20"
+isort = ">=5.7"
+flake8 = ">=3.8"
+pre-commit = ">=2.13"
+pyupgrade = ">=2.19"
 
 # docs
 markdown = "*"
 markdown-include = ">=0.5,<1"
-mkdocs = ">=1.0.0,<2.0.0"
+mkdocs = "^1.2.3"
+uritemplate = "*"
 
 # ctl
-ctl = "^1.0.0"
-jinja2 = "^2.11.2"
+ctl = "^1"
+jinja2 = "^3"
 tmpl = "^1"
 
+[build-system]
+requires = ["poetry>=0.12"]
+build-backend = "poetry.masonry.api"
+
 [tool.poetry.plugins."markdown.extensions"]
 pymdgen = "pymdgen.md:Extension"
+
+[tool.isort]
+profile = "black"
+multi_line_output = 3
diff --git a/src/fullctl/django/admin.py b/src/fullctl/django/admin.py
index 4fea3646..b6b12029 100644
--- a/src/fullctl/django/admin.py
+++ b/src/fullctl/django/admin.py
@@ -1,5 +1,8 @@
 import reversion
+from django.conf.urls import url
 from django.contrib import admin
+from django.http import HttpResponseForbidden
+from django.shortcuts import redirect
 from django_handleref.admin import VersionAdmin
 
 import fullctl.django.auditlog as auditlog
@@ -10,6 +13,7 @@
     Task,
     TaskSchedule,
 )
+from fullctl.django.models.concrete.service_bridge import ServiceBridgeAction
 
 
 class BaseAdmin(VersionAdmin):
@@ -116,3 +120,58 @@ def log_object(self, obj=None):
         elif obj and obj.object_id:
             return "<deleted>"
         return ""
+
+
+class UrlActionMixin:
+    def make_redirect(self, obj, action):
+        opts = obj.model._meta
+        return redirect(f"admin:{opts.app_label}_{opts.model_name}_changelist")
+
+    def actions_view(self, request, object_id, action, **kwargs):
+        """
+        Allows one to call any actions defined in this model admin
+        to be called via an admin view placed at <model_name>/<id>/<action>/<action_name>.
+        """
+        if not request.user.is_superuser:
+            return HttpResponseForbidden(request)
+
+        obj = self.get_queryset(request).filter(pk=object_id)
+        if obj.exists():
+            redir = self.make_redirect(obj, action)
+            action = self.get_action(action)
+            if action:
+                action[0](self, request, obj)
+                return redir
+        return redirect(
+            "admin:%s_%s_changelist"
+            % (obj.model._meta.app_label, obj.model._meta.model_name)
+        )
+
+    def get_urls(self):
+        """
+        Adds the actions view as a subview of this model's admin views.
+        """
+        info = self.model._meta.app_label, self.model._meta.model_name
+
+        urls = [
+            url(
+                r"^(\d+)/action/([\w]+)/$",
+                self.admin_site.admin_view(self.actions_view),
+                name="%s_%s_actions" % info,
+            ),
+        ] + super().get_urls()
+        return urls
+
+
+@admin.register(ServiceBridgeAction)
+class ServiceBridgeAction(admin.ModelAdmin):
+    list_display = (
+        "name",
+        "reference",
+        "target",
+        "action",
+        "function",
+        "description",
+        "created",
+        "updated",
+    )
diff --git a/src/fullctl/django/auth.py b/src/fullctl/django/auth.py
index 284f3e87..ea7c35ce 100644
--- a/src/fullctl/django/auth.py
+++ b/src/fullctl/django/auth.py
@@ -2,8 +2,16 @@
 
 import django_grainy.remote
 import django_grainy.util
+import requests
 from django.conf import settings
 from django.contrib.auth import get_user_model
+from django.core.cache import cache
+from django.db import transaction
+from social_django.models import UserSocialAuth
+
+import fullctl.django.models.concrete.account as account_models
+import fullctl.service_bridge.aaactl as aaactl
+from fullctl.django.context import current_request
 
 
 class RemotePermissionsError(IOError):
@@ -23,6 +31,41 @@ class Permissions(django_grainy.util.Permissions):
     pass
 
 
+def require_user(aaactl_user_id):
+
+    """
+    Fetches and syncs a user from aaactl using the service bridge
+    """
+
+    user = get_user_model().objects.filter(social_auth__uid=aaactl_user_id).first()
+
+    if not user:
+
+        aaactl_user = aaactl.User().object(aaactl_user_id)
+
+        user = get_user_model().objects.create_user(
+            username=aaactl_user.username,
+            first_name=aaactl_user.first_name,
+            last_name=aaactl_user.last_name,
+            is_staff=aaactl_user.is_staff,
+            is_superuser=aaactl_user.is_superuser,
+            email=aaactl_user.email,
+        )
+
+        account_models.Organization.sync(
+            aaactl_user.organizations, user, backend="twentyc"
+        )
+
+        UserSocialAuth.objects.create(
+            user=user,
+            provider="twentyc",
+            extra_data={},
+            uid=aaactl_user_id,
+        )
+
+    return user
+
+
 class RemotePermissions(django_grainy.remote.Permissions):
 
     """
@@ -34,9 +77,45 @@ class RemotePermissions(django_grainy.remote.Permissions):
     def __init__(self, obj):
         super().__init__(obj, **settings.GRAINY_REMOTE)
 
-    def fetch(self, *args, **kwargs):
+    @transaction.atomic
+    def handle_impersonation(self, response):
+
+        user_id = response.headers.get("X-User")
+
+        if not user_id:
+            return
+
+        with current_request() as request:
+
+            if not request.user.is_superuser:
+                return
+
+            user = require_user(user_id)
+
+            request.impersonating = {"superuser": request.user, "user": user}
+            request.user = user
+
+    def fetch(self, url, cache_key, **params):
         try:
-            return super().fetch(*args, **kwargs)
+
+            if self.cache > 0:
+                cached = cache.get(cache_key)
+                if cached:
+                    return json.loads(cached)
+
+            headers = {}
+            self.prepare_request(params, headers)
+            response = requests.get(url, params=params, headers=headers)
+
+            self.handle_impersonation(response)
+
+            data = response.json()
+
+            if self.cache > 0:
+                cache.set(cache_key, json.dumps(data), self.cache)
+
+            return data
+
         except json.decoder.JSONDecodeError:
             raise RemotePermissionsError()
 
diff --git a/src/fullctl/django/context.py b/src/fullctl/django/context.py
index 329e35b9..a74176c6 100644
--- a/src/fullctl/django/context.py
+++ b/src/fullctl/django/context.py
@@ -5,6 +5,8 @@
 # manner.
 _current_request = contextvars.ContextVar("current_request")
 
+_service_bridge_sync = contextvars.ContextVar("service_bridge_sync")
+
 
 @contextmanager
 def current_request(request=None):
@@ -27,3 +29,57 @@ def current_request(request=None):
     finally:
         if token:
             _current_request.reset(token)
+
+
+class ServiceBridgeSync:
+
+    """
+    Context object for service bridge sync
+
+    Can perform push and pull actions on objects according
+    to context configuration
+    """
+
+    def __init__(self, push=False, pull=False, **kwargs):
+        self.enable_push = push
+        self.enable_pull = pull
+        self.save = kwargs.get("save", True)
+        self.cache = kwargs.get("cache")
+
+    def push(self, obj):
+        if not self.enable_push:
+            return
+        obj.push()
+
+    def pull(self, obj):
+        if not self.enable_pull:
+            return
+
+        if self.enable_pull == "sot" and not obj.reference_is_sot:
+            return
+
+        changed = obj.sync_from_reference(if_sot=(self.enable_pull == "sot"))
+        obj.pull()
+
+        if changed and self.save:
+            obj.save()
+
+
+@contextmanager
+def service_bridge_sync(push=False, pull=False, cache=None, save=True):
+    """
+    opens a service bridge sync context in which ServiceBridgeModel
+    instances will automatically pull from and push to references during
+    load and save operations
+    """
+    token = None
+
+    try:
+        yield _service_bridge_sync.get()
+    except LookupError:
+        mgr = ServiceBridgeSync(push=push, pull=pull, cache=cache, save=save)
+        token = _service_bridge_sync.set(mgr)
+        yield mgr
+    finally:
+        if token:
+            _service_bridge_sync.reset(token)
diff --git a/src/fullctl/django/context_processors.py b/src/fullctl/django/context_processors.py
index ecf421c8..6a612300 100644
--- a/src/fullctl/django/context_processors.py
+++ b/src/fullctl/django/context_processors.py
@@ -28,9 +28,10 @@ def account_service(request):
     context.update(
         account_service={
             "urls": {
-                "billing_setup": f"{settings.OAUTH_TWENTYC_HOST}/billing/setup?org={org_slug}",
-                "create_org": f"{settings.OAUTH_TWENTYC_HOST}/account/org/create/",
-                "manage_org": f"{settings.OAUTH_TWENTYC_HOST}/account/?org={org_slug}",
+                "billing_setup": f"{settings.OAUTH_TWENTYC_URL}/billing/setup?org={org_slug}",
+                # TODO: flesh out to redirect to org/create
+                "create_org": f"{settings.OAUTH_TWENTYC_URL}/account/",
+                "manage_org": f"{settings.OAUTH_TWENTYC_URL}/account/?org={org_slug}",
             },
         },
         # TODO: deprecated
@@ -41,11 +42,11 @@ def account_service(request):
         service_name=settings.SERVICE_TAG.replace("ctl", ""),
     )
 
-    if settings.OAUTH_TWENTYC_HOST:
+    if settings.OAUTH_TWENTYC_URL:
         context.update(
             service_applications=[
-                svcapp.for_org(org)
-                for svcapp in ServiceApplication().objects(group="fullctl")
+                service_application.for_org(org)
+                for service_application in ServiceApplication().objects(group="fullctl")
             ],
         )
 
diff --git a/src/fullctl/django/decorators.py b/src/fullctl/django/decorators.py
index f99f23c1..b15a8ae5 100644
--- a/src/fullctl/django/decorators.py
+++ b/src/fullctl/django/decorators.py
@@ -2,6 +2,7 @@
 from django.shortcuts import redirect
 from django.urls import reverse
 
+import fullctl.django.context as context
 from fullctl.django.models import Instance, Organization
 
 
@@ -75,3 +76,18 @@ def wrapped(request, *args, **kwargs):
         wrapped.public = public
 
         return wrapped
+
+
+class service_bridge_sync:
+    def __init__(self, **kwargs):
+        self.ctx_args = kwargs
+
+    def __call__(self, fn):
+        ctx_args = self.ctx_args
+
+        def wrapped(*args, **kwargs):
+            with context.service_bridge_sync(**ctx_args):
+                return fn(*args, **kwargs)
+
+        wrapped.__name__ = fn.__name__
+        return wrapped
diff --git a/src/fullctl/django/exceptions.py b/src/fullctl/django/exceptions.py
new file mode 100644
index 00000000..1f7a71c6
--- /dev/null
+++ b/src/fullctl/django/exceptions.py
@@ -0,0 +1,10 @@
+class FullctlException(Exception):
+    pass
+
+
+class TemplateRenderError(FullctlException, ValueError):
+    """Error that gets raised when template render fails"""
+
+    def __init__(self, tmpl_exc):
+        msg = f"Could not render template: {tmpl_exc}"
+        super().__init__(msg)
diff --git a/src/fullctl/django/fields/__init__.py b/src/fullctl/django/fields/__init__.py
new file mode 100644
index 00000000..126af9dc
--- /dev/null
+++ b/src/fullctl/django/fields/__init__.py
@@ -0,0 +1,17 @@
+class CastOnAssignDescriptor:
+    """
+    A property descriptor which ensures that `field.to_python()` is called on _every_ assignment to the field.
+    This used to be provided by the `django.db.models.subclassing.Creator` class, which in turn
+    was used by the deprecated-in-Django-1.10 `SubfieldBase` class, hence the reimplementation here.
+    """
+
+    def __init__(self, field):
+        self.field = field
+
+    def __get__(self, obj, type=None):
+        if obj is None:
+            return self
+        return obj.__dict__[self.field.name]
+
+    def __set__(self, obj, value):
+        obj.__dict__[self.field.name] = self.field.to_python(value)
diff --git a/src/fullctl/django/fields/service_bridge/__init__.py b/src/fullctl/django/fields/service_bridge/__init__.py
new file mode 100644
index 00000000..99bb10cd
--- /dev/null
+++ b/src/fullctl/django/fields/service_bridge/__init__.py
@@ -0,0 +1,179 @@
+from importlib import import_module
+from logging import getLogger
+
+from django.conf import settings
+from django.db.models import CharField, PositiveIntegerField
+
+from fullctl.django.fields import CastOnAssignDescriptor
+
+log = getLogger("django")
+
+BRIDGE_MAP = {}
+
+
+for name in dir(settings):
+    if name.startswith("SERVICE_BRIDGE_REF_"):
+        path = getattr(settings, name).split(".")
+        name = name.split("SERVICE_BRIDGE_REF_")[1].lower()
+        print("loading", path, "for", name)
+        if not path or len(path) < 2:
+            BRIDGE_MAP[name] = None
+            continue
+        bridge = getattr(import_module(".".join(path[:-1])), path[-1])
+        BRIDGE_MAP[name] = bridge
+
+
+class ReferencedObject:
+    @property
+    def object(self):
+        if not hasattr(self, "_object"):
+            self._object = self.load()
+        return self._object
+
+    @property
+    def ux_url(self):
+
+        if not self.bridge:
+            return None
+
+        return self.bridge().ux_url(self.id)
+
+    @property
+    def api_url(self):
+
+        if not self.bridge:
+            return None
+
+        return self.bridge().api_url(self.id)
+
+    def __init__(self, bridge, id, remote_lookup="id"):
+        self.bridge = bridge
+        self.remote_lookup = remote_lookup
+        self.id = id
+        self.source = None
+
+    def __repr__(self):
+        return f"{self.bridge.__name__} {self.id}"
+
+    def __str__(self):
+        return f"{self.id}"
+
+    def __int__(self):
+        return self.id
+
+    def __lt__(self, b):
+        return int(self) < int(b)
+
+    def __gt__(self, b):
+        return int(self) > int(b)
+
+    def __le__(self, b):
+        return int(self) <= int(b)
+
+    def __ge__(self, b):
+        return int(self) >= int(b)
+
+    def __len__(self):
+        return len(str(self))
+
+    def load(self):
+        if not self.bridge:
+            return None
+
+        kwargs = {self.remote_lookup: self.id}
+        bridge = self.bridge()
+        self.source = f"{bridge.ref_tag}.{self.id}@{bridge.host}"
+        return bridge.first(**kwargs)
+
+
+class ReferencedObjectFieldMixin:
+
+    """
+    References an object on another fullctl service via the
+    service bridge
+    """
+
+    base_type = int
+
+    def __init__(
+        self, bridge=None, remote_lookup="id", bridge_type=None, *args, **kwargs
+    ):
+
+        if bridge and bridge_type:
+            raise AttributeError("Cannot specify both bridge and bridge_type")
+
+        if bridge_type:
+            try:
+                bridge = BRIDGE_MAP[bridge_type]
+            except KeyError:
+                raise KeyError(
+                    f"settings.SERVICE_BRIDGE_REF_{bridge_type.upper} not set"
+                )
+
+        self.bridge_type = bridge_type
+        self.bridge = bridge
+        self.remote_lookup = remote_lookup
+
+        super().__init__(*args, **kwargs)
+
+    def deconstruct(self):
+
+        name, path, args, kwargs = super().deconstruct()
+
+        if self.bridge_type:
+            kwargs["bridge_type"] = self.bridge_type
+        else:
+            kwargs["bridge"] = self.bridge
+
+        if self.remote_lookup != "id":
+            kwargs["remote_lookup"] = self.remote_lookup
+
+        return (name, path, args, kwargs)
+
+    def from_db_value(self, value, expression, connection):
+
+        if value is None:
+            return None
+
+        return ReferencedObject(self.bridge, value, self.remote_lookup)
+
+    def to_python(self, value):
+
+        if value is None:
+            return None
+
+        if isinstance(value, ReferencedObject):
+            return value
+
+        return ReferencedObject(self.bridge, value, self.remote_lookup)
+
+    def get_prep_value(self, value):
+
+        if value is None:
+            return None
+        if isinstance(value, self.base_type):
+            return value
+        return value.id
+
+    def contribute_to_class(self, cls, name):
+        super().contribute_to_class(cls, name)
+        setattr(cls, name, CastOnAssignDescriptor(self))
+
+
+class ReferencedObjectField(ReferencedObjectFieldMixin, PositiveIntegerField):
+    """
+    References an object on another fullctl service via the
+    service bridge using a positive integer
+    """
+
+    pass
+
+
+class ReferencedObjectCharField(ReferencedObjectFieldMixin, CharField):
+
+    """
+    References an object on another fullctl service via the
+    service bridge using a char field
+    """
+
+    base_type = str
diff --git a/src/fullctl/django/fields/service_bridge/prefixctl.py b/src/fullctl/django/fields/service_bridge/prefixctl.py
new file mode 100644
index 00000000..82e0d650
--- /dev/null
+++ b/src/fullctl/django/fields/service_bridge/prefixctl.py
@@ -0,0 +1,9 @@
+import fullctl.service_bridge.prefixctl as prefixctl_bridge
+from fullctl.django.fields.service_bridge import ReferencedObjectField
+
+
+class PrefixSet(ReferencedObjectField):
+    def __init__(self, *args, **kwargs):
+
+        kwargs["bridge"] = prefixctl_bridge.PrefixSet
+        return super().__init__(*args, **kwargs)
diff --git a/src/fullctl/django/migrations/0020_auditlog_action_length.py b/src/fullctl/django/migrations/0020_auditlog_action_length.py
new file mode 100644
index 00000000..abce0243
--- /dev/null
+++ b/src/fullctl/django/migrations/0020_auditlog_action_length.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.14 on 2022-08-11 11:54
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("django_fullctl", "0019_default_org"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="auditlog",
+            name="action",
+            field=models.CharField(max_length=128),
+        ),
+    ]
diff --git a/src/fullctl/django/migrations/0021_servicebridgeaction.py b/src/fullctl/django/migrations/0021_servicebridgeaction.py
new file mode 100644
index 00000000..a1b15141
--- /dev/null
+++ b/src/fullctl/django/migrations/0021_servicebridgeaction.py
@@ -0,0 +1,92 @@
+# Generated by Django 3.2.15 on 2022-09-06 14:37
+
+import django.db.models.manager
+import django_handleref.models
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("django_fullctl", "0020_auditlog_action_length"),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name="ServiceBridgeAction",
+            fields=[
+                ("id", models.AutoField(primary_key=True, serialize=False)),
+                (
+                    "created",
+                    django_handleref.models.CreatedDateTimeField(
+                        auto_now_add=True, verbose_name="Created"
+                    ),
+                ),
+                (
+                    "updated",
+                    django_handleref.models.UpdatedDateTimeField(
+                        auto_now=True, verbose_name="Updated"
+                    ),
+                ),
+                ("version", models.IntegerField(default=0)),
+                (
+                    "status",
+                    models.CharField(
+                        choices=[
+                            ("ok", "Ok"),
+                            ("pending", "Pending"),
+                            ("deactivated", "Deactivated"),
+                            ("failed", "Failed"),
+                            ("expired", "Expired"),
+                        ],
+                        default="ok",
+                        max_length=12,
+                    ),
+                ),
+                ("name", models.CharField(max_length=255, unique=True)),
+                (
+                    "reference",
+                    models.CharField(
+                        help_text="should be {module_path}.{class_name} of the service bridge class",
+                        max_length=255,
+                    ),
+                ),
+                (
+                    "target",
+                    models.CharField(
+                        help_text="should be {app_label}.{mode_name} of the target model",
+                        max_length=255,
+                    ),
+                ),
+                (
+                    "description",
+                    models.CharField(blank=True, max_length=255, null=True),
+                ),
+                (
+                    "action",
+                    models.CharField(
+                        choices=[("pull", "Pull"), ("push", "Push")],
+                        default="pull",
+                        max_length=8,
+                    ),
+                ),
+                (
+                    "data_map",
+                    models.JSONField(
+                        blank=True,
+                        default={},
+                        help_text="map reference fields to target fields - only fields specified here will be affected by this action. Leave empty to use the default definitions for the model, if they exist.",
+                        null=True,
+                    ),
+                ),
+            ],
+            options={
+                "verbose_name": "Service Bridge Action",
+                "verbose_name_plural": "Service Bridge Actions",
+                "db_table": "fullctl_service_bridge_action",
+            },
+            managers=[
+                ("handleref", django.db.models.manager.Manager()),
+            ],
+        ),
+    ]
diff --git a/src/fullctl/django/migrations/0022_auto_20220907_1253.py b/src/fullctl/django/migrations/0022_auto_20220907_1253.py
new file mode 100644
index 00000000..a64cc8a1
--- /dev/null
+++ b/src/fullctl/django/migrations/0022_auto_20220907_1253.py
@@ -0,0 +1,35 @@
+# Generated by Django 3.2.15 on 2022-09-07 12:53
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("django_fullctl", "0021_servicebridgeaction"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="servicebridgeaction",
+            name="function",
+            field=models.CharField(
+                blank=True,
+                choices=[
+                    ("nautbot_push_device_loc", "Nautbot: update device location")
+                ],
+                max_length=255,
+                null=True,
+            ),
+        ),
+        migrations.AlterField(
+            model_name="servicebridgeaction",
+            name="data_map",
+            field=models.JSONField(
+                blank=True,
+                default=dict,
+                help_text="map reference fields to target fields - only fields specified here will be affected by this action. Leave empty to use the default definitions for the model, if they exist.",
+                null=True,
+            ),
+        ),
+    ]
diff --git a/src/fullctl/django/migrations/0023_auto_20220907_1354.py b/src/fullctl/django/migrations/0023_auto_20220907_1354.py
new file mode 100644
index 00000000..b91070d6
--- /dev/null
+++ b/src/fullctl/django/migrations/0023_auto_20220907_1354.py
@@ -0,0 +1,39 @@
+# Generated by Django 3.2.15 on 2022-09-07 13:54
+
+import django.db.models.manager
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("django_fullctl", "0022_auto_20220907_1253"),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name="ServiceBridgeActionTask",
+            fields=[],
+            options={
+                "proxy": True,
+                "indexes": [],
+                "constraints": [],
+            },
+            bases=("django_fullctl.task",),
+            managers=[
+                ("handleref", django.db.models.manager.Manager()),
+            ],
+        ),
+        migrations.AlterField(
+            model_name="servicebridgeaction",
+            name="function",
+            field=models.CharField(
+                blank=True,
+                choices=[
+                    ("nautobot_push_device_loc", "Nautobot: update device location")
+                ],
+                max_length=255,
+                null=True,
+            ),
+        ),
+    ]
diff --git a/src/fullctl/django/models/abstract/__init__.py b/src/fullctl/django/models/abstract/__init__.py
index 7616e264..29fc0dba 100644
--- a/src/fullctl/django/models/abstract/__init__.py
+++ b/src/fullctl/django/models/abstract/__init__.py
@@ -1,1 +1,2 @@
 from fullctl.django.models.abstract.base import *  # noqa: F401, F403
+from fullctl.django.models.abstract.service_bridge import *  # noqa: F401, F403
diff --git a/src/fullctl/django/models/abstract/base.py b/src/fullctl/django/models/abstract/base.py
index 04b07487..3003eafb 100644
--- a/src/fullctl/django/models/abstract/base.py
+++ b/src/fullctl/django/models/abstract/base.py
@@ -1,8 +1,10 @@
 from django.db import models
 from django.utils.translation import gettext_lazy as _
+from django_countries.fields import CountryField
 from django_handleref.models import HandleRefModel as SoftDeleteHandleRefModel
 
 __all__ = [
+    "GeoModel",
     "HandleRefModel",
     "PdbRefModel",
 ]
@@ -33,6 +35,33 @@ def delete(self):
         return super().delete(hard=True)
 
 
+class GeoModel(models.Model):
+
+    """
+    Mixin class to use on models that need to store a geo location
+    """
+
+    address1 = models.CharField(_("Address 1"), max_length=255, blank=True)
+    address2 = models.CharField(_("Address 2"), max_length=255, blank=True)
+    city = models.CharField(_("City"), max_length=255, blank=True)
+    state = models.CharField(_("State"), max_length=255, blank=True)
+    zipcode = models.CharField(_("Zip-Code"), max_length=48, blank=True)
+    country = CountryField(_("Country"), blank=True)
+
+    suite = models.CharField(_("Suite"), max_length=255, blank=True)
+    floor = models.CharField(_("Floor"), max_length=255, blank=True)
+
+    latitude = models.DecimalField(
+        _("Latitude"), max_digits=9, decimal_places=6, blank=True, null=True
+    )
+    longitude = models.DecimalField(
+        _("Longitude"), max_digits=9, decimal_places=6, blank=True, null=True
+    )
+
+    class Meta:
+        abstract = True
+
+
 class PdbRefModel(HandleRefModel):
 
     """
diff --git a/src/fullctl/django/models/abstract/service_bridge.py b/src/fullctl/django/models/abstract/service_bridge.py
new file mode 100644
index 00000000..c44c2704
--- /dev/null
+++ b/src/fullctl/django/models/abstract/service_bridge.py
@@ -0,0 +1,245 @@
+from django.db import models
+
+from fullctl.django.context import service_bridge_sync
+from fullctl.django.models.abstract import HandleRefModel
+from fullctl.django.models.concrete.service_bridge import ServiceBridgeAction
+from fullctl.utils import rgetattr
+
+__all__ = [
+    "ServiceBridgeReferenceModel",
+]
+
+
+class ServiceBridgeReferenceModel(HandleRefModel):
+
+    """
+    Enables a model to have a main reference at another service
+    supported by the fullctl service bridge
+
+    Model can also have multiple additional service bridge actions
+    to facilitate the pushing to remote references as well as pulling
+    in additional data from secondary references.
+
+    Should define a `ServiceBridge` Meta class with `map_{service_name}` property
+    for mapping reference field names to fullctl field_names
+    """
+
+    reference_is_sot = models.BooleanField(default=True)
+
+    class Meta:
+        abstract = True
+
+    @property
+    def reference_source(self):
+        """
+        Returns the service name for the reference
+        """
+        if not self.reference or not self.reference.bridge:
+            return None
+        return f"{self.reference.bridge.Meta.service}"
+
+    @property
+    def reference_ux_url(self):
+        """
+        Returns the UX management url for the reference (if available)
+        """
+        if not self.reference:
+            return None
+        return self.reference.ux_url
+
+    @property
+    def reference_api_url(self):
+        """
+        Returns the API url for the reference
+        """
+        if not self.reference:
+            return None
+
+        return self.reference.api_url
+
+    @property
+    def fullctl_id(self):
+        return self.id
+
+    @property
+    def sot(self):
+        """
+        Returns the source of truth object, which is either `self` or if `reference_is_sot` is `True`
+        , the reference object
+        """
+        if self.reference_is_sot:
+            return self.reference.object
+        return self
+
+    @property
+    def service_bridge_actions(self):
+        """
+        Returns a `list` of ServiceBridgeAction instances for this model
+        """
+        if not hasattr(self, "_service_bridge_actions"):
+            qset = ServiceBridgeAction.objects.filter(
+                target__iexact=f"{self._meta.app_label}.{self._meta.model_name}"
+            )
+            self._service_bridge_actions = list(qset)
+        return self._service_bridge_actions
+
+    @classmethod
+    def from_db(cls, *args, **kwargs):
+        # When the `service_bridge_sync` context is active for pulling data
+        # we want to substitute any values read from the databse with the values
+        # that exist on the source of truth.
+        instance = super().from_db(*args, **kwargs)
+        with service_bridge_sync() as sync:
+            sync.pull(instance)
+
+        return instance
+
+    def refresh_from_db(self, *args, **kwargs):
+        # When the `service_bridge_sync` context is active for pulling data
+        # we want to substitute any values read from the databse with the values
+        # that exist on the source of truth.
+
+        r = super().refresh_from_db(*args, **kwargs)
+
+        with service_bridge_sync() as sync:
+            sync.pull(self)
+
+        return r
+
+    def reverse_field_map(self, service_name=None):
+
+        """
+        Returns the field mapping from fullctl to reference fields
+        as a dict
+
+        Fullctl field names will be keys, reference field names will be the values
+        """
+        return {[(v, k) for k, v in self.field_map(service_name).items()]}
+
+    def field_map(self, service_name=None):
+
+        """
+        Returns the field mapping from reference to fullctl fields
+        as a dict
+
+        Reference field names will be the keys, fullctl field names will be the values
+        """
+        if not service_name:
+            service_name = self.reference.bridge.Meta.service
+        return getattr(self.ServiceBridge, f"map_{service_name}")
+
+    def sync_from_reference(self, ref_obj=None, if_sot=False, save=False):
+
+        """
+        Updates the instance from the reference using the field map (only fields
+        in the field map will be updated) specified in the ServiceBridge meta
+        class
+
+        Arguments:
+
+        - ref_obj: Service bridge entity - if not specified will use self.reference.object
+        - if_sot(`bool`): only sync if `reference_is_sot` is `True`
+        - save(`bool`): if changes are detected during sync, save them locally
+        """
+
+        if if_sot and not self.reference_is_sot:
+            return
+
+        if ref_obj == self:
+            return
+
+        if self.reference and not ref_obj:
+            ref_obj = self.reference.object
+
+        changed = False
+        if ref_obj:
+            for dest_field, src_field in self.field_map().items():
+                dest_value = rgetattr(ref_obj, dest_field)
+                src_value = getattr(self, src_field)
+                try:
+                    if dest_value == type(dest_value)(src_value):
+                        continue
+                except (ValueError, TypeError):
+                    pass
+
+                changed = True
+                # print(f"{src_field} changed from {src_value} to {dest_value}")
+                setattr(self, src_field, dest_value)
+
+        # print("sync from reference", self, changed)
+
+        if changed and save:
+            self.save()
+
+        return changed
+
+    def save(self, *args, **kwargs):
+        super().save(*args, **kwargs)
+
+        # if the service_bridge_sync context is actuve for pushing
+        # push updates to references
+
+        with service_bridge_sync() as sync:
+            sync.push(self)
+
+    def push(self):
+        """
+        Performs push actions
+        """
+
+        actions = self.service_bridge_actions
+        for action in actions:
+            if action.action == "push":
+                action.run_as_task(self)
+
+    def pull(self):
+        """
+        Perform pull actions
+        """
+        actions = self.service_bridge_actions
+        for action in actions:
+            if action.action == "pull":
+                action.run(self)
+
+    def service_bridge_data(self, service_name, field_map=None):
+
+        """
+        Returns a dict of fields and values according to the field map
+
+        If no field map is providied, the default field map specified in the
+        `ServiceBridge` Meta class will be used
+
+        Arguments:
+
+        - service_name (`str`): name of the service (e.g, pdbctl, nautbot etc.)
+
+        - field_map (`dict`): if specified this field map will be used, should
+          be a dict of reference field names mapped to fullctl field names
+        """
+
+        if not field_map:
+            field_map = self.field_map(service_name)
+
+        data = {}
+
+        ref = self.sot
+
+        with service_bridge_sync() as sync:
+            sync.pull(self)
+
+        for dest_field, src_field in field_map.items():
+            if "." in dest_field:
+                current = data
+                field_name = dest_field.split(".")[-1]
+                for nested in dest_field.split(".")[:-1]:
+                    current[nested] = {}
+                    current = current[nested]
+                current[field_name] = getattr(
+                    ref, src_field, getattr(self, src_field, None)
+                )
+            else:
+                data[dest_field] = getattr(
+                    ref, src_field, getattr(self, src_field, None)
+                )
+
+        return data
diff --git a/src/fullctl/django/models/abstract/template.py b/src/fullctl/django/models/abstract/template.py
new file mode 100644
index 00000000..058af55e
--- /dev/null
+++ b/src/fullctl/django/models/abstract/template.py
@@ -0,0 +1,97 @@
+"""
+Models to facilitate customization and rendering of jinja2 templates
+"""
+
+import os
+
+from django.conf import settings
+from django.db import models
+from django.utils.html import strip_tags
+from jinja2 import DictLoader, Environment, FileSystemLoader
+
+from fullctl.django.exceptions import TemplateRenderError
+
+from .base import HandleRefModel
+
+
+def make_variable_name(value):
+    return f"{value}".replace(" ", "_").replace(".", "_")
+    # FIXME: make this work magically, or wait until we are on py3
+    # trans = string.maketrans(u" .", u"__")
+    # return string.translate(u"{}".format(value), trans)
+
+
+class TemplateModel(HandleRefModel):
+
+    name = models.CharField(max_length=255)
+    body = models.TextField()
+
+    # extended model needs to implement `type` field of type CharField
+
+    class Meta:
+        abstract = True
+
+    @property
+    def template_path(self):
+        """
+        Returns the template path using the handleref tag and
+        type
+        """
+        return os.path.join(self.HandleRef.tag, f"{self.type}.txt")
+
+    @property
+    def template_loader_paths(self):
+        return [
+            os.path.join(settings.SERVICE_APP_DIR, "templates", settings.SERVICE_TAG)
+        ]
+
+    @property
+    def context(self):
+        if not hasattr(self, "_context"):
+            self._context = {}
+        return self._context
+
+    def get_data(self):
+        return dict()
+
+    def get_env(self):
+        """
+        Returns the jinja template environment
+        """
+
+        if self.body:
+
+            # if body is not empty, we use a dict loader
+            # to make jinja load it as the template
+            loader = DictLoader({self.template_path: self.body})
+        else:
+
+            # if body is empty we will load the default
+            # template from file
+            #
+            # all templates are located at
+            # templates/peerctl/<handle_ref_tag>
+            loader = FileSystemLoader(self.template_loader_paths)
+
+        env = Environment(trim_blocks=True, loader=loader)
+
+        env.filters["make_variable_name"] = make_variable_name
+
+        return env
+
+    def render(self):
+        """
+        renders a template to UTF-8
+        """
+
+        # if content_override is specified return that immediately
+        # and skip the template rendering entirely
+        if getattr(self, "content_override", None):
+            return self.content_override
+
+        env = self.get_env()
+        template = env.get_template(self.template_path)
+        try:
+            return strip_tags(template.render(context=self.context, **self.get_data()))
+        except Exception as exc:
+            raise TemplateRenderError(exc)
diff --git a/src/fullctl/django/models/concrete/account.py b/src/fullctl/django/models/concrete/account.py
index 464af986..eb98140a 100644
--- a/src/fullctl/django/models/concrete/account.py
+++ b/src/fullctl/django/models/concrete/account.py
@@ -6,7 +6,7 @@
 from django.utils.translation import gettext_lazy as _
 from django_grainy.decorators import grainy_model
 
-from fullctl.django.auth import permissions
+import fullctl.django.auth as auth
 from fullctl.django.models.abstract import HandleRefModel
 
 __all__ = [
@@ -90,7 +90,7 @@ def accessible(cls, user):
         - `list`
         """
 
-        perms = permissions(user)
+        perms = auth.permissions(user)
 
         # user is a member of these orgs
         if hasattr(user, "org_set"):
@@ -240,7 +240,7 @@ class OrganizationUser(HandleRefModel):
     is_default = models.BooleanField(default=False)
 
     class HandleRef:
-        tag = "orguser"
+        tag = "org_user"
 
     class Meta:
         db_table = "fullctl_org_user"
diff --git a/src/fullctl/django/models/concrete/auditlog.py b/src/fullctl/django/models/concrete/auditlog.py
index 36b35cf4..a1f36fed 100644
--- a/src/fullctl/django/models/concrete/auditlog.py
+++ b/src/fullctl/django/models/concrete/auditlog.py
@@ -23,7 +23,7 @@ class AuditLog(models.Model):
         get_user_model(), null=True, blank=True, on_delete=models.PROTECT
     )
     key = models.CharField(max_length=8, null=True, blank=True)
-    action = models.CharField(max_length=32)
+    action = models.CharField(max_length=128)
     info = models.CharField(
         max_length=255, help_text=_("Any extra information for the log entry")
     )
diff --git a/src/fullctl/django/models/concrete/service_bridge.py b/src/fullctl/django/models/concrete/service_bridge.py
new file mode 100644
index 00000000..1354fa7a
--- /dev/null
+++ b/src/fullctl/django/models/concrete/service_bridge.py
@@ -0,0 +1,288 @@
+import importlib
+import logging
+
+from django.apps import apps
+from django.db import models
+from django.utils.functional import lazy
+from django.utils.translation import gettext_lazy as _
+
+from fullctl.django.models.abstract import HandleRefModel
+from fullctl.django.models.concrete.tasks import Task, TaskLimitError
+from fullctl.django.tasks import register
+
+__all__ = [
+    "ServiceBridgeAction",
+]
+
+# stores action handlers
+# use @service_bridge_action decorator
+# to decorate a function
+
+handlers = {}
+
+log = logging.getLogger(__name__)
+
+
+def handler_choices():
+
+    r = []
+
+    for handler_info in handlers.values():
+        _, _id, label = handler_info
+        r.append((_id, label))
+
+    return r
+
+
+class service_bridge_action:
+
+    """
+    Decorates a function to be a service bridge action handler
+
+    The function will then become available as a choice in the action's
+    `function` propertry in django-admin
+
+    Arguments:
+
+    - id (`str`): unique identifier for the handler
+    - label (`str`): user friendly label (use gettext)
+    """
+
+    def __init__(self, _id, label):
+        self.id = _id
+        self.label = label
+
+    def __call__(self, fn):
+        handlers[self.id] = (fn, self.id, self.label)
+        return fn
+
+
+@register
+class ServiceBridgeActionTask(Task):
+
+    """
+    Task handler for ServiceBridgeAction
+    """
+
+    class Meta:
+        proxy = True
+
+    class HandleRef:
+        tag = "task_service_bridge_action"
+
+    class TaskMeta:
+        limit = 1
+
+    @property
+    def generate_limit_id(self):
+        return str(self.param["args"][0]) + "." + str(self.param["args"][1])
+
+    def run(self, action_id, obj_id):
+        action = ServiceBridgeAction.objects.get(id=action_id)
+        obj = action.target_model.objects.get(id=obj_id)
+        action.run(obj)
+
+
+class ServiceBridgeAction(HandleRefModel):
+
+    """
+    Maps a service bridge class to a fullctl handle-ref model for either
+    push or pull operations in a configurable fashion.
+    """
+
+    name = models.CharField(max_length=255, unique=True)
+    reference = models.CharField(
+        max_length=255,
+        help_text=_("should be {module_path}.{class_name} of the service bridge class"),
+    )
+    target = models.CharField(
+        max_length=255,
+        help_text=_("should be {app_label}.{mode_name} of the target model"),
+    )
+    description = models.CharField(max_length=255, null=True, blank=True)
+    action = models.CharField(
+        max_length=8, choices=(("pull", _("Pull")), ("push", _("Push"))), default="pull"
+    )
+    function = models.CharField(
+        max_length=255, choices=lazy(handler_choices, list)(), null=True, blank=True
+    )
+    data_map = models.JSONField(
+        null=True,
+        blank=True,
+        help_text=_(
+            "map reference fields to target fields - only fields specified here will be affected by this action. Leave empty to use the default definitions for the model, if they exist."
+        ),
+        default=dict,
+    )
+
+    class HandelRef:
+        tag = "service_bridge_action"
+
+    class Meta:
+        db_table = "fullctl_service_bridge_action"
+        verbose_name = _("Service Bridge Action")
+        verbose_name_plural = _("Service Bridge Actions")
+
+    @property
+    def target_model(self):
+        """
+        Returns the model class for the specified target
+        """
+        app_label, model = self.target.split(".")
+        return apps.get_model(app_label, model)
+
+    @property
+    def reference_service_name(self):
+        """
+        Returns the service name of the reference source (e.g., pdbctl, nautobot etc.)
+        """
+        service_name, _ = tuple(self.reference.split("."))
+        return service_name
+
+    @property
+    def reference_client(self):
+        """
+        Returns a service bridge client for the reference specified in this action
+        """
+
+        if hasattr(self, "_reference_client"):
+            return self._reference_client
+
+        service_name, class_name = tuple(self.reference.split("."))
+
+        mod = importlib.import_module(f"fullctl.service_bridge.{service_name}")
+        cls = getattr(mod, class_name)
+
+        self._reference_client = cls()
+        return self._reference_client
+
+    def __str__(self):
+        return f"ServiceBridgeAction({self.name})[{self.target} {self.action} {self.reference}]"
+
+    def reference_object(self, obj):
+
+        """
+        Attempts to load and return the reference object for this action
+
+        Arguments:
+
+        - obj (`ServiceBridgeModel`)
+        """
+
+        client = self.reference_client
+        service_name = self.reference_service_name
+
+        try:
+
+            # TODO: should lookup field be a property of ServiceBridgeAction ?
+
+            lookup_field = getattr(obj.ServiceBridge, f"lookup_{service_name}")
+            return client.first(**{lookup_field: obj.id})
+        except AttributeError:
+            return obj.reference.object
+
+    def run_as_task(self, obj):
+
+        """
+        Creates a task for this action and the specified ServiceBridgeModel instance
+
+        Arguments:
+
+        - obj (`ServiceBridgeModel`)
+        """
+
+        try:
+            ServiceBridgeActionTask.create_task(self.id, obj.id)
+        except TaskLimitError:
+            pass
+
+    def run(self, obj):
+
+        """
+        Runs this action for the specified ServiceBridgeModel instance
+        """
+
+        fn = getattr(self, self.action)
+        fn(obj)
+
+    def push(self, obj):
+
+        """
+        Pushes data for the specified ServiceBridgeModel instance to the reference
+        using th service bridge
+
+        Arguments:
+
+        - obj (`ServiceBridgeModel`)
+        """
+
+        client = self.reference_client
+        service_name = self.reference_service_name
+        reference_object = self.reference_object(obj)
+
+        # data = obj.service_bridge_data(service_name, self.data_map)
+
+        if not self.function:
+
+            # No function handler is specified, perform a data push
+
+            if not reference_object:
+
+                # reference object does not exist yet, create it
+
+                client.create(obj.service_bridge_data(service_name))
+
+            elif not self.data_map:
+
+                # reference object exists and no custom field mapping has been provided
+                # perform a PUT update using the pre-defined service bridge field map for
+                # the service
+
+                # TODO: specify method in schema?
+
+                client.update(reference_object, obj.service_bridge_data(service_name))
+
+            else:
+
+                # reference object exists and custom field mapping has been provided
+                # perform a PATCH update using the custom field mapping
+
+                # TODO: specify method in schema?
+
+                client.partial_update(
+                    reference_object,
+                    obj.service_bridge_data(service_name, self.data_map),
+                )
+        else:
+
+            # Function handler specified, attempt to run it
+
+            try:
+                fn, _, _ = handlers[self.function]
+            except KeyError:
+                log.error(
+                    f"Unknown service bridge action handler specified on {self}: {self.function}"
+                )
+                return
+            fn("push", obj)
+
+    def pull(self, obj):
+        """
+        Pulls data for the specified ServiceBridgeModel instance from the reference using
+        the service bridge
+
+        Arguments:
+
+        - obj (`ServiceBridgeModel`)
+        """
+
+        if self.function:
+
+            try:
+                fn, _, _ = handlers[self.function]
+            except KeyError:
+                log.error(
+                    f"Unknown service bridge action handler specified on {self}: {self.function}"
+                )
+                return
+            fn("pull", obj)
diff --git a/src/fullctl/django/rest/api_schema.py b/src/fullctl/django/rest/api_schema.py
index de7737a3..91524fe4 100644
--- a/src/fullctl/django/rest/api_schema.py
+++ b/src/fullctl/django/rest/api_schema.py
@@ -1,8 +1,25 @@
+from django.conf import settings
+from rest_framework import serializers
 from rest_framework.schemas.openapi import AutoSchema
+from rest_framework.schemas.openapi import SchemaGenerator as BaseSchemaGenerator
+from rest_framework.schemas.openapi import is_list_view
 
 from fullctl.django.rest.serializers import ModelSerializer
 
 
+class SchemaGenerator(BaseSchemaGenerator):
+    def has_view_permissions(self, path, method, view):
+
+        generate_service_bridge = getattr(
+            settings, "API_DOCS_GENERATE_SERVICE_BRIDGE", False
+        )
+
+        if "service-bridge" in path and not generate_service_bridge:
+            return False
+
+        return super().has_view_permissions(path, method, view)
+
+
 class BaseSchema(AutoSchema):
 
     """
@@ -37,18 +54,30 @@ def get_operation_type(self, *args):
 
         return method.lower()
 
-    def _get_operation_id(self, path, method):
+    def get_operation_id(self, path, method):
         """
         We override this so operation ids become "{op} {reftag}"
         """
 
-        serializer, model = self.get_classes(path, method)
-        op_type = self.get_operation_type(path, method)
+        # serializer, model = self.get_classes(path, method)
+        # op_type = self.get_operation_type(path, method)
+        method_name = getattr(self.view, "action", method.lower())
 
-        if model:
-            return f"{model.HandleRef.tag}.{op_type}"
+        if is_list_view(path, method, self.view):
+            action = "list"
+        elif method_name not in self.method_mapping:
+            action = method_name
+        else:
+            action = self.method_mapping[method.lower()]
+
+        # name = self.get_operation_id_base(path, method, action)
 
-        return super()._get_operation_id(path, method)
+        name = self.view.__class__.__name__
+
+        if "service-bridge" in path:
+            name = f"Service Bridge: {name}"
+
+        return f"{name} {action}"
 
     def get_classes(self, *op_args):
         """
@@ -89,6 +118,9 @@ def get_operation(self, *args, **kwargs):
 
         serializer, model = self.get_classes(*args)
 
+        if not model or not hasattr(model, "HandleRef"):
+            return op_dict
+
         # if we were able to get a model we want to include the markdown documentation
         # for the model type in the openapi description field (docs/api/obj_*.md)
 
@@ -99,6 +131,59 @@ def get_operation(self, *args, **kwargs):
 
         return op_dict
 
+    def get_components(self, path, method):
+        """
+        Return components with their properties from the serializer.
+        """
+
+        request_serializer = self.get_request_serializer(path, method)
+        response_serializer = self.get_response_serializer(path, method)
+
+        components = {}
+
+        if isinstance(request_serializer, serializers.Serializer):
+            component_name = self.get_component_name(request_serializer)
+            content = self.map_serializer(request_serializer)
+            components.setdefault(component_name, content)
+
+        if isinstance(response_serializer, serializers.Serializer):
+            component_name = self.get_component_name(response_serializer)
+            content = self.map_serializer(response_serializer)
+            components.setdefault(component_name, content)
+
+        return components
+
+    def get_serializer(self, path, method, direction="response"):
+        view = self.view
+
+        if hasattr(view, "get_serializer_dynamic"):
+            return view.get_serializer_dynamic(path, method, direction)
+
+        return super().get_serializer(path, method)
+
+    def get_response_serializer(self, path, method):
+        return self.get_serializer(path, method, "response")
+
+    def get_request_serializer(self, path, method):
+        return self.get_serializer(path, method, "request")
+
+    def get_request_body(self, path, method):
+        if method not in ("PUT", "PATCH", "POST", "DELETE"):
+            return {}
+
+        self.request_media_types = self.map_parsers(path, method)
+
+        serializer = self.get_request_serializer(path, method)
+
+        if not isinstance(serializer, serializers.Serializer):
+            item_schema = {}
+        else:
+            item_schema = self._get_reference(serializer)
+
+        return {
+            "content": {ct: {"schema": item_schema} for ct in self.request_media_types}
+        }
+
     def request_body_schema(self, op_dict, content="application/json"):
         """
         Helper function that return the request body schema
@@ -112,6 +197,43 @@ def request_body_schema(self, op_dict, content="application/json"):
             .get("schema", {})
         )
 
+    def get_reference(self, serializer):
+        return {"$ref": f"#/components/schemas/{self.get_component_name(serializer)}"}
+
+    def get_responses(self, path, method):
+
+        self.response_media_types = self.map_renderers(path, method)
+
+        serializer = self.get_response_serializer(path, method)
+
+        if not isinstance(serializer, serializers.Serializer):
+            item_schema = {}
+        else:
+            item_schema = self.get_reference(serializer)
+
+        response_schema = {
+            "type": "object",
+            "properties": {
+                "data": {
+                    "type": "array",
+                    "items": item_schema,
+                }
+            },
+        }
+
+        status_code = "201" if method == "POST" else "200"
+        return {
+            status_code: {
+                "content": {
+                    ct: {"schema": response_schema} for ct in self.response_media_types
+                },
+                # description is a mandatory property,
+                # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#responseObject
+                # TODO: put something meaningful into it
+                "description": "",
+            }
+        }
+
 
 class PeeringDBImportSchema(AutoSchema):
     def __init__(self, *args, **kwargs):
diff --git a/src/fullctl/django/rest/decorators.py b/src/fullctl/django/rest/decorators.py
index e8e8db05..9c89b724 100644
--- a/src/fullctl/django/rest/decorators.py
+++ b/src/fullctl/django/rest/decorators.py
@@ -1,3 +1,5 @@
+from functools import wraps
+
 import reversion
 from django.conf import settings
 from django.contrib.auth import get_user_model
@@ -7,6 +9,7 @@
 
 from fullctl.django.auth import Permissions, RemotePermissions
 from fullctl.django.models import Instance, Organization
+from fullctl.django.rest.authentication import APIKey
 from fullctl.django.rest.core import HANDLEREF_FIELDS
 from fullctl.service_bridge.client import AaaCtl
 
@@ -121,7 +124,14 @@ def __call__(self, fn):
             enable_apply_perms=decorator.enable_apply_perms,
             **decorator.kwargs,
         )
-        def wrapped(self, request, *args, **kwargs):
+        def inner(self, request, *args, **kwargs):
+
+            """
+            inner wrapper, called after grainy permissioning logic ran
+
+            handles auth requirement, loading of organization instance
+            and opening of reversion context
+            """
 
             if decorator.require_auth and not request.user.is_authenticated:
                 return Response(status=401)
@@ -137,18 +147,40 @@ def wrapped(self, request, *args, **kwargs):
 
                 return fn(self, request, *args, **kwargs)
 
-        wrapped.__name__ = fn.__name__
+        inner.__name__ = fn.__name__
 
-        return wrapped
+        @wraps(fn)
+        def outer(self, request, *args, **kwargs):
+
+            """
+            outer wrapper, called before grainy permissioning logic runs
+
+            handles superuser imitation for sufficiently provisioned api keys
+            """
+
+            as_user = request.headers.get("X-User")
+            if as_user and hasattr(request, "api_key"):
+                if permissions_cls(APIKey(request.api_key)).check(
+                    f"superuser.{as_user}", "c"
+                ):
+                    request.user = get_user_model().objects.get(
+                        social_auth__uid=as_user
+                    )
+
+            return inner(self, request, *args, **kwargs)
+
+        outer.__name__ = fn.__name__
+
+        return outer
 
 
 class _aaactl:
     @property
     def connected(self):
-        return getattr(settings, "AAACTL_HOST", None) is not None
+        return getattr(settings, "AAACTL_URL", None) is not None
 
     def bridge(self, org_slug):
-        return AaaCtl(settings.AAACTL_HOST, settings.SERVICE_KEY, org_slug)
+        return AaaCtl(settings.AAACTL_URL, settings.SERVICE_KEY, org_slug)
 
 
 class billable(_aaactl):
@@ -156,7 +188,7 @@ class billable(_aaactl):
     """
     Will use the aaactl service bridge to determine
     if the specified product/service has accumulated
-    costs during the current subscription cycle and
+    costs during the current subscription subscription_cycle and
     return an error response if the there are costs
     but the organization has not yet set up billing
     """
@@ -185,7 +217,7 @@ def wrapped(viewset, request, *args, **kwargs):
                 return Response(
                     {
                         "non_field_errors": [
-                            f"Billing setup required to continue using {product}. Please set up billing for your organization at {settings.AAACTL_HOST}/billing/setup?org={request.org.slug}"
+                            f"Billing setup required to continue using {product}. Please set up billing for your organization at {settings.AAACTL_URL}/billing/setup?org={request.org.slug}"
                         ]
                     },
                     status=403,
@@ -203,7 +235,9 @@ def register(cls):
         if not hasattr(cls, "ref_tag"):
             cls.ref_tag = cls.Meta.model.HandleRef.tag
             cls.Meta.fields += ["grainy"] + HANDLEREF_FIELDS
-        setattr(Serializers, cls.ref_tag, cls)
+
+        ref_tag = cls.ref_tag.replace(".", "__")
+        setattr(Serializers, ref_tag, cls)
         return cls
 
     return (Serializers, register)
diff --git a/src/fullctl/django/rest/fields.py b/src/fullctl/django/rest/fields.py
new file mode 100644
index 00000000..27bf1230
--- /dev/null
+++ b/src/fullctl/django/rest/fields.py
@@ -0,0 +1,15 @@
+from rest_framework.fields import ChoiceField
+
+
+class DynamicChoiceField(ChoiceField):
+
+    """
+    A rest serializer field that allows for dynamic choice values
+    """
+
+    def __init__(self, choices, **kwargs):
+
+        if callable(choices):
+            choices = choices()
+
+        return super().__init__(choices, **kwargs)
diff --git a/src/fullctl/django/rest/mixins.py b/src/fullctl/django/rest/mixins.py
index a1833366..ca689bca 100644
--- a/src/fullctl/django/rest/mixins.py
+++ b/src/fullctl/django/rest/mixins.py
@@ -22,3 +22,29 @@ class OrgQuerysetMixin:
     def get_queryset(self):
         org_tag = self.kwargs["org_tag"]
         return self.queryset.filter(instance__org__slug=org_tag)
+
+
+class ContainerQuerysetMixin:
+    """
+    For objects with URLS that require an "org_tag" as well as
+    a secondary tag from a container entity,
+    this filters the resulting queryset by matching the org slug and the
+    container entity slug to the url tags
+
+    Class needs to define the following class properties
+
+    - container_url_field (`str`): the name of the url parameter identified the container
+    - container_lookup_field (`str`): the database field to use for lookups
+    """
+
+    def get_queryset(self):
+        org_tag = self.kwargs["org_tag"]
+        container_tag = self.kwargs[self.container_url_field]
+        container_lookup_field = self.container_lookup_field
+
+        filters = {
+            f"{container_lookup_field}__slug": container_tag,
+            f"{container_lookup_field}__instance__org__slug": org_tag,
+        }
+
+        return self.queryset.filter(**filters)
diff --git a/src/fullctl/django/rest/serializers/__init__.py b/src/fullctl/django/rest/serializers/__init__.py
index d5873dd2..225abb13 100644
--- a/src/fullctl/django/rest/serializers/__init__.py
+++ b/src/fullctl/django/rest/serializers/__init__.py
@@ -31,7 +31,9 @@ def __call__(self, attrs, serializer):
 
 
 class ModelSerializer(serializers.ModelSerializer):
-    grainy = serializers.SerializerMethodField()
+    grainy = serializers.SerializerMethodField(help_text="Grainy permission namespace")
+
+    status = serializers.CharField(help_text="Object status", read_only=True)
 
     def get_grainy(self, obj):
         if hasattr(obj, "Grainy"):
diff --git a/src/fullctl/django/rest/serializers/aaactl_sync.py b/src/fullctl/django/rest/serializers/aaactl_sync.py
index 839ee35d..6cec976f 100644
--- a/src/fullctl/django/rest/serializers/aaactl_sync.py
+++ b/src/fullctl/django/rest/serializers/aaactl_sync.py
@@ -35,7 +35,7 @@ class Meta:
 @register
 class OrganizationUser(serializers.Serializer):
 
-    ref_tag = "orguser"
+    ref_tag = "org_user"
     user = serializers.IntegerField()
     default_org = serializers.IntegerField()
     orgs = serializers.ListField(child=serializers.IntegerField(), allow_empty=True)
@@ -57,9 +57,9 @@ def save(self):
             if not user.org_set.filter(org=org).exists():
                 models.OrganizationUser.objects.create(org=org, user=user)
 
-        for orguser in user.org_set.all():
-            if orguser.org.remote_id not in self.validated_data["orgs"]:
-                orguser.delete()
+        for org_user in user.org_set.all():
+            if org_user.org.remote_id not in self.validated_data["orgs"]:
+                org_user.delete()
 
         user.org_set.all().update(is_default=False)
         user.org_set.filter(org__remote_id=default_org).update(is_default=True)
diff --git a/src/fullctl/django/rest/serializers/org.py b/src/fullctl/django/rest/serializers/org.py
index 675bf102..7145c3d5 100644
--- a/src/fullctl/django/rest/serializers/org.py
+++ b/src/fullctl/django/rest/serializers/org.py
@@ -9,7 +9,7 @@
 
 @register
 class OrganizationUser(ModelSerializer):
-    ref_tag = "orguser"
+    ref_tag = "org_user"
 
     name = serializers.SerializerMethodField()
     email = serializers.SerializerMethodField()
diff --git a/src/fullctl/django/rest/serializers/service_bridge.py b/src/fullctl/django/rest/serializers/service_bridge.py
new file mode 100644
index 00000000..0702bd5d
--- /dev/null
+++ b/src/fullctl/django/rest/serializers/service_bridge.py
@@ -0,0 +1,13 @@
+from rest_framework import serializers
+
+
+class HeartbeatSerializer(serializers.Serializer):
+
+    status = serializers.CharField()
+
+    class Meta:
+        fields = ["status"]
+
+
+class StatusSerializer(serializers.Serializer):
+    pass
diff --git a/src/fullctl/django/rest/serializers/template.py b/src/fullctl/django/rest/serializers/template.py
new file mode 100644
index 00000000..4a980105
--- /dev/null
+++ b/src/fullctl/django/rest/serializers/template.py
@@ -0,0 +1,26 @@
+from rest_framework import serializers
+
+
+class TemplateSerializer(serializers.Serializer):
+
+    """
+    This serializer can be used to preview templates
+    It needs to be instantiated with a TemplateBase
+    object
+    """
+
+    body = serializers.SerializerMethodField()
+
+    class Meta:
+        fields = ["id", "name", "type", "body"]
+
+    def get_body(self, obj):
+        try:
+            return obj.render()
+        except Exception as exc:
+            return (
+                "!!! ERROR !!!\nWhen trying to render the template "
+                "we encountered the following issue:\n\n{}\n\nPlease fix and try again.".format(
+                    exc
+                )
+            )
diff --git a/src/fullctl/django/rest/urls/service_bridge_proxy.py b/src/fullctl/django/rest/urls/service_bridge_proxy.py
index cf193deb..0652fbd9 100644
--- a/src/fullctl/django/rest/urls/service_bridge_proxy.py
+++ b/src/fullctl/django/rest/urls/service_bridge_proxy.py
@@ -1,6 +1,7 @@
 import json
 
 import requests
+from django.conf import settings
 from django.http import JsonResponse
 from django.urls import include, path
 
@@ -15,8 +16,10 @@ def proxy_api(service, host, endpoints):
     """
 
     paths = [
-        proxy_api_endpoint(service, host, {"remote": remote, "local": local})
-        for remote, local in endpoints
+        proxy_api_endpoint(
+            service, host, {"remote": remote, "local": local, "name": name}
+        )
+        for remote, local, name in endpoints
     ]
 
     return include(paths)
@@ -24,7 +27,7 @@ def proxy_api(service, host, endpoints):
 
 def proxy_api_endpoint(service, host, endpoint):
     def view_proxy(request, org_tag, *args, **kwargs):
-        api_key = request.user.key_set.first()
+        api_key = settings.SERVICE_KEY
         method = request.method.lower()
         request_fn = getattr(requests, method)
 
@@ -34,13 +37,16 @@ def view_proxy(request, org_tag, *args, **kwargs):
         if method in ["post", "put", "patch"]:
             _kwargs.update(json=json.loads(request.body))
 
-        print(_kwargs)
-
         endpoint_remote = endpoint["remote"].format(org_tag=org_tag, **kwargs)
 
         url = f"{host}/api/{endpoint_remote}"
 
-        response = request_fn(url, params={"key": api_key.key}, **_kwargs)
+        headers = {
+            "Authorization": f"Bearer {api_key}",
+            "X-User": request.user.social_auth.first().uid,
+        }
+
+        response = request_fn(url, headers=headers, **_kwargs)
         print("proxied response in", response.elapsed.total_seconds())
 
         json_dumps_params = {}
@@ -55,14 +61,15 @@ def view_proxy(request, org_tag, *args, **kwargs):
         )
 
     return path(
-        endpoint["local"], view_proxy, name=f"proxies-api-{service}-{endpoint['local']}"
+        endpoint["local"], view_proxy, name=f"proxies-api-{service}-{endpoint['name']}"
     )
 
 
 def setup(service, patterns):
 
     if service in PROXIED:
-        raise ValueError(f"Proxied api for service {service} already setup")
+        print(f"Proxied api for service {service} already setup")
+        return
 
     PROXIED[service] = patterns
 
diff --git a/src/fullctl/django/rest/views/aaactl_sync.py b/src/fullctl/django/rest/views/aaactl_sync.py
index 41499500..f4a9f438 100644
--- a/src/fullctl/django/rest/views/aaactl_sync.py
+++ b/src/fullctl/django/rest/views/aaactl_sync.py
@@ -91,7 +91,7 @@ class OrganizationUser(CachedObjectMixin, viewsets.GenericViewSet):
     """
 
     path_prefix = "/aaactl-sync"
-    serializer_class = Serializers.orguser
+    serializer_class = Serializers.org_user
     queryset = get_user_model().objects.all()
 
     # this approach assumes that aaactl is the only authentication
@@ -99,7 +99,7 @@ class OrganizationUser(CachedObjectMixin, viewsets.GenericViewSet):
     lookup_field = "social_auth__uid"
     lookup_url_kwarg = "remote_id"
 
-    @grainy_endpoint("aaactl_sync.orguser")
+    @grainy_endpoint("aaactl_sync.org_user")
     def update(self, request, remote_id, *args, **kwargs):
         self.get_object()
         serializer = self.serializer_class(data=request.data)
diff --git a/src/fullctl/django/rest/views/account.py b/src/fullctl/django/rest/views/account.py
index 2ef98d70..581ed609 100644
--- a/src/fullctl/django/rest/views/account.py
+++ b/src/fullctl/django/rest/views/account.py
@@ -22,7 +22,7 @@ class Organization(viewsets.GenericViewSet):
     def list(self, request, *args, **kwargs):
 
         """
-        list the organizations that the user belongs
+        List the organizations that the requesting user belongs
         to or has permissions to
         """
 
@@ -43,5 +43,10 @@ class User(viewsets.GenericViewSet):
     @action(detail=False, methods=["GET"])
     @grainy_endpoint()
     def asns(self, request, org, *args, **kwargs):
+
+        """
+        Lists the asns that the requesting user has been verified for.
+        """
+
         serializer = Serializers.asn(verified_asns(request.perms), many=True)
         return Response(serializer.data)
diff --git a/src/fullctl/django/rest/views/service_bridge.py b/src/fullctl/django/rest/views/service_bridge.py
index d14a99ca..9cfb908b 100644
--- a/src/fullctl/django/rest/views/service_bridge.py
+++ b/src/fullctl/django/rest/views/service_bridge.py
@@ -4,8 +4,13 @@
 from rest_framework import viewsets
 from rest_framework.response import Response
 
+from fullctl.django.models import Instance, Organization
 from fullctl.django.rest.core import BadRequest
 from fullctl.django.rest.decorators import grainy_endpoint
+from fullctl.django.rest.serializers.service_bridge import (
+    HeartbeatSerializer,
+    StatusSerializer,
+)
 
 
 class MethodFilter:
@@ -24,20 +29,32 @@ class SystemViewSet(viewsets.GenericViewSet):
 
 
 class HeartbeatViewSet(SystemViewSet):
+
     ref_tag = "heartbeat"
+    serializer_class = HeartbeatSerializer
 
     @grainy_endpoint("service_bridge.system")
     def list(self, request):
+        """
+        Service heart-beat, check if the service is alive and responding
+        """
+
         return Response({"status": "ok"})
 
 
 class StatusViewSet(SystemViewSet):
+
     ref_tag = "status"
+    serializer_class = StatusSerializer
     checks = []
 
     @grainy_endpoint("service_bridge.system")
     def list(self, request):
 
+        """
+        Returns service bridge status for all the service bridges
+        in use
+        """
         results = {}
 
         for check in self.checks:
@@ -74,6 +91,11 @@ def check_bridge_ixctl(self, request):
 
         return ixctl.Ixctl(cache_duration=1).heartbeat()
 
+    def check_bridge_devicectl(self, request):
+        import fullctl.service_bridge.devicectl as devicectl
+
+        return devicectl.Ixctl(cache_duration=1).heartbeat()
+
 
 class DataViewSet(viewsets.ModelViewSet):
     valid_filters = []
@@ -154,3 +176,57 @@ def join_relations(self, qset, request):
             qset = qset.select_related(*field)
 
         return qset, join
+
+    def prepare_write_data(self, request):
+
+        data = request.data.copy()
+        org_slug = data.get("org")
+        if org_slug:
+            org = Organization.objects.get(slug=org_slug)
+            data["instance"] = Instance.get_or_create(org).id
+
+        return data
+
+    @grainy_endpoint("service_bridge")
+    def create(self, request, *args, **kwargs):
+
+        data = self.prepare_write_data(request)
+
+        serializer = self.serializer_class(data=data)
+        if not serializer.is_valid():
+            return BadRequest(serializer.errors)
+
+        obj = serializer.save()
+
+        if hasattr(self, "after_create"):
+            self.after_create(obj, data)
+
+        return Response(serializer.data)
+
+    @grainy_endpoint("service_bridge")
+    def partial_update(self, request, pk, *args, **kwargs):
+
+        data = self.prepare_write_data(request)
+        instance = self.get_object()
+
+        serializer = self.serializer_class(instance, data=data, partial=True)
+
+        if not serializer.is_valid():
+            return BadRequest(serializer.errors)
+
+        serializer.save()
+
+        if hasattr(self, "after_update"):
+            self.after_update(instance, data)
+
+        return Response(serializer.data)
+
+    @grainy_endpoint("service_bridge")
+    def destroy(self, request, pk, *args, **kwargs):
+        instance = self.get_object()
+        serializer = self.serializer_class(instance)
+        response = Response(serializer.data)
+
+        instance.delete()
+
+        return response
diff --git a/src/fullctl/django/rest/views/template.py b/src/fullctl/django/rest/views/template.py
new file mode 100644
index 00000000..34e0df67
--- /dev/null
+++ b/src/fullctl/django/rest/views/template.py
@@ -0,0 +1,35 @@
+import os
+
+from rest_framework import viewsets
+
+from fullctl.django.rest.mixins import CachedObjectMixin, OrgQuerysetMixin
+
+
+class TemplateRenderView(CachedObjectMixin, OrgQuerysetMixin, viewsets.GenericViewSet):
+    def _render(self, request, instance, type, pk, *args, **kwargs):
+
+        model = self.queryset.model
+
+        if pk == "default":
+            body = request.data.get("body")
+
+            tmpl = model(name="Default", instance=instance, type=type)
+
+            if not body:
+                path = os.path.join(tmpl.template_loader_paths[0], tmpl.template_path)
+                with open(path) as fh:
+                    body = fh.read()
+
+            tmpl.body = body
+
+            tmpl.full_clean()
+        else:
+            tmpl = self.queryset.get(id=pk)
+
+        context = kwargs.get("context")
+        if context:
+            for k, v in context.items():
+                tmpl.context[k] = v
+
+        serializer = self.serializer_class(instance=tmpl)
+        return serializer
diff --git a/src/fullctl/django/rest/views/usage.py b/src/fullctl/django/rest/views/usage.py
index 21ac409b..7792f770 100644
--- a/src/fullctl/django/rest/views/usage.py
+++ b/src/fullctl/django/rest/views/usage.py
@@ -14,10 +14,6 @@
 @route
 class Usage(CachedObjectMixin, OrgQuerysetMixin, viewsets.GenericViewSet):
 
-    """
-    Manage user's organizations
-    """
-
     serializer_class = Serializers.usage
     queryset = models.Organization.objects.all()
 
@@ -27,8 +23,7 @@ class Usage(CachedObjectMixin, OrgQuerysetMixin, viewsets.GenericViewSet):
     def list(self, request, org, *args, **kwargs):
 
         """
-        list the organizations that the user belongs
-        to or has permissions to
+        List organization's usage for metered apis
         """
 
         data = []
diff --git a/src/fullctl/django/settings/__init__.py b/src/fullctl/django/settings/__init__.py
index 1ee242b7..f0aab36a 100644
--- a/src/fullctl/django/settings/__init__.py
+++ b/src/fullctl/django/settings/__init__.py
@@ -172,6 +172,11 @@ def set_default_v1(self):
         self.set_option("GOOGLE_ANALYTICS_ID", "")
         self.set_option("CLOUDFLARE_ANALYTICS_ID", "")
 
+        self.set_option(
+            "SERVICE_APP_DIR",
+            os.path.join(self.scope["BASE_DIR"], "main", f"django_{service_tag}"),
+        )
+
         # eval from default.py file
         filename = os.path.join(os.path.dirname(__file__), "default.py")
         self.try_include(filename)
@@ -267,24 +272,34 @@ def set_default_append(self):
 
     def set_service_bridges(self):
         # no default so we error sooner
-        self.set_option("AAACTL_HOST", "")
-        self.set_option("PDBCTL_HOST", "")
-        self.set_option("PEERCTL_HOST", "")
-        self.set_option("IXCTL_HOST", "")
-
-    def set_twentyc_oauth(self, AAACTL_HOST=None):
-        if not AAACTL_HOST:
-            AAACTL_HOST = self.get("AAACTL_HOST")
+        self.set_option("AAACTL_URL", "")
+        self.set_option("PDBCTL_URL", "")
+        self.set_option("PEERCTL_URL", "")
+        self.set_option("PREFIXCTL_URL", "")
+        self.set_option("IXCTL_URL", "")
+        self.set_option("DEVICECTL_URL", "")
+
+    def set_twentyc_social_oauth(self, AAACTL_URL=None):
+        """
+        This function sets the variables required to OAuth against aaactl using
+        django-social-auth. It does not set the SOCIAL_AUTH_PIPELINE or
+        AUTHENTICATION_BACKENDS.
+        """
+        if not AAACTL_URL:
+            # call this separately, incase this wasn't called from set_twentyc_service
+            # TODO - make this unneeded
+            self.set_option("AAACTL_URL", "")
+            AAACTL_URL = self.get("AAACTL_URL")
 
-        self.set_option("OAUTH_TWENTYC_HOST", AAACTL_HOST)
+        self.set_option("OAUTH_TWENTYC_URL", AAACTL_URL)
         self.set_option(
-            "OAUTH_TWENTYC_ACCESS_TOKEN_URL", f"{AAACTL_HOST}/account/auth/o/token/"
+            "OAUTH_TWENTYC_ACCESS_TOKEN_URL", f"{AAACTL_URL}/account/auth/o/token/"
         )
         self.set_option(
-            "OAUTH_TWENTYC_AUTHORIZE_URL", f"{AAACTL_HOST}/account/auth/o/authorize/"
+            "OAUTH_TWENTYC_AUTHORIZE_URL", f"{AAACTL_URL}/account/auth/o/authorize/"
         )
         self.set_option(
-            "OAUTH_TWENTYC_PROFILE_URL", f"{AAACTL_HOST}/account/auth/o/profile/"
+            "OAUTH_TWENTYC_PROFILE_URL", f"{AAACTL_URL}/account/auth/o/profile/"
         )
 
         self.set_option("OAUTH_TWENTYC_KEY", "")
@@ -294,6 +309,11 @@ def set_twentyc_oauth(self, AAACTL_HOST=None):
         self.set_option("SOCIAL_AUTH_TWENTYC_SECRET", self.get("OAUTH_TWENTYC_SECRET"))
         self.set_option("SOCIAL_AUTH_REDIRECT_IS_HTTPS", True)
 
+    def set_twentyc_oauth(self, AAACTL_URL=None):
+        if not AAACTL_URL:
+            AAACTL_URL = self.get("AAACTL_URL")
+        self.set_twentyc_social_oauth(AAACTL_URL)
+
         AUTHENTICATION_BACKENDS = [
             "fullctl.django.social.backends.twentyc.TwentycOAuth2",
             # fall back to local auth
@@ -302,8 +322,8 @@ def set_twentyc_oauth(self, AAACTL_HOST=None):
         self.set_option("AUTHENTICATION_BACKENDS", AUTHENTICATION_BACKENDS)
 
         GRAINY_REMOTE = {
-            "url_load": urljoin(AAACTL_HOST, "grainy/load/"),
-            # "url_get": f"{OAUTH_TWENTYC_HOST}/grainy/get/" + "{}/",
+            "url_load": urljoin(AAACTL_URL, "grainy/load/"),
+            # "url_get": f"{OAUTH_TWENTYC_URL}/grainy/get/" + "{}/",
         }
         self.set_option("GRAINY_REMOTE", GRAINY_REMOTE)
 
@@ -375,3 +395,11 @@ def set_languages_docs(self):
                 base, ext = os.path.splitext(file)
                 if ext == ".md":
                     API_DOC_INCLUDES[base] = os.path.join(API_DOC_PATH, file)
+
+    def set_netom_integration(self):
+        import netom
+
+        self.set_option("NETOM_DIR", os.path.dirname(netom.__file__))
+        self.set_option(
+            "NETOM_TEMPLATE_DIR", os.path.join(NETOM_DIR, "templates", "netom0")
+        )
diff --git a/src/fullctl/django/settings/default.py b/src/fullctl/django/settings/default.py
index ac9fb6a7..543b23aa 100644
--- a/src/fullctl/django/settings/default.py
+++ b/src/fullctl/django/settings/default.py
@@ -88,4 +88,6 @@
     {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
 ]
 
+CSRF_FROM_SESSIONS = True
+
 FULLCTL_ADDON_URLS = []
diff --git a/src/fullctl/django/static/common/20c/twentyc.rest.js b/src/fullctl/django/static/common/20c/twentyc.rest.js
index eb39d93d..e105027c 100644
--- a/src/fullctl/django/static/common/20c/twentyc.rest.js
+++ b/src/fullctl/django/static/common/20c/twentyc.rest.js
@@ -1314,6 +1314,7 @@ twentyc.rest.Select = twentyc.cls.extend(
         $(this.proxy_data).find('option').each(function() {
           select.append($(this).clone());
         });
+        $(this).trigger("load:after", [select, {}, this]);
         return new Promise((resolve, reject) => {
           resolve();
         });
@@ -1732,6 +1733,7 @@ twentyc.rest.List = twentyc.cls.extend(
         var method = ($(this).data("api-method") || "POST").toLowerCase();
         var action = $(this).data("api-action").toLowerCase();
         var callback = $(this).data("api-callback");
+        var callback_name = $(this).data("api-callback");
         var confirm_set = $(this).data("confirm")
 
         if(callback)
@@ -1749,6 +1751,8 @@ twentyc.rest.List = twentyc.cls.extend(
           )
           widget[method](_action, row.data("apiobject")).then(
             callback, widget.action_failure.bind(widget)
+          ).then(
+            $(widget).trigger("api_callback_"+callback_name+":after")
           );
         });
       });
@@ -1800,9 +1804,7 @@ twentyc.rest.List = twentyc.cls.extend(
       Specific to django-rest-framework: we add "ordering" as a query
       parameter to the API calls
       */
-      this.payload = function(){return {ordering: this.ordering}}
-
-      console.log("sort init", this);
+      this.payload = function(){return {ordering: this.ordering};};
     },
 
     sort: function(target, secondary) {
diff --git a/src/fullctl/django/static/common/app.css b/src/fullctl/django/static/common/app.css
index 90b62b73..a0e1186a 100644
--- a/src/fullctl/django/static/common/app.css
+++ b/src/fullctl/django/static/common/app.css
@@ -35,6 +35,10 @@ button span.label {
 }
 
 
+[data-api-submit] {
+  position: relative;
+}
+
 button:focus {
   outline: none;
 }
@@ -411,7 +415,7 @@ div.header .app-switcher .selected {
   padding-bottom: 9px;
   padding-left: 15px;
   padding-right: 28px;
-  width: 210px;
+  width: 220px;
   display: inline-block;
   cursor: pointer;
   position: relative;
@@ -464,6 +468,11 @@ input.slim {
   margin-top: 3px;
 }
 
+.list-body label {
+  font-size: 11px;
+  margin:0px;
+}
+
 
 .list-body + .list-no-data {
   display: none;
@@ -481,6 +490,7 @@ tbody.list-body:empty + tbody.list-no-data {
 }
 
 
+.list-body td input.slim,
 .list-body div.secondary input[type="text"],
 div.toolbar-field input[type="text"] {
   border: 1px solid;
@@ -575,6 +585,14 @@ div.menu .button span.label {
   text-decoration: underline;
 }
 
+.shift-down .menu-deco-border {
+  margin-top: 55px;
+}
+
+.shift-down .menu .section {
+  padding-top: 55px;
+}
+
 div.toolbar .button {
   border: none;
   border-radius: 25px;
@@ -785,7 +803,7 @@ div.menu div.menu-deco-border {
   position: absolute;
   height: 2px;
   left: 15px;
-  right: -15px;
+  right: -12px;
   z-index: 10;
   top: 0px;
 }
@@ -823,6 +841,12 @@ div.list-th,
 .table td,
 .table th {
   border-top: none;
+  padding: 12px;
+}
+
+.table th {
+  vertical-align: bottom;
+  border-bottom: 2px solid #dee2e6;
 }
 
 .tool {
@@ -861,6 +885,11 @@ div.list-th,
   --margin-top: -12px;
   --margin-bottom: -12px;
 }
+
+.marg-top-xl {
+  margin-top: 55px;
+}
+
 td.property {
   padding: 0px 15px;
 }
@@ -875,7 +904,7 @@ td.property {
 }
 
 div.list-body .secondary .property {
-  margin-left: -15px;
+  margin-left: calc(var(--bs-gutter-x) * -0.5);
 }
 
 div.list-body .secondary .property .transparent-field,
@@ -902,7 +931,7 @@ div.list-body .secondary button {
 }
 
 tr.secondary > td {
-  padding: 0px 15px;
+  padding: 0px calc(var(--bs-gutter-x) * .5);
 }
 
 .row.double-row > div.field > span {
@@ -1011,12 +1040,6 @@ div.inner-list > div {
   position: relative;
 }
 
-.prefix-container a[data-api-action="delete_prefix"] {
-  position: absolute;
-  right: 0;
-  padding-right: 8px;
-}
-
 .prefix-container a,
 .prefix-container span {
   display: inline-block;
@@ -1083,10 +1106,8 @@ h5.modal-title {
   line-height: 26px;
 }
 
-button.close {
+button.btn-close {
   text-shadow: none;
-  position: relative;
-  top: -40px;
   opacity: 1;
 }
 
@@ -1149,7 +1170,9 @@ div.controls select:disabled {
 
 
 div.controls input[type="text"],
-div.controls input[type="password"] {
+div.controls input[type="text"]:disabled,
+div.controls input[type="password"],
+div.controls input[type="password"]:disabled {
   padding: 6px 8px;
   height: auto;
 }
diff --git a/src/fullctl/django/static/common/app.js b/src/fullctl/django/static/common/app.js
index 0afb3dac..31e2127b 100644
--- a/src/fullctl/django/static/common/app.js
+++ b/src/fullctl/django/static/common/app.js
@@ -13,6 +13,12 @@ fullctl.template = function(name) {
 fullctl.application = {}
 fullctl.widget = {}
 fullctl.formatters = {}
+fullctl.modals = {}
+fullctl.util = {}
+
+fullctl.util.slugify = (txt) => {
+  return txt.toLowerCase().replace(/\s/g, "-").replace(/_/g, "-").replace(/[^a-zA-Z0-9-]/g,"").replace(/-+/g, '-');
+};
 
 fullctl.formatters.pretty_speed = (value) => {
   if(value >= 1000000)
@@ -223,7 +229,6 @@ fullctl.application.Tool = $tc.extend(
 
     menu : function() {
       var menu = this.template("menu")
-      console.log(this);
       this.$e.menu.append(menu);
       return menu
     },
@@ -410,6 +415,26 @@ fullctl.application.Application = $tc.define(
       }
       this.tools = this.$t = {}
       $(fullctl.application).trigger("initialized", [this, id]);
+      $('[data-grainy-remove]').each(function() {
+        $(this).grainy_toggle($(this).data("grainy-remove"), "r");
+      });
+
+
+      $('[data-bs-toggle="tab"]').on('show.bs.tab', function() {
+        window.history.replaceState({}, document.title, $(this).attr("href"));
+      });
+
+
+    },
+
+    autoload_page : function() {
+      var hash = window.location.hash;
+      if(hash) {
+        hash = hash.substr(1);
+        if(this.get_page(hash)) {
+          this.page(hash);
+        }
+      }
     },
 
     tool : function(name, fn) {
@@ -425,14 +450,175 @@ fullctl.application.Application = $tc.define(
           this.$t[i].sync(app);
         }
       }
+
+    },
+
+    /**
+     * Gets the element representing the page in the pages menu.
+     *
+     * @method get_page
+     * @param {String} page Value set for aria-controls
+     */
+    get_page : function(page) {
+      return $('[data-component="pages"]').find('[aria-controls="'+page+'"]');
     },
 
     page : function(page) {
-      $('[data-component="pages"]').find('[aria-controls="'+page+'"]').tab('show');
+      this.get_page(page).tab('show');
+    },
+
+    /**
+     * Makes visible the page in the pages menu so you can navigate to the tab
+     *
+     * @method show_page
+     * @param {String} page Value set for aria-controls
+     */
+    show_page : function(page) {
+      this.get_page(page).show();
+    },
+
+    /**
+     * Hides the page in the pages menu so you can't navigate to the tab
+     *
+     * @method hide_page
+     * @param {String} page Value set for aria-controls
+     */
+    hide_page : function(page) {
+      this.get_page(page).hide();
     }
   }
 );
 
+fullctl.application.ContainerApplication = $tc.extend(
+	"ContainerApplication",
+	{
+    init_container : function(ref_tag, ref_tag_p) {
+
+      this[ref_tag_p] = this.containers = {}
+      this[ref_tag+"_slugs"] = this.container_slugs = {}
+
+      var selector_name = "select_"+ref_tag;
+
+      this.selector_name = selector_name;
+      this.title_base = window.document.title;
+
+      this.$c.toolbar.widget(selector_name, ($e) => {
+        var e = $e[selector_name];
+        var w = new twentyc.rest.Select(e);
+        $(w).on("load:after", (event, element, data) => {
+          var i;
+          for(i = 0; i < data.length; i++) {
+            this.containers[data[i].id] = data[i];
+            this.container_slugs[data[i].id] = data[i].slug;
+          }
+
+          if(data.length == 0) {
+            e.attr('disabled', true);
+            $(this).trigger("no-containers", []);
+            this.permission_ui();
+          } else {
+            e.attr('disabled', false)
+            this.permission_ui();
+          }
+        });
+        return w
+
+      });
+
+      $(this.$c.toolbar.$w[selector_name]).one("load:after", () => {
+
+        if(this.preselect_container) {
+          this[selector_name](this["preselect_"+ref_tag])
+        } else {
+          this.sync();
+          this.sync_url(this.$c.toolbar.$e[selector_name].val());
+          this.sync_title(this.$c.toolbar.$e[selector_name].val());
+        }
+      });
+
+      $(this.$c.toolbar.$e[selector_name]).on("change", () => {
+        this.sync();
+        this.sync_url(this.$c.toolbar.$e[selector_name].val())
+        if(this.$t.settings) { this.$t.settings.sync(); }
+      });
+
+      this[ref_tag] = function() { return this.container(); }.bind(this);
+      this[ref_tag+"_slug"] = function() { return this.container_slug(); }.bind(this);
+      this[ref_tag+"_object"] = function() { return this.container_object(); }.bind(this);
+      this["unload_"+ref_tag] = function() { return this.unload_container(); }.bind(this);
+      this["select_"+ref_tag] = function(id){ return this.select_container(id); }.bind(this);
+      this["refresh_select_"+ref_tag] = function() { return this.refresh_select_container(); }.bind(this);
+
+    },
+
+    permission_ui : function () { return; },
+
+    container : function() {
+      return this.$c.toolbar.$w[this.selector_name].element.val();
+    },
+
+    container_slug : function() {
+      return this.container_slugs[this.container()];
+    },
+
+    container_object: function() {
+      return this.containers[this.container()]
+    },
+
+    unload_container : function(id) {
+      delete this.containers[id];
+      delete this.container_slugs[id];
+    },
+
+    select_container : function(id) {
+      if(id)
+        this.$c.toolbar.$e[this.selector_name].val(id);
+      else {
+        id = this.$c.toolbar.$e[this.selector_name].find('option').val();
+        this.$c.toolbar.$e[this.selector_name].val(id);
+      }
+
+      this.sync();
+      this.sync_url(id);
+      this.sync_title(id);
+    },
+
+    sync_title : function(id) {
+      var container = this.containers[id];
+      if(container)
+        window.document.title = this.title_base + " / " + container.slug;
+      else
+        window.document.title = this.title_base;
+    },
+
+    sync_url: function(id) {
+      var container = this.containers[id];
+      var url = new URL(window.location)
+      if(!container) {
+        $('#no-container-notify').show();
+        url.pathname = `/${fullctl.org.slug}/`
+      } else {
+        url.pathname = `/${fullctl.org.slug}/${container.slug}/`
+        $('#no-container-notify').hide();
+      }
+      window.history.pushState({}, '', url);
+    },
+
+    refresh : function() {
+      return this.refresh_select_container();
+    },
+
+    refresh_select_container : function() {
+      return this.$c.toolbar.$w[this.selector_name].refresh();
+    }
+
+
+
+
+	},
+	fullctl.application.Application
+)
+
 
 fullctl.application.Orgctl = $tc.extend(
   "Orgctl",
@@ -511,6 +697,59 @@ fullctl.application.Orgctl.PermissionsModal = $tc.extend(
   fullctl.application.Modal
 );
 
+fullctl.TemplateSelect = $tc.extend(
+  "TemplateSelect",
+  {
+    load_params : function() {
+      //to override
+    }
+  },
+  twentyc.rest.Select
+)
+
+fullctl.TemplatePreview = $tc.extend(
+  "TemplatePreview",
+  {
+    TemplatePreview: function(jq, select_widget, type) {
+      this.Form(jq);
+      this.select = new select_widget(this.element.find('select'));
+      this.editor = this.element.find('textarea');
+      this.type = type;
+
+      if(type) {
+        this.select.filter = (tmpl) => {
+          return tmpl.type == type;
+        };
+      }
+
+      $(this.select).on("load:after", ()=>{ this.preview();});
+      $(this.select.element).on("change", ()=>{ this.preview();});
+      $(this).on("api-write:success", (ev,ep,data,response) => {
+          this.editor.val(response.first().body);
+      });
+
+      this.select.load();
+    },
+
+    payload : function() {
+      return { type: this.type }
+    },
+
+    preview : function() {
+      var tmpl_id = parseInt(this.select.element.val())
+      if(tmpl_id)
+        var url = this.editor.data("api-preview").replace("tmpl_id", tmpl_id);
+      else
+        var url = this.editor.data("api-preview-default").replace("type", this.type);
+
+      this.base_url = url;
+
+      this.submit();
+    }
+  },
+  twentyc.rest.Form
+);
+
 $.fn.grainy_toggle = function(namespace, level) {
   if(grainy.check(namespace, level)) {
     this.show();
diff --git a/src/fullctl/django/static/common/logos/aclctl-dark.svg b/src/fullctl/django/static/common/logos/aclctl-dark.svg
new file mode 100755
index 00000000..37c6df18
--- /dev/null
+++ b/src/fullctl/django/static/common/logos/aclctl-dark.svg
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="1072px"
+   height="359px"
+   viewBox="0 0 1072 359"
+   version="1.1"
+   id="svg22"
+   sodipodi:docname="aclCtl-Lockup-DarkBG.svg"
+   inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:dc="http://purl.org/dc/elements/1.1/">
+  <defs
+     id="defs26" />
+  <sodipodi:namedview
+     id="namedview24"
+     pagecolor="#ffffff"
+     bordercolor="#000000"
+     borderopacity="0.25"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     showgrid="false"
+     inkscape:zoom="1.1287313"
+     inkscape:cx="839.88102"
+     inkscape:cy="279.07439"
+     inkscape:window-width="2560"
+     inkscape:window-height="1377"
+     inkscape:window-x="3832"
+     inkscape:window-y="-8"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg22" />
+  <title
+     id="title15">aclCtl-Lockup-DarkBG</title>
+  <g
+     id="aaaCtl-Lockup-DarkBG"
+     stroke="none"
+     stroke-width="1"
+     fill="none"
+     fill-rule="evenodd"
+     transform="translate(-100.99835,7.0876036)">
+    <g
+       id="Group-6"
+       transform="translate(0.76,25)">
+      <path
+         d="m 839.6076,233.0972 h 12.728 v 12.728 c -56.236,56.236 -147.412,56.236 -203.648,0 -56.232,-56.236 -56.236,-147.412 0,-203.648 56.236,-56.236 147.412,-56.236 203.648,0 l -12.728,12.728 c -49.128,-49.128 -129.064,-49.128 -178.192,0 -49.128,49.128 -49.124,129.064 0,178.192 49.128,49.128 129.064,49.128 178.192,0"
+         id="Fill-18"
+         fill="#d1ff27" />
+      <path
+         d="m 750.5128,54.044 c -23.048,0 -46.096,8.772 -63.64,26.32 -35.092,35.088 -35.092,92.188 0,127.276 l 12.728,-12.728 c -28.116,-28.116 -28.116,-73.704 0,-101.82 C 713.6608,79.032 732.0848,72 750.5128,72 Z m 76.368,13.592 v 12.728 25.452 h -25.456 -12.728 c -21.056,-21.052 -55.312,-21.052 -76.368,0 -21.056,21.056 -21.056,55.316 0,76.368 21.056,21.056 55.312,21.056 76.368,0 l 38.184,38.184 c -42.18,42.18 -110.56,42.176 -152.736,0 -42.176,-42.176 -42.176,-110.556 0,-152.732 42.176,-42.18 110.56,-42.18 152.736,0 z m 184.852,201.288 h 59.512 V 0 h -59.512 z M 936.9248,59.068 h 49.332 v 46.748 h -49.332 v 93.808 c 0,13.704 5.872,20.36 19.188,20.36 10.96,0 20.748,-2.74 29.364,-7.44 v 47.768 c -12.532,7.44 -27.016,12.136 -46.984,12.136 -36.412,0 -61.08,-14.488 -61.08,-63.036 V 105.816 h -25.056 V 59.068 h 25.056 V 0 h 59.512 z"
+         id="Fill-19"
+         fill="#bcbec0" />
+      <path
+         id="Fill-20"
+         d="m 89.851328,63.722656 c -14.071929,0 -26.385992,1.577314 -37.86914,4.335938 3.649162,6.006096 7.406017,11.946917 11.21289,17.857422 1.217673,1.833657 2.373336,3.70966 3.566406,5.560546 6.319758,-1.021516 12.919418,-1.642578 20.017578,-1.642578 37.243968,0 59.132818,18.432666 59.132818,54.140626 v 6.91211 c -15.42831,-4.36018 -31.15233,-7.54792 -51.769536,-8.28516 0.931662,2.29931 1.849097,4.60405 2.787109,6.90039 1.88683,5.07122 3.597907,10.20001 5.230467,15.35352 17.72314,1.23591 32.53297,4.55899 44.13672,7.91797 v 19.19726 c 0,17.59811 -9.367,32.30658 -23.98828,41.86719 0.6561,2.10104 1.30939,4.20324 2.02734,6.28711 1.25998,3.95664 2.94571,7.88279 4.15821,11.91211 6.90035,-5.12023 12.73585,-10.94974 17.41797,-17.0625 v 29.95117 h 28.4121 V 143.58984 c 0,-52.219944 -30.33271,-79.867184 -84.472652,-79.867184 z m 199.292972,0 c -28.79597,0 -50.29962,6.527729 -72.1836,16.511719 l 8.83203,24.191405 c 18.42799,-8.44799 36.85923,-14.591796 60.28321,-14.591796 37.24396,0 59.13281,18.432666 59.13281,54.140626 v 6.91211 c -17.66398,-4.992 -35.71267,-8.44922 -61.05664,-8.44922 -50.29995,0 -84.85156,22.27381 -84.85156,64.50977 0,41.46795 37.23955,62.20312 74.10351,62.20312 34.93997,0 57.9807,-16.1278 71.80469,-34.17578 v 29.95117 h 28.41211 V 143.58984 c 0,-52.219944 -30.33262,-79.867184 -84.47656,-79.867184 z m 199.29492,0 c -28.79997,0 -50.30352,6.527729 -72.1875,16.511719 l 8.83203,24.191405 c 18.42798,-8.44799 36.86518,-14.591796 60.28516,-14.591796 37.24796,0 59.13086,18.432666 59.13086,54.140626 v 6.91211 c -17.66399,-4.992 -35.70876,-8.44922 -61.05274,-8.44922 -50.30395,0 -84.85547,22.27381 -84.85547,64.50977 0,41.46795 37.23955,62.20312 74.10352,62.20312 34.93996,0 57.9807,-16.1278 71.80469,-34.17578 v 29.95117 h 28.41211 V 143.58984 c 0,-52.219944 -30.33272,-79.867184 -84.47266,-79.867184 z M 43.095469,70.466797 c -8.706032,2.638018 -17.044252,5.941751 -25.431641,9.767578 l 8.832031,24.191405 C 36.532033,99.825868 46.596307,95.957598 57.437266,93.34375 56.866139,92.472078 56.324328,91.580696 55.747813,90.712891 51.438704,84.020776 47.193601,77.287644 43.095469,70.466797 Z m 41.398437,71.980473 c -50.098049,0.10972 -84.494140375,22.36835 -84.494140375,64.5 0,41.46795 37.247458375,62.20312 74.107421375,62.20312 18.9076,0 34.221933,-4.80712 46.583983,-12 -1.03493,-4.8196 -3.38435,-9.57972 -4.73242,-14.14258 -0.54752,-1.59342 -1.03703,-3.20333 -1.54883,-4.80664 -10.096,4.80192 -21.905379,7.52539 -34.541014,7.52539 -26.879973,0 -49.916015,-14.58766 -49.916015,-39.93164 0,-25.34397 20.730552,-41.47265 57.978515,-41.47265 1.634713,0 3.19463,0.0642 4.78711,0.10156 -1.264231,-3.88028 -2.576408,-7.74492 -3.992188,-11.57617 -1.412653,-3.46628 -2.756909,-6.96069 -4.232422,-10.40039 z m 202.734374,21.875 c 24.18798,0 43.7713,4.22717 58.36328,8.45117 v 19.19726 c 0,31.48797 -29.95167,53.75586 -66.43164,53.75586 -26.87197,0 -49.91211,-14.58766 -49.91211,-39.93164 0,-25.34397 20.73251,-41.47265 57.98047,-41.47265 z m 199.29102,0 c 24.18797,0 43.77725,4.22717 58.36523,8.45117 v 19.19726 c 0,31.48797 -29.94777,53.75586 -66.42773,53.75586 -26.87598,0 -49.91602,-14.58766 -49.91602,-39.93164 0,-25.34397 20.73455,-41.47265 57.97852,-41.47265 z" />
+    </g>
+  </g>
+  <text
+     xml:space="preserve"
+     style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:400px;font-family:Archivo;-inkscape-font-specification:'Archivo Medium';white-space:pre;inline-size:0;fill:#ccff00"
+     x="80.62149"
+     y="258.69751"
+     id="text305"
+     transform="matrix(1,0,0,0.90379808,-99.226448,68.674555)"><tspan
+       id="tspan303"
+       x="80.62149"
+       y="258.69751"
+       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:400px;font-family:Roboto;-inkscape-font-specification:Roboto;fill:#ccff00">acl</tspan></text>
+  <metadata
+     id="metadata357">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:title>aclCtl-Lockup-DarkBG</dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+</svg>
diff --git a/src/fullctl/django/static/common/logos/devicectl-dark.svg b/src/fullctl/django/static/common/logos/devicectl-dark.svg
new file mode 100755
index 00000000..98c6fb14
--- /dev/null
+++ b/src/fullctl/django/static/common/logos/devicectl-dark.svg
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="1572px" height="359px" viewBox="0 0 1572 359" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>deviceCtl-Lockup-DarkBG</title>
+    <g id="deviceCtl-Lockup-DarkBG" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="Group-4" transform="translate(0.410000, 10.000000)">
+            <path d="M1339.9484,248.47 L1352.6764,248.47 L1352.6764,261.198 C1296.4404,317.434 1205.2644,317.434 1149.0284,261.198 C1092.7964,204.962 1092.7924,113.786 1149.0284,57.55 C1205.2644,1.314 1296.4404,1.314 1352.6764,57.55 L1339.9484,70.278 C1290.8204,21.15 1210.8844,21.15 1161.7564,70.278 C1112.6284,119.406 1112.6324,199.342 1161.7564,248.47 C1210.8844,297.598 1290.8204,297.598 1339.9484,248.47" id="Fill-12" fill="#D1FF27"></path>
+            <path d="M1250.8536,69.4168 C1227.8056,69.4168 1204.7576,78.1888 1187.2136,95.7368 C1152.1216,130.8248 1152.1216,187.9248 1187.2136,223.0128 L1199.9416,210.2848 C1171.8256,182.1688 1171.8256,136.5808 1199.9416,108.4648 C1214.0016,94.4048 1232.4256,87.3728 1250.8536,87.3728 L1250.8536,69.4168 Z M1327.2216,83.0088 L1327.2216,95.7368 L1327.2216,121.1888 L1301.7656,121.1888 L1289.0376,121.1888 C1267.9816,100.1368 1233.7256,100.1368 1212.6696,121.1888 C1191.6136,142.2448 1191.6136,176.5048 1212.6696,197.5568 C1233.7256,218.6128 1267.9816,218.6128 1289.0376,197.5568 L1327.2216,235.7408 C1285.0416,277.9208 1216.6616,277.9168 1174.4856,235.7408 C1132.3096,193.5648 1132.3096,125.1848 1174.4856,83.0088 C1216.6616,40.8288 1285.0456,40.8288 1327.2216,83.0088 L1327.2216,83.0088 Z M1512.0736,284.2968 L1571.5856,284.2968 L1571.5856,15.3728 L1512.0736,15.3728 L1512.0736,284.2968 Z M1437.2656,74.4408 L1486.5976,74.4408 L1486.5976,121.1888 L1437.2656,121.1888 L1437.2656,214.9968 C1437.2656,228.7008 1443.1376,235.3568 1456.4536,235.3568 C1467.4136,235.3568 1477.2016,232.6168 1485.8176,227.9168 L1485.8176,275.6848 C1473.2856,283.1248 1458.8016,287.8208 1438.8336,287.8208 C1402.4216,287.8208 1377.7536,273.3328 1377.7536,224.7848 L1377.7536,121.1888 L1352.6976,121.1888 L1352.6976,74.4408 L1377.7536,74.4408 L1377.7536,15.3768 L1437.2656,15.3768 L1437.2656,74.4408 Z" id="Fill-13" fill="#BCBEC0"></path>
+            <path d="M643.188,280.296 L672.756,280.296 L672.756,81.784 L643.188,81.784 L643.188,280.296 Z M641.272,38.396 L675.06,38.396 L675.06,5.756 L641.272,5.756 L641.272,38.396 Z M585.196,81.784 L617.068,81.784 L530.676,281.832 L504.564,281.832 L418.556,81.784 L451.192,81.784 L518.008,246.508 L585.196,81.784 Z M100.216,258.028 C62.588,258.028 30.336,228.076 30.336,180.848 C30.336,132.468 61.82,104.056 100.216,104.056 C137.076,104.056 172.016,134.772 172.016,180.848 C172.016,227.312 137.076,258.028 100.216,258.028 L100.216,258.028 Z M170.864,119.8 C155.512,97.528 131.7,77.56 94.844,77.56 C47.228,77.56 1.13686838e-13,115.96 1.13686838e-13,181.236 C1.13686838e-13,246.892 47.228,284.524 94.844,284.524 C131.32,284.524 154.74,264.172 170.864,240.364 L170.864,280.3 L200.432,280.3 L200.432,-1.98951966e-13 L170.864,-1.98951966e-13 L170.864,119.8 Z M870.12,228.844 L888.932,246.508 C868.968,268.78 845.16,284.904 807.912,284.904 C749.936,284.904 706.164,237.676 706.164,181.616 C706.164,125.172 749.936,77.56 807.912,77.56 C845.544,77.56 868.968,93.304 887.396,112.888 L867.816,133.62 C852.072,117.112 834.408,103.672 807.528,103.672 C767.212,103.672 736.496,137.844 736.496,180.848 C736.496,224.236 767.984,258.412 809.452,258.412 C834.792,258.412 854.372,245.74 870.12,228.844 L870.12,228.844 Z M261.112,170.1 C264.952,131.32 291.064,102.52 325.62,102.52 C365.172,102.52 385.904,134.004 388.976,170.1 L261.112,170.1 Z M326.388,77.56 C271.48,77.56 231.164,123.64 231.164,181.236 C231.164,243.052 275.704,284.904 330.228,284.904 C367.856,284.904 392.048,269.932 412.012,247.66 L393.584,231.152 C377.456,248.044 358.26,259.18 330.996,259.18 C296.052,259.18 265.34,234.988 261.112,191.984 L418.54,191.984 C418.924,188.528 418.924,185.456 418.924,182.768 C418.924,124.404 385.136,77.56 326.388,77.56 L326.388,77.56 Z M926.576,170.1 C930.416,131.32 956.524,102.52 991.084,102.52 C1030.632,102.52 1051.364,134.004 1054.44,170.1 L926.576,170.1 Z M1084.388,182.768 C1084.388,124.404 1050.6,77.56 991.848,77.56 C936.944,77.56 896.628,123.64 896.628,181.236 C896.628,243.052 941.168,284.904 995.688,284.904 C1033.32,284.904 1057.512,269.932 1077.48,247.66 L1059.044,231.152 C1042.92,248.044 1023.72,259.18 996.46,259.18 C961.516,259.18 930.8,234.988 926.576,191.984 L1084.008,191.984 C1084.388,188.528 1084.388,185.456 1084.388,182.768 L1084.388,182.768 Z" id="Fill-14" fill="#D1FF27"></path>
+        </g>
+    </g>
+</svg>
\ No newline at end of file
diff --git a/src/fullctl/django/static/common/themes/dark.css b/src/fullctl/django/static/common/themes/dark.css
index ac834aaa..838c9ebf 100644
--- a/src/fullctl/django/static/common/themes/dark.css
+++ b/src/fullctl/django/static/common/themes/dark.css
@@ -1,6 +1,15 @@
+:root {
+  --bs-body-color: #b8bbbc;
+  --bs-body-bg: #191f26;
+}
+
+.nav {
+  --bs-nav-link-hover-color: unset;
+}
+
 body {
-  background-color: #191f26;
-  color: #b8bbbc;
+  background-color: var(--bs-body-bg);
+  color: var(--bs-body-color);
 }
 
 .inactive {
@@ -80,6 +89,14 @@ div.validation-error.non-field-errors {
   background-color: rgba(0,0,0,0.5);
 }
 
+span.icon.fullctl {
+  background-color: #D1FF27;
+}
+.fullctl {
+  color: #D1FF27;
+}
+
+
 .ixctl {
   color: #D1FF27;
 }
@@ -187,6 +204,11 @@ div.list-body .secondary .property [data-field] {
   color: #fff;
 }
 
+.list-body label {
+  color: #D1FF27;
+}
+
+.list-body td input.slim,
 .list-body div.secondary input[type="text"],
 div.toolbar-field input[type="text"] {
   background-color: #191F26;
@@ -251,6 +273,10 @@ div.menu div.section.tabs .active {
   background-color: inherit;
 }
 
+.table th {
+  border-color: #dee2e6;
+}
+
 .modal-body .table tr.sub-row {
   background-color: #181818;
 }
@@ -425,11 +451,11 @@ h5.modal-title {
   color: #fff;
 }
 
-button.close {
+button.btn-close {
   color: #fff !important;
 }
 
-button.close:hover {
+button.btn-close:hover {
   color: #fff !important;
 }
 
@@ -710,7 +736,7 @@ a.nav-link {
 
 span.tag {
   background-color: #29323b;
-  color: #b8bbbc;
+  color: var(--bs-body-color);
 }
 
 span.tag-name {
diff --git a/src/fullctl/django/tasks/orm.py b/src/fullctl/django/tasks/orm.py
index 50cb956e..63c4a521 100644
--- a/src/fullctl/django/tasks/orm.py
+++ b/src/fullctl/django/tasks/orm.py
@@ -39,7 +39,7 @@ def fetch_tasks(limit=1, **filters):
     if filters:
         qset = qset.filter(**filters)
 
-    # order by date of creation
+    # order_history by date of creation
     qset = qset.order_by("created")
 
     # no pending tasks for this task model
diff --git a/src/fullctl/django/tasks/qualifiers.py b/src/fullctl/django/tasks/qualifiers.py
index 7b7c04c7..376ffbe8 100644
--- a/src/fullctl/django/tasks/qualifiers.py
+++ b/src/fullctl/django/tasks/qualifiers.py
@@ -20,7 +20,7 @@ def check(self, task):
 class Setting(Base):
     """
     Requires there to be specific settings value set
-    in order for the task to qualify for processing
+    in order_history for the task to qualify for processing
     on this instance
     """
 
diff --git a/src/fullctl/django/templates/common/apidocs/redoc.html b/src/fullctl/django/templates/common/apidocs/redoc.html
index bfa5ee7f..b026cafc 100644
--- a/src/fullctl/django/templates/common/apidocs/redoc.html
+++ b/src/fullctl/django/templates/common/apidocs/redoc.html
@@ -16,7 +16,7 @@
     </style>
   </head>
   <body>
-    <redoc spec-url='{% static "docs/openapi.yaml" %}'></redoc>
+    <redoc spec-url='{% url "api_schema" %}'></redoc>
     <script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"> </script>
   </body>
 </html>
diff --git a/src/fullctl/django/templates/common/apidocs/swagger.html b/src/fullctl/django/templates/common/apidocs/swagger.html
index 52285576..7e3b2f2b 100644
--- a/src/fullctl/django/templates/common/apidocs/swagger.html
+++ b/src/fullctl/django/templates/common/apidocs/swagger.html
@@ -12,7 +12,7 @@
     <script src="//unpkg.com/swagger-ui-dist@3/swagger-ui-bundle.js"></script>
     <script>
     const ui = SwaggerUIBundle({
-        url: "{% static "docs/openapi.yaml" %}",
+        url: "{% url "api_schema" %}",
         dom_id: '#swagger-ui',
         presets: [
           SwaggerUIBundle.presets.apis,
diff --git a/src/fullctl/django/templates/common/app/base.html b/src/fullctl/django/templates/common/app/base.html
index 5132b502..ba5b9f05 100644
--- a/src/fullctl/django/templates/common/app/base.html
+++ b/src/fullctl/django/templates/common/app/base.html
@@ -48,8 +48,25 @@
 </script>
 
 {% block "header" %}
+
 <div class="container-fluid header-outer">
   <div class="top-border {{ request.app_id }} app_bg"></div>
+
+  {% if request.impersonating %}
+  <div class="row">
+    <div class="col-12 col-sm-2"></div>
+    <div class="col-12 col-sm-8">
+      <div class="alert alert-info center">
+      {% blocktrans with user=request.user logged_in_user=request.impersonating.superuser %}
+      You are currently impersonating <b>{{ user }}</b> - logged in as <b>{{ logged_in_user }}</b>
+      {% endblocktrans %}
+      </div>
+    </div>
+    <div class="col-12 col-sm-2"></div>
+  </div>
+  {% endif %}
+
+
   <div class="row header" data-component="header">
 
     <!-- logo and app switcher -->
@@ -61,10 +78,16 @@
           <img class="app-logo" src="{% static service_logo_dark %}" alt="{{ service_tag }}">
         <span class="icon {{ request.app_id }} icon-caret-down"></span>
         <div class="others" style="display:none">
-          {% for svcapp in service_applications %}
-          {%   if svcapp.slug != service_tag %}
-          {%     with "common/logos/"|add:svcapp.slug|add:"-dark.svg" as other_app_logo %}
-          <a href="{{ svcapp.org_redirect }}"><img class="app-logo" src="{% static other_app_logo %}" alt="{{ svcapp.slug }}"></a>
+          {% for service_app in service_applications %}
+          {%   if service_app.slug != service_tag %}
+          {%     with "common/logos/"|add:service_app.slug|add:"-dark.svg" as other_app_logo %}
+            <a data-grainy-remove="{{ service_app.grainy }}.{{ request.org.remote_id}}" href="{{ service_app.org_redirect }}">
+              {% if not service_app.logo %}
+                <img class="app-logo" src="{% static other_app_logo %}" alt="{{ service_app.slug }}">
+              {% else %}
+                <img class="app-logo" src="{{ service_app.logo }}" alt="{{ service_app.slug }}">
+              {% endif %}
+            </a>
           {%     endwith %}
           {%   endif %}
           {% endfor %}
@@ -96,7 +119,7 @@
           <img src="{% static "common/icons/icon/org.svg" %}">
         </span>
 
-        <button data-element="button_select_org" class="button" type="button"  id="org-menu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+        <button data-element="button_select_org" class="button" type="button"  id="org-menu" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
         {{ request.org.display_name }}
         </button>
         <div class="dropdown-menu dropdown-menu-right" aria-labelledby="org-menu">
@@ -133,7 +156,7 @@
 
           {% if oauth_manages_org %}
 
-          <a class="dropdown-item" href="{{ account_service.urls.create_org }}?service={{ app_id }}">
+          <a class="dropdown-item" href="{{ account_service.urls.create_org }}">
             <span class="icon icon-add"></span>{% trans "Create Organization" %}</a>
           {% endif %}
         </div>
@@ -145,7 +168,7 @@
         <span class="icon">
           <img src="{% static "common/icons/icon/user.svg" %}">
         </span>
-        <button data-element="button_account" class="button" type="button"  id="user-menu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+        <button data-element="button_account" class="button" type="button"  id="user-menu" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
         {{ request.user.username }}
         </button>
         <div class="dropdown-menu dropdown-menu-right" aria-labelledby="user-menu">
diff --git a/src/fullctl/django/templates/common/app/modal.html b/src/fullctl/django/templates/common/app/modal.html
index 380dfc5c..e6ff8139 100644
--- a/src/fullctl/django/templates/common/app/modal.html
+++ b/src/fullctl/django/templates/common/app/modal.html
@@ -4,7 +4,7 @@
     <div class="modal-content">
       <div class="modal-header">
         <h5 class="modal-title" data-element="title"></h5>
-        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
           <span aria-hidden="true">&times;</span>
         </button>
       </div>
@@ -25,7 +25,7 @@ <h5 class="modal-title" data-element="title"></h5>
     <div class="modal-content">
       <div class="modal-header">
         <h5 class="modal-title" data-element="title"></h5>
-        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
           <span aria-hidden="true">&times;</span>
         </button>
       </div>
@@ -42,7 +42,7 @@ <h5 class="modal-title" data-element="title"></h5>
     <div class="modal-content">
       <div class="modal-header">
         <h5 class="modal-title" data-element="title"></h5>
-        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
           <span aria-hidden="true">&times;</span>
         </button>
       </div>
@@ -59,7 +59,7 @@ <h5 class="modal-title" data-element="title"></h5>
     <div class="modal-content">
       <div class="modal-header">
         <h5 class="modal-title" data-element="title"></h5>
-        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
           <span aria-hidden="true">&times;</span>
         </button>
       </div>
@@ -79,7 +79,7 @@ <h5 class="modal-title" data-element="title"></h5>
     <div class="modal-content">
       <div class="modal-header">
         <h5 class="modal-title" data-element="title"></h5>
-        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
           <span aria-hidden="true">&times;</span>
         </button>
       </div>
@@ -100,7 +100,7 @@ <h5 class="modal-title" data-element="title"></h5>
     <div class="modal-content">
       <div class="modal-header">
         <h5 class="modal-title" data-element="title"></h5>
-        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
           <span aria-hidden="true">&times;</span>
         </button>
       </div>
@@ -125,7 +125,7 @@ <h5 class="modal-title" data-element="title"></h5>
     <div class="modal-content">
       <div class="modal-header">
         <h5 class="modal-title"></h5>
-        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
           <span aria-hidden="true">&times;</span>
         </button>
       </div>
diff --git a/src/fullctl/django/templates/common/auth/login.html b/src/fullctl/django/templates/common/auth/login.html
index 51f0cd91..dfa594d3 100644
--- a/src/fullctl/django/templates/common/auth/login.html
+++ b/src/fullctl/django/templates/common/auth/login.html
@@ -12,7 +12,7 @@
     <div class="container-fluid header-outer">
         <div class="top-border {{ sertvice_tag }} app_bg"></div>
         <div class="row header">
-            <div class="col-12 col-sm-6 header-left mt-3 mb-2 ml-3 app-switcher">
+            <div class="col-12 col-sm-6 header-left mt-3 mb-2 ms-3 app-switcher">
                 <img class="app-logo" src="{% static service_logo_dark %}" alt="{{ sertvice_tag }}">
             </div>
         </div>
diff --git a/src/fullctl/django/templates/common/base.html b/src/fullctl/django/templates/common/base.html
index 15470f1a..bfbae447 100644
--- a/src/fullctl/django/templates/common/base.html
+++ b/src/fullctl/django/templates/common/base.html
@@ -9,9 +9,9 @@
 
     {% block "js" %}
     <script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.4.0/js/tether.min.js" integrity="sha384-DztdAPBWPRXSA/3eYEEUWrWCy7G5KFbe8fFjk5JAIxUYHKkDx6Qin1DkWx51bBrb" crossorigin="anonymous"></script>
-    <script src="https://unpkg.com/popper.js/dist/umd/popper.min.js" crossorigin="anonymous"></script>
-    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.4.7/js/tether.min.js" integrity="sha512-X7kCKQJMwapt5FCOl2+ilyuHJp+6ISxFTVrx+nkrhgplZozodT9taV2GuGHxBgKKpOJZ4je77OuPooJg9FJLvw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
+    <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.5/dist/umd/popper.min.js" integrity="sha384-Xe+8cL9oJa6tN/veChSP7q+mnSPaj5Bcu9mPX5F5xIGE0DVittaqT5lorf0EI7Vk" crossorigin="anonymous"></script>
+    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.min.js" integrity="sha384-ODmDIVzN+pFdexxHEHFBQH3/9/vQ9uori45z4JjnFsRydbmQbmL5t1tQ0culUzyK" crossorigin="anonymous"></script>
     <script src="{% static "common/20c/twentyc.core.min.js" %}"></script>
     <script src="{% static "common/20c/twentyc.rest.js" %}"></script>
     <script src="{% static "grainy/grainy.js" %}"></script>
@@ -29,7 +29,7 @@
     {% endblock %}
     {% block "css" %}
     <link href="https://fonts.googleapis.com/css?family=Archivo+Narrow" rel="stylesheet">
-    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
+    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous">
     {% endblock %}
   </head>
   <body>
diff --git a/src/fullctl/django/urls.py b/src/fullctl/django/urls.py
index 70f80ae8..6efaa876 100644
--- a/src/fullctl/django/urls.py
+++ b/src/fullctl/django/urls.py
@@ -6,10 +6,11 @@
 from django.views.generic import TemplateView
 
 import fullctl.django.views
+from fullctl.django.views.api_schema import api_schema
 
 urlpatterns = []
 
-if getattr(settings, "PDBCTL_HOST", None):
+if getattr(settings, "PDBCTL_URL", None):
 
     import fullctl.django.autocomplete.pdb
 
@@ -43,6 +44,7 @@
 
 urlpatterns += [
     path("_diag", fullctl.django.views.diag),
+    path("apidocs/schema.json", api_schema, name="api_schema"),
     path(
         "api/account/",
         include(
diff --git a/src/fullctl/django/validators.py b/src/fullctl/django/validators.py
index 1cbfa10e..d7e70f80 100644
--- a/src/fullctl/django/validators.py
+++ b/src/fullctl/django/validators.py
@@ -1,3 +1,4 @@
+import ipaddress
 import re
 
 from django.core.exceptions import ValidationError
@@ -41,3 +42,17 @@ def validate_alphanumeric_list(value):
 
     for item in value:
         validate_alphanumeric(item)
+
+
+def ip_address_string(value):
+    """
+    Takes an ip address string and returns a validated normalized
+    ip address string.
+
+    This will strep the net mask if it is provided
+    """
+
+    if not value:
+        return value
+    value = str(value)
+    return str(ipaddress.ip_interface(value))
diff --git a/src/fullctl/django/views.py b/src/fullctl/django/views/__init__.py
similarity index 96%
rename from src/fullctl/django/views.py
rename to src/fullctl/django/views/__init__.py
index 2294b3e6..88c22bb1 100644
--- a/src/fullctl/django/views.py
+++ b/src/fullctl/django/views/__init__.py
@@ -38,7 +38,7 @@ def login(request):
 
 
 def logout(request):
-    response = redirect(f"{settings.AAACTL_HOST}/account/auth/logout/")
+    response = redirect(f"{settings.AAACTL_URL}/account/auth/logout/")
     request.session.delete()
     return response
 
diff --git a/src/fullctl/django/views/api_schema.py b/src/fullctl/django/views/api_schema.py
new file mode 100644
index 00000000..372887c0
--- /dev/null
+++ b/src/fullctl/django/views/api_schema.py
@@ -0,0 +1,7 @@
+from django.http import JsonResponse
+
+from fullctl.django.rest.api_schema import SchemaGenerator
+
+
+def api_schema(request):
+    return JsonResponse(SchemaGenerator(title="fullCtl API").get_schema())
diff --git a/src/fullctl/django/views/template.py b/src/fullctl/django/views/template.py
new file mode 100644
index 00000000..7fa9657f
--- /dev/null
+++ b/src/fullctl/django/views/template.py
@@ -0,0 +1,19 @@
+import os
+
+from django.http import HttpResponse
+from django.views import View
+
+
+class TemplateFileView(View):
+
+    TemplateModel = None
+
+    def get(self, request, template_type):
+        try:
+            tmpl = self.TemplateModel(type=template_type)
+            path = os.path.join(tmpl.template_loader_paths[0], tmpl.template_path)
+            with open(path) as fh:
+                template_text = fh.read()
+        except KeyError:
+            return HttpResponse(status=404)
+        return HttpResponse(template_text)
diff --git a/src/fullctl/service_bridge/aaactl.py b/src/fullctl/service_bridge/aaactl.py
index b3dc9696..ff6c7d0a 100644
--- a/src/fullctl/service_bridge/aaactl.py
+++ b/src/fullctl/service_bridge/aaactl.py
@@ -2,7 +2,7 @@
     from django.conf import settings
 
     DEFAULT_SERVICE_KEY = settings.SERVICE_KEY
-except ImportError:
+except (ImportError, AttributeError):
     DEFAULT_SERVICE_KEY = ""
 
 from fullctl.service_bridge.client import Bridge, DataObject
@@ -34,7 +34,7 @@ def __init__(self, key=None, org=None, **kwargs):
         kwargs.setdefault("cache_duration", 10)
         kwargs.setdefault("cache", CACHE)
 
-        super().__init__(settings.AAACTL_HOST, key, org, **kwargs)
+        super().__init__(settings.AAACTL_URL, key, org, **kwargs)
         self.url = f"{self.url}/service-bridge"
 
 
@@ -43,13 +43,19 @@ class ServiceApplicationObject(AaactlEntity):
 
     def for_org(self, org):
         if org:
-            self.org_redirect = self.invite_redirect.format(org=org)
+            self.org_redirect = self.service_url.format(org=org)
         else:
-            self.org_redirect = self.api_host
+            self.org_redirect = self.api_url
         return self
 
 
 class ServiceApplication(Aaactl):
     class Meta(Aaactl.Meta):
-        ref_tag = "svcapp"
+        ref_tag = "service_application"
         data_object_cls = ServiceApplicationObject
+
+
+class User(Aaactl):
+    class Meta(Aaactl.Meta):
+        ref_tag = "user"
+        data_object_cls = AaactlEntity
diff --git a/src/fullctl/service_bridge/client.py b/src/fullctl/service_bridge/client.py
index 4e348d8e..adc07d35 100644
--- a/src/fullctl/service_bridge/client.py
+++ b/src/fullctl/service_bridge/client.py
@@ -4,6 +4,7 @@
 import urllib.parse
 
 import requests
+import requests.exceptions
 
 from fullctl.service_bridge.data import DataObject
 
@@ -39,6 +40,9 @@ class Bridge:
     # responses for the specified duration (seconds)
     cache_duration = 0
 
+    results_key = "data"
+    url_prefix = "data/"
+
     class Meta:
         service = "base"
         ref_tag = "base"
@@ -60,18 +64,21 @@ def __init__(self, host, key, org_slug, **kwargs):
         self.url = f"{host}/api"
         self.org = org_slug
         self.key = key
+        self.host = host
         self.cache = kwargs.get("cache", None)
         self.cache_duration = kwargs.get("cache_duration", 5)
 
     def _data(self, response):
         status = response.status_code
-        if status == 200:
-            return response.json().get("data")
+        if status in [200, 201, 202, 203, 204, 205]:
+            return response.json().get(self.results_key)
         elif status in [401, 403]:
             raise AuthError(self, status)
         elif status in [400]:
+            print(response.json())
             raise ServiceBridgeError(self, 400, data=response.json())
         else:
+            # print(response.content)
             raise ServiceBridgeError(self, status)
 
     def _requests_kwargs(self, **kwargs):
@@ -79,6 +86,7 @@ def _requests_kwargs(self, **kwargs):
             kwargs["headers"].update(self.auth_headers)
         else:
             kwargs["headers"] = self.auth_headers
+
         return kwargs
 
     def cached(self, url, now, params):
@@ -96,7 +104,7 @@ def test_data(self, path, params):
             param_str = ""
         file_path = os.path.join(TEST_DATA_PATH, f"{path.rstrip('/')}{param_str}.json")
         with open(file_path) as fh:
-            return json.load(fh)["data"]
+            return json.load(fh)[self.results_key]
 
     def get(self, endpoint, **kwargs):
         url = f"{self.url}/{endpoint}/"
@@ -128,12 +136,22 @@ def put(self, endpoint, **kwargs):
         url = f"{self.url}/{endpoint}/"
         return self._data(requests.put(url, **self._requests_kwargs(**kwargs)))
 
+    def patch(self, endpoint, **kwargs):
+        url = f"{self.url}/{endpoint}/"
+        return self._data(requests.patch(url, **self._requests_kwargs(**kwargs)))
+
     def delete(self, endpoint, **kwargs):
         url = f"{self.url}/{endpoint}/"
-        return self._data(requests.delete(url, **self._requests_kwargs(**kwargs)))
+        try:
+            return self._data(requests.delete(url, **self._requests_kwargs(**kwargs)))
+        except ServiceBridgeError as exc:
+            if exc.status == 404:
+                pass
+            else:
+                raise
 
     def object(self, id, raise_on_notfound=True, join=None):
-        url = f"data/{self.ref_tag}/{id}"
+        url = f"{self.url_prefix}{self.ref_tag}/{id}"
         params = {}
 
         if join:
@@ -147,7 +165,7 @@ def object(self, id, raise_on_notfound=True, join=None):
             return None
 
     def objects(self, **kwargs):
-        url = f"data/{self.ref_tag}"
+        url = f"{self.url_prefix}{self.ref_tag}"
         for k, v in kwargs.items():
             if isinstance(v, list):
                 kwargs[k] = ",".join([str(a) for a in v])
@@ -155,6 +173,45 @@ def objects(self, **kwargs):
         for row in data:
             yield self.data_object_cls(ref_tag=self.ref_tag, **row)
 
+    def create(self, data):
+        url = f"{self.url_prefix}{self.ref_tag}"
+        data = self.post(url, json=data)
+        return data
+
+    def destroy(self, obj):
+        url = f"{self.url_prefix}{self.ref_tag}/{obj.id}"
+        try:
+            data = self.delete(url)
+            return data
+        except requests.exceptions.JSONDecodeError:
+            return {}
+
+    def update(self, obj, data):
+        url = f"{self.url_prefix}{self.ref_tag}/{obj.id}"
+        data = self.put(url, json=data)
+        return data
+
+    def partial_update(self, obj, data):
+        url = f"{self.url_prefix}{self.ref_tag}/{obj.id}"
+        data = self.patch(url, json=data)
+        return data
+
+    def update_if_changed(self, obj, data):
+
+        diff = {}
+
+        for field, value in data.items():
+            if getattr(obj, field) != value:
+                diff[field] = value
+
+        if not diff:
+            return
+
+        url = f"{self.url_prefix}{self.ref_tag}/{obj.id}"
+        data = self.patch(url, data=diff)
+
+        return data
+
     def first(self, **kwargs):
         for o in self.objects(**kwargs):
             return o
@@ -167,6 +224,12 @@ def status(self):
         data = self.get("system/status")
         return data[0]
 
+    def ux_url(self, id):
+        return None
+
+    def api_url(self, id):
+        return f"{self.url}/{self.url_prefix}{self.ref_tag}/{id}"
+
 
 class AaaCtl(Bridge):
 
@@ -178,13 +241,13 @@ class AaaCtl(Bridge):
 
     def requires_billing(self, product_name):
 
-        sub = self.require_subscription(product_name)
+        subscription = self.require_subscription(product_name)
 
-        if not sub:
+        if not subscription:
             return False
 
-        if not sub["pay"]:
-            for item in sub.get("items"):
+        if not subscription["payment_method"]:
+            for item in subscription.get("items"):
                 if item["name"].lower() == product_name and item["cost"] > 0:
                     return True
 
diff --git a/src/fullctl/service_bridge/devicectl.py b/src/fullctl/service_bridge/devicectl.py
new file mode 100644
index 00000000..68ce4ea2
--- /dev/null
+++ b/src/fullctl/service_bridge/devicectl.py
@@ -0,0 +1,99 @@
+try:
+    from django.conf import settings
+
+    DEFAULT_SERVICE_KEY = settings.SERVICE_KEY
+except ImportError:
+    DEFAULT_SERVICE_KEY = ""
+
+from fullctl.service_bridge.client import Bridge, DataObject
+
+CACHE = {}
+
+
+class DeviceCtlEntity(DataObject):
+    description = "DeviceCtl Object"
+    source = "devicectl"
+
+
+class Devicectl(Bridge):
+
+    """
+    Service bridge to devicectl for data
+    retrieval
+    """
+
+    class Meta:
+        service = "devicectl"
+        ref_tag = "base"
+        data_object_cls = DeviceCtlEntity
+
+    def __init__(self, key=None, org=None, **kwargs):
+        if not key:
+            key = DEFAULT_SERVICE_KEY
+
+        kwargs.setdefault("cache_duration", 10)
+        kwargs.setdefault("cache", CACHE)
+
+        super().__init__(settings.DEVICECTL_URL, key, org, **kwargs)
+        self.url = f"{self.url}/service-bridge"
+
+
+class Device(Devicectl):
+    class Meta(Devicectl.Meta):
+        ref_tag = "device"
+
+    def request_dummy_device(self, org_slug, name, purpose):
+
+        device = self.first(name=name, org_slug=org_slug)
+
+        if device:
+            return device
+
+        device = self.create(
+            {
+                "name": name,
+                "description": f"{purpose}",
+                "org": org_slug,
+                "type": "dummy",
+            }
+        )
+
+        return device[0]
+
+
+class Facility(Devicectl):
+    class Meta(Devicectl.Meta):
+        ref_tag = "facility"
+
+
+class Port(Devicectl):
+    class Meta(Devicectl.Meta):
+        ref_tag = "port"
+
+    def request_dummy_ports(
+        self, org_slug, ports, name_prefix, device_type, member_id_arg="id"
+    ):
+        return self.post(
+            "data/port/request_dummy_ports",
+            json={
+                "org": org_slug,
+                "name_prefix": name_prefix,
+                "device_type": device_type,
+                "ports": ports,
+            },
+        )
+
+
+class PortInfo(Devicectl):
+    class Meta(Devicectl.Meta):
+        ref_tag = "port_info"
+
+
+class VirtualPort(Devicectl):
+    class Meta(Devicectl.Meta):
+        ref_tag = "virtual_port"
+
+
+class IPAddress(Devicectl):
+    class Meta(Devicectl.Meta):
+        ref_tag = "ip"
diff --git a/src/fullctl/service_bridge/ixctl.py b/src/fullctl/service_bridge/ixctl.py
index 78bf4ae0..44f064bf 100644
--- a/src/fullctl/service_bridge/ixctl.py
+++ b/src/fullctl/service_bridge/ixctl.py
@@ -35,7 +35,7 @@ def __init__(self, key=None, org=None, **kwargs):
         kwargs.setdefault("cache_duration", 10)
         kwargs.setdefault("cache", CACHE)
 
-        super().__init__(settings.IXCTL_HOST, key, org, **kwargs)
+        super().__init__(settings.IXCTL_URL, key, org, **kwargs)
         self.url = f"{self.url}/service-bridge"
 
 
diff --git a/src/fullctl/service_bridge/nautobot.py b/src/fullctl/service_bridge/nautobot.py
new file mode 100644
index 00000000..730e2087
--- /dev/null
+++ b/src/fullctl/service_bridge/nautobot.py
@@ -0,0 +1,120 @@
+try:
+    from django.conf import settings
+
+    DEFAULT_NAUTOBOT_TOKEN = settings.NAUTOBOT_TOKEN
+except ImportError:
+    DEFAULT_NAUTOBOT_TOKEN = ""
+
+from fullctl.service_bridge.client import Bridge, DataObject
+
+CACHE = {}
+
+
+class NautobotObject(DataObject):
+    source = "nautobot"
+    description = "Nautobot Object"
+
+
+class Nautobot(Bridge):
+
+    """
+    Service bridge for nautobot data retrieval
+    """
+
+    url_prefix = ""
+    results_key = "results"
+
+    class Meta:
+        service = "nautobot"
+        ref_tag = "base"
+        data_object_cls = NautobotObject
+
+    def __init__(self, key=None, org=None, **kwargs):
+
+        if not key:
+            key = DEFAULT_NAUTOBOT_TOKEN
+
+        kwargs.setdefault("cache_duration", 10)
+        kwargs.setdefault("cache", CACHE)
+
+        super().__init__(settings.NAUTOBOT_URL, key, org, **kwargs)
+
+    @property
+    def auth_headers(self):
+        return {"Authorization": f"Token {self.key}"}
+
+    def ux_url(self, id):
+        return f"{self.host}/{self.ref_tag}/{id}/?tab=main"
+
+
+class DeviceTypeObject(NautobotObject):
+    description = "Nautobot Device Type"
+
+
+class DeviceType(Nautobot):
+    class Meta(Nautobot.Meta):
+        ref_tag = "dcim/device-types"
+        data_object_cls = DeviceTypeObject
+
+
+class DeviceObject(NautobotObject):
+    description = "Nautobot Device"
+
+
+class Device(Nautobot):
+    class Meta(Nautobot.Meta):
+        ref_tag = "dcim/devices"
+        data_object_cls = DeviceObject
+
+
+class InterfaceObject(NautobotObject):
+    description = "Nautobot Interface"
+
+
+class Interface(Nautobot):
+    class Meta(Nautobot.Meta):
+        ref_tag = "dcim/interfaces"
+        data_object_cls = InterfaceObject
+
+
+class IPAddressObject(NautobotObject):
+    description = "Nautobot IP-Address"
+
+
+class IPAddress(Nautobot):
+    class Meta(Nautobot.Meta):
+        ref_tag = "ipam/ip-addresses"
+        data_object_cls = IPAddressObject
+
+
+class SiteObject(NautobotObject):
+    description = "Nautobot Site"
+
+
+class Site(Nautobot):
+    class Meta(Nautobot.Meta):
+        ref_tag = "dcim/sites"
+        data_object_cls = SiteObject
+
+
+class CustomFieldObject(NautobotObject):
+    description = "Nautobot CustomField"
+
+
+class CustomField(Nautobot):
+    class Meta(Nautobot.Meta):
+        ref_tag = "extras/custom-fields"
+        data_object_cls = CustomFieldObject
+
+    def sync(self, fields):
+
+        nautobot_fields = {nbf.name: nbf for nbf in self.objects()}
+
+        for field in fields:
+
+            nautobot_field = nautobot_fields.get(field["name"])
+
+            if not nautobot_field:
+                self.create(field)
+            else:
+                self.update(nautobot_field, field)
diff --git a/src/fullctl/service_bridge/pdbctl.py b/src/fullctl/service_bridge/pdbctl.py
index 9415b383..506206e7 100644
--- a/src/fullctl/service_bridge/pdbctl.py
+++ b/src/fullctl/service_bridge/pdbctl.py
@@ -34,7 +34,7 @@ def __init__(self, key=None, org=None, **kwargs):
         kwargs.setdefault("cache_duration", 10)
         kwargs.setdefault("cache", CACHE)
 
-        super().__init__(settings.PDBCTL_HOST, key, org, **kwargs)
+        super().__init__(settings.PDBCTL_URL, key, org, **kwargs)
         self.url = f"{self.url}/service-bridge"
 
 
@@ -43,6 +43,11 @@ class Meta(Pdbctl.Meta):
         ref_tag = "ix"
 
 
+class Facility(Pdbctl):
+    class Meta(Pdbctl.Meta):
+        ref_tag = "fac"
+
+
 class NetworkObject(PeeringDBEntity):
     description = "PeeringDB net"
 
diff --git a/src/fullctl/service_bridge/peerctl.py b/src/fullctl/service_bridge/peerctl.py
index a6193edc..80491476 100644
--- a/src/fullctl/service_bridge/peerctl.py
+++ b/src/fullctl/service_bridge/peerctl.py
@@ -34,7 +34,7 @@ def __init__(self, key=None, org=None, **kwargs):
         kwargs.setdefault("cache_duration", 10)
         kwargs.setdefault("cache", CACHE)
 
-        super().__init__(settings.PEERCTL_HOST, key, org, **kwargs)
+        super().__init__(settings.PEERCTL_URL, key, org, **kwargs)
         self.url = f"{self.url}/service-bridge"
 
 
diff --git a/src/fullctl/service_bridge/prefixctl.py b/src/fullctl/service_bridge/prefixctl.py
new file mode 100644
index 00000000..b03e5f80
--- /dev/null
+++ b/src/fullctl/service_bridge/prefixctl.py
@@ -0,0 +1,48 @@
+try:
+    from django.conf import settings
+
+    DEFAULT_SERVICE_KEY = settings.SERVICE_KEY
+except ImportError:
+    DEFAULT_SERVICE_KEY = ""
+
+from fullctl.service_bridge.client import Bridge, DataObject
+
+CACHE = {}
+
+
+class PrefixctlEntity(DataObject):
+    source = "prefixctl"
+    description = "Prefixctl Object"
+
+
+class Prefixctl(Bridge):
+
+    """
+    Service bridge for prefixctl data retrieval
+    """
+
+    class Meta:
+        service = "prefixctl"
+        ref_tag = "base"
+        data_object_cls = PrefixctlEntity
+
+    def __init__(self, key=None, org=None, **kwargs):
+
+        if not key:
+            key = DEFAULT_SERVICE_KEY
+
+        kwargs.setdefault("cache_duration", 10)
+        kwargs.setdefault("cache", CACHE)
+
+        super().__init__(settings.PREFIXCTL_URL, key, org, **kwargs)
+        self.url = f"{self.url}/service-bridge"
+
+
+class PrefixSetObject(PrefixctlEntity):
+    description = "PrefixCtl PrefixSet"
+
+
+class PrefixSet(Prefixctl):
+    class Meta(Prefixctl.Meta):
+        ref_tag = "prefix_set"
+        data_object_cls = PrefixSetObject
diff --git a/src/fullctl/service_bridge/sot.py b/src/fullctl/service_bridge/sot.py
index 7299b1dd..c02866fa 100644
--- a/src/fullctl/service_bridge/sot.py
+++ b/src/fullctl/service_bridge/sot.py
@@ -13,12 +13,16 @@
 
 SOURCE_MAP = {
     "member": {"pdbctl": pdbctl.NetworkIXLan, "ixctl": ixctl.InternetExchangeMember},
-    "portinfo": {"pdbctl": pdbctl.NetworkIXLan, "ixctl": ixctl.InternetExchangeMember},
+    "port_info": {"pdbctl": pdbctl.NetworkIXLan, "ixctl": ixctl.InternetExchangeMember},
     "ix": {"pdbctl": pdbctl.InternetExchange, "ixctl": ixctl.InternetExchange},
     "as_set": {"pdbctl": pdbctl.Network, "peerctl": peerctl.Network},
 }
 
 
+class ReferenceNotSetError(ValueError):
+    pass
+
+
 class ReferenceNotFoundError(KeyError):
     pass
 
@@ -72,6 +76,8 @@ def ref_bridge(cls, source):
     @property
     def ref_parts(self):
         """Return reference source name and id as a tuple"""
+        if not self.ref_id:
+            raise ReferenceNotSetError()
         src, _id = self.ref_id.split(":")
         return (src, int(_id))
 
@@ -113,10 +119,18 @@ class SourceOfTruth:
 
     def object(self, *args, **kwargs):
         for source, params in self.sources:
+
+            client = source()
+
+            # source host not specified, skip
+            # TODO: error when no source hosts are specified ?
+            if not client.host:
+                continue
+
             kwargs.update(params)
             kwargs["raise_on_notfound"] = False
             try:
-                return source().object(*args, **kwargs)
+                return client.object(*args, **kwargs)
             except ServiceBridgeError as exc:
                 if exc.status == 404:
                     continue
@@ -129,9 +143,17 @@ def objects(self, **kwargs):
         _result = []
 
         for source, params in self.sources:
+
+            client = source()
+
+            # source host not specified, skip
+            # TODO: error when no source hosts are specified ?
+            if not client.host:
+                continue
+
             kwargs.update(params)
             try:
-                for obj in source().objects(**kwargs):
+                for obj in client.objects(**kwargs):
                     _result.append(obj)
 
             except ServiceBridgeError as exc:
diff --git a/src/fullctl/utils/__init__.py b/src/fullctl/utils/__init__.py
new file mode 100644
index 00000000..4cda9c42
--- /dev/null
+++ b/src/fullctl/utils/__init__.py
@@ -0,0 +1,13 @@
+import functools
+
+
+def rgetattr(obj, attr, *args):
+    def _getattr(obj, attr):
+        return getattr(obj, attr, *args)
+
+    return functools.reduce(_getattr, [obj] + attr.split("."))
+
+
+def rsetattr(obj, attr, val):
+    pre, _, post = attr.rpartition(".")
+    return setattr(rgetattr(obj, pre) if pre else obj, post, val)
diff --git a/tox.ini b/tox.ini
index b386b7bd..79a57caa 100644
--- a/tox.ini
+++ b/tox.ini
@@ -18,7 +18,7 @@ per-file-ignores =
     tests/django_tests/testdev.py: F821
 
 [tox]
-envlist = py{37,38,39}-{django2,django3}
+envlist = py{37,38,39,310}-{django2,django3}
 
 # Need isolated build if we're using Poetry as well
 # https://tox.readthedocs.io/en/latest/example/package.html
@@ -30,20 +30,18 @@ python =
     3.7: py37
     3.8: py38
     3.9: py39
+    3.10: py310
 
 [pytest]
-pythonpaths = tests/django_tests
 norecursedirs = .ctl data gen .tox
 
 [testenv]
-extras = dev 
+extras = dev
 deps =
   poetry
   django2: django>=2.2,<3
   django3: django>=3.2,<4
 
-commands = 
+commands =
   poetry install -v
-  poetry run pytest -vs --cov={envsitepackagesdir}/fullctl --cov-report=xml tests/ 
-
-
+  poetry run pytest -vs --cov={toxinidir}/src --cov-report=term-missing --cov-report=xml tests/


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 95eb0d93..a358064d 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -8,49 +8,28 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v2
-      - uses: actions/setup-python@v2
-        with:
-          python-version: 3.9
-      - name: Install Poetry
-        uses: snok/install-poetry@v1
-        with:
-          virtualenvs-create: true
-          virtualenvs-in-project: true
-          installer-parallel: true
-
-          # virtualenvs-path: ~/.venv
-      - name: Load cached venv
-        id: cached-poetry-dependencies
-        uses: actions/cache@v2
-        with:
-          path: .venv
-          key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }}
-      # install dependencies if cache does not exist
-      - name: Check cache and install dependencies
-        run: poetry install --no-interaction --no-root
-        if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
-      # poetry run pre-commit run --all-files
+      - name: Install virtualenv from poetry
+        uses: 20c/workflows/poetry@v1
       - name: Run linters
         run: |
-            poetry run isort src/
-            poetry run black --check src/
-            poetry run pre-commit run --all-files
-
+          poetry run isort .
+          poetry run black --check src/
+          poetry run pre-commit run --all-files
 
   test:
-# XXX    needs: linting
+    needs: linting
     strategy:
-      fail-fast: false
+      fail-fast: true
       matrix:
         os: [ "ubuntu-latest", "macos-latest" ]
-        python-version: [ "3.7", "3.8", "3.9" ]
+        python-version: [ "3.7", "3.8", "3.9", "3.10" ]
         #django-version: [ "django>=2.2,<3", "django>=3.1,<4" ]
     runs-on: ${{ matrix.os }}
     steps:
       - name: Check out repository
         uses: actions/checkout@v2
-      - name: Set up python ${{ matrix.python-version }}
-        uses: actions/setup-python@v2
+      - name: Install virtualenv from poetry
+        uses: 20c/workflows/poetry@v1
         with:
           python-version: ${{ matrix.python-version }}
       - name: Install dependencies
@@ -60,7 +39,6 @@ jobs:
       - name: Run tests
         run: tox
       - name: Upload coverage
-        uses: codecov/codecov-action@v1
+        uses: codecov/codecov-action@v3
         with:
-          file: ./coverage.xml
           fail_ci_if_error: true
diff --git a/tests/data/aaactl/api/service-bridge/data/svcapp/group%3Dfullctl.json b/tests/data/aaactl/api/service-bridge/data/service_application/group%3Dfullctl.json
similarity index 100%
rename from tests/data/aaactl/api/service-bridge/data/svcapp/group%3Dfullctl.json
rename to tests/data/aaactl/api/service-bridge/data/service_application/group%3Dfullctl.json
diff --git "a/tests/data/aaactl/api/service-bridge/data/svcapp/group\\%3Dfullctl.json" "b/tests/data/aaactl/api/service-bridge/data/service_application/group\\%3Dfullctl.json"
similarity index 100%
rename from "tests/data/aaactl/api/service-bridge/data/svcapp/group\\%3Dfullctl.json"
rename to "tests/data/aaactl/api/service-bridge/data/service_application/group\\%3Dfullctl.json"
diff --git a/tests/django_tests/project/settings.py b/tests/django_tests/project/settings.py
index 4ab188d3..0e9435ca 100644
--- a/tests/django_tests/project/settings.py
+++ b/tests/django_tests/project/settings.py
@@ -172,8 +172,8 @@
     },
 }
 
-OAUTH_TWENTYC_HOST = "localhost"
+OAUTH_TWENTYC_URL = "localhost"
 SERVICE_TAG = "fullctl"
 SERVICE_KEY = "s3cr3t"
-AAACTL_HOST = "test://aaactl"
+AAACTL_URL = "test://aaactl"
 SUPPORT_EMAIL = "support@localhost"
diff --git a/tests/django_tests/test_context_processors.py b/tests/django_tests/test_context_processors.py
index a3ac68ba..fe66b0f3 100644
--- a/tests/django_tests/test_context_processors.py
+++ b/tests/django_tests/test_context_processors.py
@@ -12,7 +12,7 @@ def test_account_service(db, dj_account_objects, settings):
 
     expected = {
         "urls": {
-            "create_org": "localhost/account/org/create/",
+            "create_org": "localhost/account/",
             "manage_org": "localhost/account/?org=test",
             "billing_setup": "localhost/billing/setup?org=test",
         }
@@ -29,7 +29,7 @@ def test_account_service_no_org(db, dj_account_objects, settings):
 
     expected = {
         "urls": {
-            "create_org": "localhost/account/org/create/",
+            "create_org": "localhost/account/",
             "manage_org": "localhost/account/?org=",
             "billing_setup": "localhost/billing/setup?org=",
         }
diff --git a/tests/django_tests/test_setting.py b/tests/django_tests/test_setting.py
index e4c344fe..4c5aa1f8 100644
--- a/tests/django_tests/test_setting.py
+++ b/tests/django_tests/test_setting.py
@@ -162,21 +162,21 @@ def test_SettingsManager_set_service_bridges():
     settings_manager = settings.SettingsManager(g)
     settings_manager.set_service_bridges()
 
-    assert g["AAACTL_HOST"] == ""
-    assert g["PDBCTL_HOST"] == ""
-    assert g["PEERCTL_HOST"] == ""
-    assert g["IXCTL_HOST"] == ""
+    assert g["AAACTL_URL"] == ""
+    assert g["PDBCTL_URL"] == ""
+    assert g["PEERCTL_URL"] == ""
+    assert g["IXCTL_URL"] == ""
 
 
 def test_SettingsManager_set_twentyc_oauth():
     g = {}
     settings_manager = settings.SettingsManager(g)
-    settings_manager.set_option("AAACTL_HOST", "aaactlhost.com")
+    settings_manager.set_option("AAACTL_URL", "aaactlhost.com")
     settings_manager.set_twentyc_oauth()
     aaactlhost = "aaactlhost.com"
     settings_manager.set_twentyc_oauth(aaactlhost)
 
-    assert g["OAUTH_TWENTYC_HOST"] == aaactlhost
+    assert g["OAUTH_TWENTYC_URL"] == aaactlhost
     assert g["OAUTH_TWENTYC_ACCESS_TOKEN_URL"] == "aaactlhost.com/account/auth/o/token/"
     assert (
         g["OAUTH_TWENTYC_AUTHORIZE_URL"] == "aaactlhost.com/account/auth/o/authorize/"