From 49b2e417732d5f4ee0e9085eca57f480a610919b Mon Sep 17 00:00:00 2001 From: bakebot Date: Tue, 9 Jan 2024 12:08:58 +0000 Subject: [PATCH 1/8] Cookie initialy baked by NetworkToCode Cookie Drift Manager Tool Template: ``` { "template": "https://github.com/nautobot/cookiecutter-nautobot-app.git", "dir": "nautobot-app", "ref": "nautobot-app-v1.2", "path": null } ``` Cookie: ``` { "remote": "https://github.com/nautobot/nautobot-app-design-builder.git", "path": "/opt/ntc/drift-manager/outputs/nautobot-app-design-builder", "repository_path": "/opt/ntc/drift-manager/outputs/nautobot-app-design-builder", "dir": "", "branch_prefix": "drift-manager", "context": { "codeowner_github_usernames": "@abates @mzbroch", "full_name": "Network to Code, LLC", "email": "opensource@networktocode.com", "github_org": "nautobot", "plugin_name": "nautobot_design_builder", "verbose_name": "Nautobot Design Builder", "plugin_slug": "nautobot-design-builder", "project_slug": "nautobot-app-design-builder", "repo_url": "https://github.com/nautobot/nautobot-app-design-builder", "base_url": "design-builder", "min_nautobot_version": "1.6.8", "max_nautobot_version": "2.9999", "camel_name": "NautobotDesignBuilder", "project_short_description": "Nautobot app that uses design templates to easily create data objects in Nautobot with minimal input from a user.", "model_class_name": "None", "open_source_license": "Apache-2.0", "docs_base_url": "https://docs.nautobot.com", "docs_app_url": "https://docs.nautobot.com/projects/nautobot-design-builder/en/latest", "_template": "https://github.com/nautobot/cookiecutter-nautobot-app.git", "_output_dir": "/opt/ntc/drift-manager/outputs", "_repo_dir": "/opt/ntc/drift-manager/outputs/.cookiecutters/cookiecutter-nautobot-app/nautobot-app", "_checkout": "nautobot-app-v1.2" }, "base_branch": "develop", "remote_name": "origin", "pull_request_strategy": "PullRequestStrategy.CREATE", "post_actions": [ "PostAction.BLACK" ], "baked_commit_ref": "", "draft": false } ``` CLI Arguments: ``` { "cookie_dir": "", "input": false, "json_filename": "design-builder.json", "output_dir": "./outputs", "push": true, "template": "https://github.com/nautobot/cookiecutter-nautobot-app.git", "template_dir": "nautobot-app", "template_ref": "nautobot-app-v1.2", "pull_request": null, "post_action": [ "black" ], "disable_post_actions": false, "draft": false } ``` --- .cookiecutter.json | 53 ++- .flake8 | 6 +- .github/ISSUE_TEMPLATE/bug_report.md | 8 +- .github/ISSUE_TEMPLATE/feature_request.md | 4 +- .../pull_request_template.md | 39 +- .github/workflows/ci.yml | 138 +++++-- .github/workflows/rebake.yml | 118 ++++++ .github/workflows/upstream_testing.yml | 13 + .gitignore | 7 +- .yamllint.yml | 1 + LICENSE | 15 + README.md | 54 ++- development/Dockerfile | 107 ++++-- development/development.env | 3 - development/docker-compose.base.yml | 28 +- development/docker-compose.dev.yml | 10 +- development/docker-compose.mysql.yml | 8 +- development/docker-compose.postgres.yml | 4 +- development/nautobot_config.py | 101 +++-- docs/admin/compatibility_matrix.md | 7 +- docs/admin/install.md | 25 +- docs/admin/release_notes/version_1.0.md | 43 ++- docs/admin/uninstall.md | 15 +- docs/admin/upgrade.md | 7 +- docs/assets/extra.css | 9 + docs/assets/overrides/partials/copyright.html | 2 +- docs/dev/arch_decision.md | 7 + docs/dev/code_reference/api.md | 5 + docs/dev/code_reference/index.md | 3 + docs/dev/code_reference/package.md | 1 + docs/dev/contributing.md | 19 +- docs/dev/dev_environment.md | 119 +++--- docs/dev/extending.md | 39 +- docs/images/icon-nautobot-design-builder.png | Bin 0 -> 74601 bytes docs/requirements.txt | 2 +- docs/user/app_getting_started.md | 26 +- docs/user/app_overview.md | 18 +- docs/user/app_use_cases.md | 12 + docs/user/external_interactions.md | 17 + docs/user/faq.md | 4 - invoke.example.yml | 8 +- invoke.mysql.yml | 8 +- mkdocs.yml | 35 +- nautobot_design_builder/__init__.py | 33 +- nautobot_design_builder/api/__init__.py | 1 + .../migrations/__init__.py | 0 nautobot_design_builder/tests/__init__.py | 80 +--- nautobot_design_builder/tests/test_api.py | 28 ++ nautobot_design_builder/tests/test_basic.py | 34 ++ pyproject.toml | 56 +-- tasks.py | 348 +++++++++++++++--- 51 files changed, 1159 insertions(+), 569 deletions(-) create mode 100644 .github/workflows/rebake.yml create mode 100644 .github/workflows/upstream_testing.yml create mode 100644 LICENSE create mode 100644 docs/dev/arch_decision.md create mode 100644 docs/dev/code_reference/api.md create mode 100644 docs/dev/code_reference/package.md create mode 100644 docs/images/icon-nautobot-design-builder.png create mode 100644 docs/user/app_use_cases.md create mode 100644 docs/user/external_interactions.md create mode 100644 nautobot_design_builder/api/__init__.py create mode 100644 nautobot_design_builder/migrations/__init__.py create mode 100644 nautobot_design_builder/tests/test_api.py create mode 100644 nautobot_design_builder/tests/test_basic.py diff --git a/.cookiecutter.json b/.cookiecutter.json index c842e4ed..ffc1cb8e 100644 --- a/.cookiecutter.json +++ b/.cookiecutter.json @@ -1,22 +1,35 @@ { - "cookiecutter": { - "codeowner_github_usernames": "@abates @mzbroch", - "full_name": "Network to Code, LLC", - "email": "info@networktocode.com", - "github_org": "nautobot", - "plugin_name": "nautobot_design_builder", - "verbose_name": "Nautobot Design Builder", - "plugin_slug": "nautobot-design-builder", - "project_slug": "nautobot-plugin-design-builder", - "repo_url": "https://github.com/nautobot/nautobot-plugin-design-builder", - "base_url": "design-builder", - "min_nautobot_version": "1.6.0", - "max_nautobot_version": "1.9999", - "camel_name": "NautobotDesignBuilder", - "project_short_description": "A Nautobot App that uses design templates to easily create data objects in Nautobot with minimal input from a user.", - "model_class_name": "None", - "open_source_license": "Apache-2.0", - "docs_base_url": "https://docs.nautobot.com", - "docs_app_url": "https://docs.nautobot.com/projects/design-builder/en/latest" - } + "cookiecutter": { + "codeowner_github_usernames": "@abates @mzbroch", + "full_name": "Network to Code, LLC", + "email": "opensource@networktocode.com", + "github_org": "nautobot", + "plugin_name": "nautobot_design_builder", + "verbose_name": "Nautobot Design Builder", + "plugin_slug": "nautobot-design-builder", + "project_slug": "nautobot-app-design-builder", + "repo_url": "https://github.com/nautobot/nautobot-app-design-builder", + "base_url": "design-builder", + "min_nautobot_version": "1.6.8", + "max_nautobot_version": "2.9999", + "camel_name": "NautobotDesignBuilder", + "project_short_description": "Nautobot app that uses design templates to easily create data objects in Nautobot with minimal input from a user.", + "model_class_name": "None", + "open_source_license": "Apache-2.0", + "docs_base_url": "https://docs.nautobot.com", + "docs_app_url": "https://docs.nautobot.com/projects/nautobot-design-builder/en/latest", + "_drift_manager": { + "template": "https://github.com/nautobot/cookiecutter-nautobot-app.git", + "template_dir": "nautobot-app", + "template_ref": "nautobot-app-v1.2", + "cookie_dir": "", + "branch_prefix": "drift-manager", + "pull_request_strategy": "create", + "post_actions": [ + "black" + ], + "draft": false, + "baked_commit_ref": "b205e8e892aa9fa8dd665963cfdc2f30410d8695" + } + } } diff --git a/.flake8 b/.flake8 index 888023fd..c9f5e84d 100644 --- a/.flake8 +++ b/.flake8 @@ -1,7 +1,7 @@ [flake8] -# E501: Line length is enforced by Black, so flake8 doesn't need to check it -# W503: Black disagrees with this rule, as does PEP 8; Black wins -ignore = E501, W503 +ignore = + E501, # Line length is enforced by Black, so flake8 doesn't need to check it + W503 # Black disagrees with this rule, as does PEP 8; Black wins exclude = migrations, __pycache__, diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 0c10ac35..70e517a8 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,12 +1,12 @@ --- name: 🐛 Bug Report -about: Report a reproducible bug in the current release of design-builder +about: Report a reproducible bug in the current release of nautobot-design-builder --- ### Environment -* Python version: -* Nautobot version: -* design-builder version: +* Python version: +* Nautobot version: +* nautobot-design-builder version: ### Expected Behavior diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 28b626df..2501eed7 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -5,8 +5,8 @@ about: Propose a new feature or enhancement --- ### Environment -* Nautobot version: -* design-builder version: +* Nautobot version: +* nautobot-design-builder version: -## Change Notes +# Closes: # -## Justification +## What's Changed + + + +## To Do + + +- [ ] Explanation of Change(s) +- [ ] Added change log fragment(s) (for more information see [the documentation](https://docs.nautobot.com/projects/core/en/stable/development/#creating-changelog-fragments)) +- [ ] Attached Screenshots, Payload Example +- [ ] Unit, Integration Tests +- [ ] Documentation Updates (when adding/changing features) +- [ ] Example Plugin Updates (when adding/changing features) +- [ ] Outline Remaining Work, Constraints from Design diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3db1abc9..a163c061 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ on: # yamllint disable-line rule:truthy rule:comments pull_request: ~ env: - PLUGIN_NAME: "nautobot-plugin-design-builder" + PLUGIN_NAME: "nautobot-app-design-builder" jobs: black: @@ -22,7 +22,7 @@ jobs: INVOKE_NAUTOBOT_DESIGN_BUILDER_LOCAL: "True" steps: - name: "Check out repository code" - uses: "actions/checkout@v2" + uses: "actions/checkout@v4" - name: "Setup environment" uses: "networktocode/gh-action-setup-poetry-environment@v4" - name: "Linting: black" @@ -33,7 +33,7 @@ jobs: INVOKE_NAUTOBOT_DESIGN_BUILDER_LOCAL: "True" steps: - name: "Check out repository code" - uses: "actions/checkout@v2" + uses: "actions/checkout@v4" - name: "Setup environment" uses: "networktocode/gh-action-setup-poetry-environment@v4" - name: "Linting: bandit" @@ -44,12 +44,12 @@ jobs: INVOKE_NAUTOBOT_DESIGN_BUILDER_LOCAL: "True" steps: - name: "Check out repository code" - uses: "actions/checkout@v2" + uses: "actions/checkout@v4" - name: "Setup environment" uses: "networktocode/gh-action-setup-poetry-environment@v4" - name: "Linting: pydocstyle" run: "poetry run invoke pydocstyle" - check-docs-build: + flake8: runs-on: "ubuntu-22.04" env: INVOKE_NAUTOBOT_DESIGN_BUILDER_LOCAL: "True" @@ -58,26 +58,26 @@ jobs: uses: "actions/checkout@v4" - name: "Setup environment" uses: "networktocode/gh-action-setup-poetry-environment@v4" - - name: "Check Docs Build" - run: "poetry run invoke build-and-check-docs" - flake8: + - name: "Linting: flake8" + run: "poetry run invoke flake8" + poetry: runs-on: "ubuntu-22.04" env: INVOKE_NAUTOBOT_DESIGN_BUILDER_LOCAL: "True" steps: - name: "Check out repository code" - uses: "actions/checkout@v2" + uses: "actions/checkout@v4" - name: "Setup environment" uses: "networktocode/gh-action-setup-poetry-environment@v4" - - name: "Linting: flake8" - run: "poetry run invoke flake8" + - name: "Checking: poetry lock file" + run: "poetry run invoke lock --check" yamllint: runs-on: "ubuntu-22.04" env: INVOKE_NAUTOBOT_DESIGN_BUILDER_LOCAL: "True" steps: - name: "Check out repository code" - uses: "actions/checkout@v2" + uses: "actions/checkout@v4" - name: "Setup environment" uses: "networktocode/gh-action-setup-poetry-environment@v4" - name: "Linting: yamllint" @@ -87,6 +87,7 @@ jobs: - "bandit" - "pydocstyle" - "flake8" + - "poetry" - "yamllint" - "black" runs-on: "ubuntu-22.04" @@ -94,44 +95,123 @@ jobs: fail-fast: true matrix: python-version: ["3.11"] - nautobot-version: ["1.6"] + nautobot-version: ["1.6.8"] env: - INVOKE_DESIGN_BUILDER_PYTHON_VER: "${{ matrix.python-version }}" - INVOKE_DESIGN_BUILDER_NAUTOBOT_VER: "${{ matrix.nautobot-version }}" + INVOKE_NAUTOBOT_DESIGN_BUILDER_PYTHON_VER: "${{ matrix.python-version }}" + INVOKE_NAUTOBOT_DESIGN_BUILDER_NAUTOBOT_VER: "${{ matrix.nautobot-version }}" steps: - name: "Check out repository code" - uses: "actions/checkout@v2" + uses: "actions/checkout@v4" - name: "Setup environment" uses: "networktocode/gh-action-setup-poetry-environment@v4" - name: "Set up Docker Buildx" id: "buildx" - uses: "docker/setup-buildx-action@v1" + uses: "docker/setup-buildx-action@v3" + - name: "Build" + uses: "docker/build-push-action@v5" + with: + builder: "${{ steps.buildx.outputs.name }}" + context: "./" + push: false + load: true + tags: "${{ env.PLUGIN_NAME }}/nautobot:${{ matrix.nautobot-version }}-py${{ matrix.python-version }}" + file: "./development/Dockerfile" + cache-from: "type=gha,scope=${{ matrix.nautobot-version }}-py${{ matrix.python-version }}" + cache-to: "type=gha,scope=${{ matrix.nautobot-version }}-py${{ matrix.python-version }}" + build-args: | + NAUTOBOT_VER=${{ matrix.nautobot-version }} + PYTHON_VER=${{ matrix.python-version }} - name: "Copy credentials" run: "cp development/creds.example.env development/creds.env" - name: "Linting: pylint" run: "poetry run invoke pylint" + check-migrations: + needs: + - "bandit" + - "pydocstyle" + - "flake8" + - "poetry" + - "yamllint" + - "black" + runs-on: "ubuntu-22.04" + strategy: + fail-fast: true + matrix: + python-version: ["3.11"] + nautobot-version: ["1.6.8"] + env: + INVOKE_NAUTOBOT_DESIGN_BUILDER_PYTHON_VER: "${{ matrix.python-version }}" + INVOKE_NAUTOBOT_DESIGN_BUILDER_NAUTOBOT_VER: "${{ matrix.nautobot-version }}" + steps: + - name: "Check out repository code" + uses: "actions/checkout@v4" + - name: "Setup environment" + uses: "networktocode/gh-action-setup-poetry-environment@v4" + - name: "Set up Docker Buildx" + id: "buildx" + uses: "docker/setup-buildx-action@v3" + - name: "Build" + uses: "docker/build-push-action@v5" + with: + builder: "${{ steps.buildx.outputs.name }}" + context: "./" + push: false + load: true + tags: "${{ env.PLUGIN_NAME }}/nautobot:${{ matrix.nautobot-version }}-py${{ matrix.python-version }}" + file: "./development/Dockerfile" + cache-from: "type=gha,scope=${{ matrix.nautobot-version }}-py${{ matrix.python-version }}" + cache-to: "type=gha,scope=${{ matrix.nautobot-version }}-py${{ matrix.python-version }}" + build-args: | + NAUTOBOT_VER=${{ matrix.nautobot-version }} + PYTHON_VER=${{ matrix.python-version }} + - name: "Copy credentials" + run: "cp development/creds.example.env development/creds.env" + - name: "Checking: migrations" + run: "poetry run invoke check-migrations" unittest: needs: - "pylint" + - "check-migrations" strategy: fail-fast: true matrix: + python-version: ["3.8", "3.11"] python-version: ["3.8", "3.11"] db-backend: ["postgresql"] nautobot-version: ["1.6", "stable"] include: + - python-version: "3.11" + db-backend: "postgresql" + nautobot-version: "1.6.8" - python-version: "3.11" db-backend: "mysql" - nautobot-version: "1.6" + nautobot-version: "stable" runs-on: "ubuntu-22.04" env: - INVOKE_DESIGN_BUILDER_PYTHON_VER: "${{ matrix.python-version }}" - INVOKE_DESIGN_BUILDER_NAUTOBOT_VER: "${{ matrix.nautobot-version }}" + INVOKE_NAUTOBOT_DESIGN_BUILDER_PYTHON_VER: "${{ matrix.python-version }}" + INVOKE_NAUTOBOT_DESIGN_BUILDER_NAUTOBOT_VER: "${{ matrix.nautobot-version }}" steps: - name: "Check out repository code" - uses: "actions/checkout@v2" + uses: "actions/checkout@v4" - name: "Setup environment" uses: "networktocode/gh-action-setup-poetry-environment@v4" + - name: "Set up Docker Buildx" + id: "buildx" + uses: "docker/setup-buildx-action@v3" + - name: "Build" + uses: "docker/build-push-action@v5" + with: + builder: "${{ steps.buildx.outputs.name }}" + context: "./" + push: false + load: true + tags: "${{ env.PLUGIN_NAME }}/nautobot:${{ matrix.nautobot-version }}-py${{ matrix.python-version }}" + file: "./development/Dockerfile" + cache-from: "type=gha,scope=${{ matrix.nautobot-version }}-py${{ matrix.python-version }}" + cache-to: "type=gha,scope=${{ matrix.nautobot-version }}-py${{ matrix.python-version }}" + build-args: | + NAUTOBOT_VER=${{ matrix.nautobot-version }} + PYTHON_VER=${{ matrix.python-version }} - name: "Copy credentials" run: "cp development/creds.example.env development/creds.env" - name: "Use Mysql invoke settings when needed" @@ -145,8 +225,6 @@ jobs: name: "Publish to GitHub" runs-on: "ubuntu-22.04" if: "startsWith(github.ref, 'refs/tags/v')" - env: - INVOKE_NAUTOBOT_DESIGN_BUILDER_LOCAL: "True" steps: - name: "Check out repository code" uses: "actions/checkout@v4" @@ -160,16 +238,12 @@ jobs: run: "echo RELEASE_VERSION=${GITHUB_REF:10} >> $GITHUB_ENV" - name: "Run Poetry Version" run: "poetry version $RELEASE_VERSION" - - name: "Install Dependencies (needed for mkdocs)" - run: "poetry install --no-root" - - name: "Build Documentation" - run: "poetry run invoke build-and-check-docs" - name: "Run Poetry Build" run: "poetry build" - name: "Upload binaries to release" uses: "svenstaro/upload-release-action@v2" with: - repo_token: "${{ secrets.GH_NAUTOBOT_BOT_TOKEN }}" + repo_token: "${{ secrets.NTC_GITHUB_TOKEN }}" # use GH_NAUTOBOT_BOT_TOKEN for Nautobot Org repos. file: "dist/*" tag: "${{ github.ref }}" overwrite: true @@ -180,8 +254,6 @@ jobs: name: "Push Package to PyPI" runs-on: "ubuntu-22.04" if: "startsWith(github.ref, 'refs/tags/v')" - env: - INVOKE_NAUTOBOT_DESIGN_BUILDER_LOCAL: "True" steps: - name: "Check out repository code" uses: "actions/checkout@v4" @@ -195,10 +267,6 @@ jobs: run: "echo RELEASE_VERSION=${GITHUB_REF:10} >> $GITHUB_ENV" - name: "Run Poetry Version" run: "poetry version $RELEASE_VERSION" - - name: "Install Dependencies (needed for mkdocs)" - run: "poetry install --no-root" - - name: "Build Documentation" - run: "poetry run invoke build-and-check-docs" - name: "Run Poetry Build" run: "poetry build" - name: "Push to PyPI" @@ -223,7 +291,7 @@ jobs: # ENVs cannot be used directly in job.if. This is a workaround to check # if SLACK_WEBHOOK_URL is present. if: "env.SLACK_WEBHOOK_URL != ''" - uses: "slackapi/slack-github-action@v1.17.0" + uses: "slackapi/slack-github-action@v1" with: payload: | { diff --git a/.github/workflows/rebake.yml b/.github/workflows/rebake.yml new file mode 100644 index 00000000..13d1e3a0 --- /dev/null +++ b/.github/workflows/rebake.yml @@ -0,0 +1,118 @@ +--- +name: "Rebake Cookie" +on: # yamllint disable-line rule:truthy + workflow_call: + inputs: + cookie: + description: "The cookie to rebake" + type: "string" + default: "" + draft: + description: "Whether to create the pull request as a draft" + type: "string" + default: "" + pull-request: + description: "The pull request strategy" + type: "string" + default: "" + template: + description: "The template repository URL" + type: "string" + default: "" + template-dir: + description: "The directory within the template repository to use as the template" + type: "string" + default: "" + template-ref: + description: "The branch or tag to use for the template" + type: "string" + default: "" + drift-manager-tag: + description: "The drift manager Docker image tag to use" + type: "string" + default: "latest" + workflow_dispatch: + inputs: + cookie: + description: "The cookie to rebake" + type: "string" + default: "" + draft: + description: "Whether to create the pull request as a draft" + type: "string" + default: "" + pull-request: + description: "The pull request strategy" + type: "string" + default: "" + template: + description: "The template repository URL" + type: "string" + default: "" + template-dir: + description: "The directory within the template repository to use as the template" + type: "string" + default: "" + template-ref: + description: "The branch or tag to use for the template" + type: "string" + default: "" + drift-manager-tag: + description: "The drift manager Docker image tag to use" + type: "string" + default: "latest" +jobs: + rebake: + runs-on: "ubuntu-22.04" + permissions: + actions: "write" + contents: "write" + packages: "read" + pull-requests: "write" + container: "ghcr.io/nautobot/cookiecutter-nautobot-app-drift-manager/prod:${{ github.event.inputs.drift-manager-tag }}" + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + steps: + - name: "Configure Rebake Arguments" + id: "config" + shell: "bash" + run: | + ARGS='--push' + + if [[ '${{ github.event.inputs.draft }}' == 'true' ]]; then + ARGS="$ARGS --draft" + elif [[ '${{ github.event.inputs.draft }}' == 'false' ]]; then + ARGS="$ARGS --no-draft" + elif [[ '${{ github.event.inputs.draft }}' == '' ]]; then + echo "Using repo default value for --draft" + else + echo "ERROR: Invalid value for draft: '${{ github.event.inputs.draft }}'" + exit 1 + fi + + if [[ '${{ github.event.inputs.pull-request }}' != '' ]]; then + ARGS="$ARGS --pull-request='${{ github.event.inputs.pull-request }}'" + fi + + if [[ '${{ github.event.inputs.template }}' != '' ]]; then + ARGS="$ARGS --template='${{ github.event.inputs.template }}'" + fi + + if [[ '${{ github.event.inputs.template-dir }}' != '' ]]; then + ARGS="$ARGS --template-dir='${{ github.event.inputs.template-dir }}'" + fi + + if [[ '${{ github.event.inputs.template-ref }}' != '' ]]; then + ARGS="$ARGS --template-ref='${{ github.event.inputs.template-ref }}'" + fi + + if [[ '${{ github.event.inputs.cookie }}' == '' ]]; then + ARGS="$ARGS '${{ github.repositoryUrl }}'" + else + ARGS="$ARGS '${{ github.event.inputs.cookie }}'" + fi + + echo "args=$ARGS" >> $GITHUB_OUTPUT + - name: "Rebake" + run: | + python -m ntc_cookie_drift_manager rebake ${{ steps.config.outputs.args }} diff --git a/.github/workflows/upstream_testing.yml b/.github/workflows/upstream_testing.yml new file mode 100644 index 00000000..fc1361ed --- /dev/null +++ b/.github/workflows/upstream_testing.yml @@ -0,0 +1,13 @@ +--- +name: "Nautobot Upstream Monitor" + +on: # yamllint disable-line rule:truthy rule:comments + schedule: + - cron: "0 4 */2 * *" # every other day at midnight + +jobs: + upstream-test: + uses: "nautobot/nautobot/.github/workflows/plugin_upstream_testing_base.yml@develop" + with: # Below could potentially be collapsed into a single argument if a concrete relationship between both is enforced + invoke_context_name: "NAUTOBOT_DESIGN_BUILDER" + plugin_name: "nautobot-app-design-builder" diff --git a/.gitignore b/.gitignore index 4a639353..fa6224d1 100644 --- a/.gitignore +++ b/.gitignore @@ -304,6 +304,7 @@ development/*.txt invoke.yml # Docs -docs/README.md -docs/CHANGELOG.md -public \ No newline at end of file +public +/compose.yaml +/dump.sql +/nautobot_design_builder/static/nautobot_design_builder/docs diff --git a/.yamllint.yml b/.yamllint.yml index b49e490c..8cc3e9a9 100644 --- a/.yamllint.yml +++ b/.yamllint.yml @@ -10,3 +10,4 @@ rules: quote-type: "double" ignore: | .venv/ + compose.yaml diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..bf295f49 --- /dev/null +++ b/LICENSE @@ -0,0 +1,15 @@ +Apache Software License 2.0 + +Copyright (c) 2024, Network to Code, LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/README.md b/README.md index ae2a3aeb..f0f50847 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,66 @@ -# Design Builder +# Nautobot Design Builder + +

- +
- - - + + +
An App for Nautobot.

## Overview -Design Builder is a Nautobot application for easily populating data within Nautobot using standardized design files. These design files are just Jinja templates that describe the Nautobot objects to be created or updated. +> Developer Note: Add a long (2-3 paragraphs) description of what the App does, what problems it solves, what functionality it adds to Nautobot, what external systems it works with etc. + +### Screenshots + +> Developer Note: Add any representative screenshots of the App in action. These images should also be added to the `docs/user/app_use_cases.md` section. + +> Developer Note: Place the files in the `docs/images/` folder and link them using only full URLs from GitHub, for example: `![Overview](https://raw.githubusercontent.com/nautobot/nautobot-app-design-builder/develop/docs/images/plugin-overview.png)`. This absolute static linking is required to ensure the README renders properly in GitHub, the docs site, and any other external sites like PyPI. + +More screenshots can be found in the [Using the App](https://docs.nautobot.com/projects/nautobot-design-builder/en/latest/user/app_use_cases/) page in the documentation. Here's a quick overview of some of the plugin's added functionality: + +![](https://raw.githubusercontent.com/nautobot/nautobot-app-design-builder/develop/docs/images/placeholder.png) + +## Try it out! + +> Developer Note: Only keep this section if appropriate. Update link to correct sandbox. + +This App is installed in the Nautobot Community Sandbox found over at [demo.nautobot.com](https://demo.nautobot.com/)! + +> For a full list of all the available always-on sandbox environments, head over to the main page on [networktocode.com](https://www.networktocode.com/nautobot/sandbox-environments/). ## Documentation Full documentation for this App can be found over on the [Nautobot Docs](https://docs.nautobot.com) website: -- [User Guide](user/app_overview.md) - Overview, Using the App, Getting Started. -- [Administrator Guide](admin/install.md) - How to Install, Configure, Upgrade, or Uninstall the App. -- [Developer Guide](dev/contributing.md) - Extending the App, Code Reference, Contribution Guide. -- [Release Notes / Changelog](admin/release_notes/). -- [Frequently Asked Questions](user/faq.md). +- [User Guide](https://docs.nautobot.com/projects/nautobot-design-builder/en/latest/user/app_overview/) - Overview, Using the App, Getting Started. +- [Administrator Guide](https://docs.nautobot.com/projects/nautobot-design-builder/en/latest/admin/install/) - How to Install, Configure, Upgrade, or Uninstall the App. +- [Developer Guide](https://docs.nautobot.com/projects/nautobot-design-builder/en/latest/dev/contributing/) - Extending the App, Code Reference, Contribution Guide. +- [Release Notes / Changelog](https://docs.nautobot.com/projects/nautobot-design-builder/en/latest/admin/release_notes/). +- [Frequently Asked Questions](https://docs.nautobot.com/projects/nautobot-design-builder/en/latest/user/faq/). ### Contributing to the Documentation You can find all the Markdown source for the App documentation under the [`docs`](https://github.com/nautobot/nautobot-app-design-builder/tree/develop/docs) folder in this repository. For simple edits, a Markdown capable editor is sufficient: clone the repository and edit away. -If you need to view the fully-generated documentation site, you can build it with [MkDocs](https://www.mkdocs.org/). A container hosting the documentation can be started using the `invoke` commands (details in the [Development Environment Guide](https://docs.nautobot.com/projects/design-builder/en/latest/dev/dev_environment/#docker-development-environment)) on [http://localhost:8001](http://localhost:8001). Using this container, as your changes to the documentation are saved, they will be automatically rebuilt and any pages currently being viewed will be reloaded in your browser. +If you need to view the fully-generated documentation site, you can build it with [MkDocs](https://www.mkdocs.org/). A container hosting the documentation can be started using the `invoke` commands (details in the [Development Environment Guide](https://docs.nautobot.com/projects/nautobot-design-builder/en/latest/dev/dev_environment/#docker-development-environment)) on [http://localhost:8001](http://localhost:8001). Using this container, as your changes to the documentation are saved, they will be automatically rebuilt and any pages currently being viewed will be reloaded in your browser. Any PRs with fixes or improvements are very welcome! ## Questions -For any questions or comments, please check the [FAQ](https://docs.nautobot.com/projects/design-builder/en/latest/user/faq/) first. Feel free to also swing by the [Network to Code Slack](https://networktocode.slack.com/) (channel `#nautobot`), sign up [here](http://slack.networktocode.com/) if you don't have an account. +For any questions or comments, please check the [FAQ](https://docs.nautobot.com/projects/nautobot-design-builder/en/latest/user/faq/) first. Feel free to also swing by the [Network to Code Slack](https://networktocode.slack.com/) (channel `#nautobot`), sign up [here](http://slack.networktocode.com/) if you don't have an account. diff --git a/development/Dockerfile b/development/Dockerfile index 16241eba..ee399e3c 100644 --- a/development/Dockerfile +++ b/development/Dockerfile @@ -1,52 +1,81 @@ -ARG NAUTOBOT_VER="1.6" -ARG PYTHON_VER=3.8 -FROM ghcr.io/nautobot/nautobot-dev:${NAUTOBOT_VER}-py${PYTHON_VER} +# ------------------------------------------------------------------------------------- +# Nautobot App Developement Dockerfile Template +# Version: 1.1.0 +# +# Apps that need to add additional steps or packages can do in the section below. +# ------------------------------------------------------------------------------------- +# !!! USE CAUTION WHEN MODIFYING LINES BELOW -# Make the value available after the FROM directive -ARG NAUTOBOT_VER -ENV prometheus_multiproc_dir=/prom_cache +# Accepts a desired Nautobot version as build argument, default to 1.6.8 +ARG NAUTOBOT_VER="1.6.8" +# Accepts a desired Python version as build argument, default to 3.11 +ARG PYTHON_VER="3.11" + +# Retrieve published development image of Nautobot base which should include most CI dependencies +FROM ghcr.io/nautobot/nautobot-dev:${NAUTOBOT_VER}-py${PYTHON_VER} + +# Runtime argument and environment setup ARG NAUTOBOT_ROOT=/opt/nautobot -ENV NAUTOBOT_ROOT ${NAUTOBOT_ROOT} +ENV prometheus_multiproc_dir=/prom_cache +ENV NAUTOBOT_ROOT=${NAUTOBOT_ROOT} +ENV INVOKE_NAUTOBOT_DESIGN_BUILDER_LOCAL=true + +# Install Poetry manually via its installer script; +# We might be using an older version of Nautobot that includes an older version of Poetry +# and CI and local development may have a newer version of Poetry +# Since this is only used for development and we don't ship this container, pinning Poetry back is not expressly necessary +# We also don't need virtual environments in container +RUN which poetry || curl -sSL https://install.python-poetry.org | python3 - && \ + poetry config virtualenvs.create false -WORKDIR $NAUTOBOT_ROOT +# !!! USE CAUTION WHEN MODIFYING LINES ABOVE +# ------------------------------------------------------------------------------------- +# App-specifc system build/test dependencies. +# +# Example: LDAP requires `libldap2-dev` to be apt-installed before the Python package. +# ------------------------------------------------------------------------------------- +# --> Start safe to modify section -# Configure poetry -RUN poetry config virtualenvs.create false \ - && poetry config installer.parallel false +# Uncomment the lines below if you are apt-installing any package. +# RUN apt-get -y update && apt-get -y install \ +# libldap2-dev \ +# && rm -rf /var/lib/apt/lists/* +# --> Stop safe to modify section # ------------------------------------------------------------------------------------- -# Install Nautobot Plugin +# Install Nautobot App # ------------------------------------------------------------------------------------- -# The temp directory is used to prepare the Poetry files. -# We need to update files to use the Nautobot version as specified -# with the NAUTOBOT_VER argument and not the version used in the lock file. -# We will use this temp directory for the process. Later, we will copy -# these files to the /source directory to override Poetry files from -# the project. -WORKDIR /tmp/install - -# Copy in only pyproject.toml/poetry.lock to help with caching this layer if no updates to dependencies -COPY poetry.lock pyproject.toml /tmp/install/ - -# Add the requested Nautobot version to pyproject -# to install the correct version based on the NAUTOBOT_VER argument -# Otherwise Poetry will override the version in this container -# with the one in the poetry.lock -RUN poetry add nautobot=${NAUTOBOT_VER} - -# --no-root declares not to install the project package since we're wanting to -# take advantage of caching dependency installation -# and the project is copied in and installed after this step -RUN poetry install --no-interaction --no-ansi --no-root - -# Copy in the rest of the source code and install local Nautobot plugin +# !!! USE CAUTION WHEN MODIFYING LINES BELOW + +# Copy in the source code WORKDIR /source COPY . /source -# Copy updated Poetry files to override the Poetry files from the project. -# This will make sure that the correct Nautobot version is used. -RUN cp /tmp/install/* /source/ -RUN poetry install --no-interaction --no-ansi + +# Get container's installed Nautobot version as a forced constraint +# NAUTOBOT_VER may be a branch name and not a published release therefor we need to get the installed version +# so pip can use it to recognize local constraints. +RUN pip show nautobot | grep "^Version: " | sed -e 's/Version: /nautobot==/' > constraints.txt + +# Use Poetry to grab dev dependencies from the lock file +# Can be improved in Poetry 1.2 which allows `poetry install --only dev` +# +# We can't use the entire freeze as it takes forever to resolve with rigidly fixed non-direct dependencies, +# especially those that are only direct to Nautobot but the container included versions slightly mismatch +RUN poetry export -f requirements.txt --without-hashes --output poetry_freeze_base.txt +RUN poetry export -f requirements.txt --with dev --without-hashes --output poetry_freeze_all.txt +RUN sort poetry_freeze_base.txt poetry_freeze_all.txt | uniq -u > poetry_freeze_dev.txt + +# Install all local project as editable, constrained on Nautobot version, to get any additional +# direct dependencies of the app +RUN --mount=type=cache,target="/root/.cache/pip",sharing=locked \ + pip install -c constraints.txt -e .[all] + +# Install any dev dependencies frozen from Poetry +# Can be improved in Poetry 1.2 which allows `poetry install --only dev` +RUN --mount=type=cache,target="/root/.cache/pip",sharing=locked \ + pip install -c constraints.txt -r poetry_freeze_dev.txt COPY development/nautobot_config.py ${NAUTOBOT_ROOT}/nautobot_config.py +# !!! USE CAUTION WHEN MODIFYING LINES ABOVE diff --git a/development/development.env b/development/development.env index a82d7672..54f0b870 100644 --- a/development/development.env +++ b/development/development.env @@ -7,13 +7,10 @@ NAUTOBOT_BANNER_TOP="Local" NAUTOBOT_CHANGELOG_RETENTION=0 NAUTOBOT_DEBUG=True -NAUTOBOT_DJANGO_EXTENSIONS_ENABLED=True -NAUTOBOT_DJANGO_TOOLBAR_ENABLED=True NAUTOBOT_LOG_LEVEL=DEBUG NAUTOBOT_METRICS_ENABLED=True NAUTOBOT_NAPALM_TIMEOUT=5 NAUTOBOT_MAX_PAGE_SIZE=0 -NAUTOBOT_INSTALLATION_METRICS_ENABLED = False # Redis Configuration Environment Variables NAUTOBOT_REDIS_HOST=redis diff --git a/development/docker-compose.base.yml b/development/docker-compose.base.yml index 973f8276..ff38d9c6 100644 --- a/development/docker-compose.base.yml +++ b/development/docker-compose.base.yml @@ -7,7 +7,7 @@ x-nautobot-build: &nautobot-build context: "../" dockerfile: "development/Dockerfile" x-nautobot-base: &nautobot-base - image: "design-builder/nautobot:${NAUTOBOT_VER}-py${PYTHON_VER}" + image: "nautobot-design-builder/nautobot:${NAUTOBOT_VER}-py${PYTHON_VER}" env_file: - "development.env" - "creds.env" @@ -21,12 +21,9 @@ services: condition: "service_started" db: condition: "service_healthy" - healthcheck: - interval: "30s" - timeout: "10s" - start_period: "120s" - retries: 3 - <<: [*nautobot-build, *nautobot-base] + <<: + - *nautobot-base + - *nautobot-build worker: entrypoint: - "sh" @@ -39,10 +36,15 @@ services: timeout: "10s" start_period: "30s" retries: 3 - test: [ - "CMD", - "bash", - "-c", - "nautobot-server celery inspect ping --destination celery@$$HOSTNAME", - ] ## $$ because of docker-compose + test: ["CMD", "bash", "-c", "nautobot-server celery inspect ping --destination celery@$$HOSTNAME"] ## $$ because of docker-compose + <<: *nautobot-base + beat: + entrypoint: + - "sh" + - "-c" # this is to evaluate the $NAUTOBOT_LOG_LEVEL from the env + - "nautobot-server celery beat -l $$NAUTOBOT_LOG_LEVEL" ## $$ because of docker-compose + depends_on: + - "nautobot" + healthcheck: + disable: true <<: *nautobot-base diff --git a/development/docker-compose.dev.yml b/development/docker-compose.dev.yml index 45e392b3..2201007b 100644 --- a/development/docker-compose.dev.yml +++ b/development/docker-compose.dev.yml @@ -12,15 +12,15 @@ services: volumes: - "./nautobot_config.py:/opt/nautobot/nautobot_config.py" - "../:/source" - - "../examples/backbone_design/designs:/opt/nautobot/designs:cached" - - "../examples/backbone_design/jobs:/opt/nautobot/jobs:cached" + healthcheck: + test: ["CMD", "true"] # Due to layering, disable: true won't work. Instead, change the test docs: entrypoint: "mkdocs serve -v -a 0.0.0.0:8080" ports: - "8001:8080" volumes: - "../:/source" - image: "design-builder/nautobot:${NAUTOBOT_VER}-py${PYTHON_VER}" + image: "nautobot-design-builder/nautobot:${NAUTOBOT_VER}-py${PYTHON_VER}" healthcheck: disable: true tty: true @@ -32,8 +32,8 @@ services: volumes: - "./nautobot_config.py:/opt/nautobot/nautobot_config.py" - "../:/source" - - "../examples/backbone_design/designs:/opt/nautobot/designs:cached" - - "../examples/backbone_design/jobs:/opt/nautobot/jobs:cached" + healthcheck: + test: ["CMD", "true"] # Due to layering, disable: true won't work. Instead, change the test # To expose postgres or redis to the host uncomment the following # postgres: # ports: diff --git a/development/docker-compose.mysql.yml b/development/docker-compose.mysql.yml index c7fa6a1f..062ada94 100644 --- a/development/docker-compose.mysql.yml +++ b/development/docker-compose.mysql.yml @@ -20,6 +20,7 @@ services: image: "mysql:8" command: - "--default-authentication-plugin=mysql_native_password" + - "--max_connections=1000" env_file: - "development.env" - "creds.env" @@ -27,7 +28,12 @@ services: volumes: - "mysql_data:/var/lib/mysql" healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + test: + - "CMD" + - "mysqladmin" + - "ping" + - "-h" + - "localhost" timeout: "20s" retries: 10 volumes: diff --git a/development/docker-compose.postgres.yml b/development/docker-compose.postgres.yml index 55afdb70..12d1de31 100644 --- a/development/docker-compose.postgres.yml +++ b/development/docker-compose.postgres.yml @@ -7,11 +7,13 @@ services: - "NAUTOBOT_DB_ENGINE=django.db.backends.postgresql" db: image: "postgres:13-alpine" + command: + - "-c" + - "max_connections=200" env_file: - "development.env" - "creds.env" volumes: - # - "./nautobot.sql:/tmp/nautobot.sql" - "postgres_data:/var/lib/postgresql/data" healthcheck: test: "pg_isready --username=$$POSTGRES_USER --dbname=$$POSTGRES_DB" diff --git a/development/nautobot_config.py b/development/nautobot_config.py index cb3da0f0..d09d2fea 100644 --- a/development/nautobot_config.py +++ b/development/nautobot_config.py @@ -1,13 +1,24 @@ """Nautobot development configuration file.""" -# pylint: disable=invalid-envvar-default import os import sys -from nautobot.core.settings import * # noqa: F403 -from nautobot.core.settings_funcs import parse_redis_connection -from importlib import metadata -from packaging.version import Version +from nautobot.core.settings import * # noqa: F403 # pylint: disable=wildcard-import,unused-wildcard-import +from nautobot.core.settings_funcs import is_truthy, parse_redis_connection +# +# Debug +# + +DEBUG = is_truthy(os.getenv("NAUTOBOT_DEBUG", False)) +_TESTING = len(sys.argv) > 1 and sys.argv[1] == "test" + +if DEBUG and not _TESTING: + DEBUG_TOOLBAR_CONFIG = {"SHOW_TOOLBAR_CALLBACK": lambda _request: True} + + if "debug_toolbar" not in INSTALLED_APPS: # noqa: F405 + INSTALLED_APPS.append("debug_toolbar") # noqa: F405 + if "debug_toolbar.middleware.DebugToolbarMiddleware" not in MIDDLEWARE: # noqa: F405 + MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware") # noqa: F405 # # Misc. settings @@ -16,6 +27,9 @@ ALLOWED_HOSTS = os.getenv("NAUTOBOT_ALLOWED_HOSTS", "").split(" ") SECRET_KEY = os.getenv("NAUTOBOT_SECRET_KEY", "") +# +# Database +# nautobot_db_engine = os.getenv("NAUTOBOT_DB_ENGINE", "django.db.backends.postgresql") default_db_settings = { @@ -45,18 +59,28 @@ DATABASES["default"]["OPTIONS"] = {"charset": "utf8mb4"} # -# Debug +# Redis # -DEBUG = True +# The django-redis cache is used to establish concurrent locks using Redis. +CACHES = { + "default": { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": parse_redis_connection(redis_database=0), + "TIMEOUT": 300, + "OPTIONS": { + "CLIENT_CLASS": "django_redis.client.DefaultClient", + }, + } +} -# Django Debug Toolbar -DEBUG_TOOLBAR_CONFIG = {"SHOW_TOOLBAR_CALLBACK": lambda _request: DEBUG and not TESTING} +# Redis Cacheops +CACHEOPS_REDIS = parse_redis_connection(redis_database=1) -if DEBUG and "debug_toolbar" not in INSTALLED_APPS: # noqa: F405 - INSTALLED_APPS.append("debug_toolbar") # noqa: F405 -if DEBUG and "debug_toolbar.middleware.DebugToolbarMiddleware" not in MIDDLEWARE: # noqa: F405 - MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware") # noqa: F405 +# +# Celery settings are not defined here because they can be overloaded with +# environment variables. By default they use `CACHES["default"]["LOCATION"]`. +# # # Logging @@ -64,10 +88,8 @@ LOG_LEVEL = "DEBUG" if DEBUG else "INFO" -TESTING = len(sys.argv) > 1 and sys.argv[1] == "test" - # Verbose logging during normal development operation, but quiet logging during unit test execution -if not TESTING: +if not _TESTING: LOGGING = { "version": 1, "disable_existing_loggers": False, @@ -103,44 +125,17 @@ } # -# Redis +# Apps # -# The django-redis cache is used to establish concurrent locks using Redis. The -# django-rq settings will use the same instance/database by default. -# -# This "default" server is now used by RQ_QUEUES. -# >> See: nautobot.core.settings.RQ_QUEUES -CACHES = { - "default": { - "BACKEND": "django_redis.cache.RedisCache", - "LOCATION": parse_redis_connection(redis_database=0), - "TIMEOUT": 300, - "OPTIONS": { - "CLIENT_CLASS": "django_redis.client.DefaultClient", - }, - } -} - -# RQ_QUEUES is not set here because it just uses the default that gets imported -# up top via `from nautobot.core.settings import *`. - -# Redis Cacheops -CACHEOPS_REDIS = parse_redis_connection(redis_database=1) - -# -# Celery settings are not defined here because they can be overloaded with -# environment variables. By default they use `CACHES["default"]["LOCATION"]`. -# - -# Enable installed plugins. Add the name of each plugin to the list. +# Enable installed Apps. Add the name of each App to the list. PLUGINS = ["nautobot_design_builder"] -# TODO: The following is necessary only until BGP models plugin -# is officially supported in 2.0 -nautobot_version = Version(Version(metadata.version("nautobot")).base_version) - -if nautobot_version < Version("2.0"): - PLUGINS.append("nautobot_bgp_models") - -PLUGINS_CONFIG = {"design_builder": {"context_repository": os.getenv("DESIGN_BUILDER_CONTEXT_REPO_SLUG", None)}} +# Apps configuration settings. These settings are used by various Apps that the user may have installed. +# Each key in the dictionary is the name of an installed App and its value is a dictionary of settings. +# PLUGINS_CONFIG = { +# 'nautobot_design_builder': { +# 'foo': 'bar', +# 'buzz': 'bazz' +# } +# } diff --git a/docs/admin/compatibility_matrix.md b/docs/admin/compatibility_matrix.md index cf37119b..697069a1 100644 --- a/docs/admin/compatibility_matrix.md +++ b/docs/admin/compatibility_matrix.md @@ -1,5 +1,8 @@ # Compatibility Matrix -| Design Builder Version | Nautobot First Support Version | Nautobot Last Support Version | +!!! warning "Developer Note - Remove Me!" + Explain how the release models of the plugin and of Nautobot work together, how releases are supported, how features and older releases are deprecated etc. + +| Nautobot Design Builder Version | Nautobot First Support Version | Nautobot Last Support Version | | ------------- | -------------------- | ------------- | -| 1.0.X | 1.6.0 | 2.0.X | +| 1.0.X | 1.6.8 | 1.99.99 | diff --git a/docs/admin/install.md b/docs/admin/install.md index b77212d5..4695cdd2 100644 --- a/docs/admin/install.md +++ b/docs/admin/install.md @@ -2,9 +2,12 @@ Here you will find detailed instructions on how to **install** and **configure** the App within your Nautobot environment. +!!! warning "Developer Note - Remove Me!" + Detailed instructions on installing the App. You will need to update this section based on any additional dependencies or prerequisites. + ## Prerequisites -- The plugin is compatible with Nautobot 1.6.0 and higher. +- The plugin is compatible with Nautobot 1.6.8 and higher. - Databases supported: PostgreSQL, MySQL !!! note @@ -12,12 +15,13 @@ Here you will find detailed instructions on how to **install** and **configure** ### Access Requirements -Design Builder does not necessarily require any external system access. However, if design jobs will be loaded from a git repository, then the Nautobot instances will need access to the git repo. +!!! warning "Developer Note - Remove Me!" + What external systems (if any) it needs access to in order to work. ## Install Guide !!! note - Plugins can be installed manually or using Python's `pip`. See the [nautobot documentation](https://nautobot.readthedocs.io/en/latest/plugins/#install-the-package) for more details. The pip package name for this plugin is [`nautobot-design-builder`](https://pypi.org/project/nautobot/design-builder/). + Plugins can be installed manually or using Python's `pip`. See the [nautobot documentation](https://nautobot.readthedocs.io/en/latest/plugins/#install-the-package) for more details. The pip package name for this plugin is [`nautobot-design-builder`](https://pypi.org/project/nautobot-design-builder/). The plugin is available as a Python package via PyPI and can be installed with `pip`: @@ -25,7 +29,7 @@ The plugin is available as a Python package via PyPI and can be installed with ` pip install nautobot-design-builder ``` -To ensure Design Builder is automatically re-installed during future upgrades, create a file named `local_requirements.txt` (if not already existing) in the Nautobot root directory (alongside `requirements.txt`) and list the `nautobot-design-builder` package: +To ensure Nautobot Design Builder is automatically re-installed during future upgrades, create a file named `local_requirements.txt` (if not already existing) in the Nautobot root directory (alongside `requirements.txt`) and list the `nautobot-design-builder` package: ```shell echo nautobot-design-builder >> local_requirements.txt @@ -62,3 +66,16 @@ Then restart (if necessary) the Nautobot services which may include: ```shell sudo systemctl restart nautobot nautobot-worker nautobot-scheduler ``` + +## App Configuration + +!!! warning "Developer Note - Remove Me!" + Any configuration required to get the App set up. Edit the table below as per the examples provided. + +The plugin behavior can be controlled with the following list of settings: + +| Key | Example | Default | Description | +| ------- | ------ | -------- | ------------------------------------- | +| `enable_backup` | `True` | `True` | A boolean to represent whether or not to run backup configurations within the plugin. | +| `platform_slug_map` | `{"cisco_wlc": "cisco_aireos"}` | `None` | A dictionary in which the key is the platform slug and the value is what netutils uses in any "network_os" parameter. | +| `per_feature_bar_width` | `0.15` | `0.15` | The width of the table bar within the overview report | diff --git a/docs/admin/release_notes/version_1.0.md b/docs/admin/release_notes/version_1.0.md index 9076e6ae..e2342da4 100644 --- a/docs/admin/release_notes/version_1.0.md +++ b/docs/admin/release_notes/version_1.0.md @@ -1,11 +1,48 @@ # v1.0 Release Notes +!!! warning "Developer Note - Remove Me!" + Guiding Principles: + + - Changelogs are for humans, not machines. + - There should be an entry for every single version. + - The same types of changes should be grouped. + - Versions and sections should be linkable. + - The latest version comes first. + - The release date of each version is displayed. + - Mention whether you follow Semantic Versioning. + + Types of changes: + + - `Added` for new features. + - `Changed` for changes in existing functionality. + - `Deprecated` for soon-to-be removed features. + - `Removed` for now removed features. + - `Fixed` for any bug fixes. + - `Security` in case of vulnerabilities. + + This document describes all new features and changes in the release `1.0`. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Release Overview -Initial Public Release +- Major features or milestones +- Achieved in this `x.y` release +- Changes to compatibility with Nautobot and/or other plugins, libraries etc. + +## [v1.0.1] - 2021-09-08 + +### Added + +### Changed + +### Fixed + +- [#123](https://github.com/nautobot/nautobot-app-design-builder/issues/123) Fixed Tag filtering not working in job launch form + +## [v1.0.0] - 2021-08-03 + +### Added -## [v1.0.0] - 2023-11-01 +### Changed -Initial Public Release +### Fixed diff --git a/docs/admin/uninstall.md b/docs/admin/uninstall.md index 63a452ba..3481dce0 100644 --- a/docs/admin/uninstall.md +++ b/docs/admin/uninstall.md @@ -2,10 +2,17 @@ Here you will find any steps necessary to cleanly remove the App from your Nautobot environment. -## Uninstall Guide +## Database Cleanup -Remove the `DESIN_BUILDER` section that was added to `nautobot_config.py` `PLUGINS` & `PLUGINS_CONFIG`. +Prior to removing the plugin from the `nautobot_config.py`, run the following command to roll back any migration specific to this plugin. -## Database Cleanup +```shell +nautobot-server migrate nautobot_app_design_builder zero +``` + +!!! warning "Developer Note - Remove Me!" + Any other cleanup operations to ensure the database is clean after the app is removed. Is there anything else that needs cleaning up, such as CFs, relationships, etc. if they're no longer desired? + +## Remove App configuration -The current version of Design Builder does not include any database models, so no database cleanup is necessary. +Remove the configuration you added in `nautobot_config.py` from `PLUGINS` & `PLUGINS_CONFIG`. diff --git a/docs/admin/upgrade.md b/docs/admin/upgrade.md index 49614d8c..a9ba697a 100644 --- a/docs/admin/upgrade.md +++ b/docs/admin/upgrade.md @@ -4,8 +4,7 @@ Here you will find any steps necessary to upgrade the App in your Nautobot envir ## Upgrade Guide -Since Design Builder does not currently include any custom data models the only requirement for updating is to update the `nautobot-design-builder` package using the `pip` command: +!!! warning "Developer Note - Remove Me!" + Add more detailed steps on how the app is upgraded in an existing Nautobot setup and any version specifics (such as upgrading between major versions with breaking changes). -```python -pip install --upgrade nautobot-design-builder -``` +When a new release comes out it may be necessary to run a migration of the database to account for any changes in the data models used by this plugin. Execute the command `nautobot-server post-upgrade` within the runtime environment of your Nautobot installation after updating the `nautobot-design-builder` package via `pip`. diff --git a/docs/assets/extra.css b/docs/assets/extra.css index a51ccd3e..dfe2e4b1 100644 --- a/docs/assets/extra.css +++ b/docs/assets/extra.css @@ -18,6 +18,15 @@ font-size: 0.7rem; } +/* +* The default max-width is 61rem which does not provide nearly enough space to present code examples or larger tables +*/ +.md-grid { + margin-left: auto; + margin-right: auto; + max-width: 95%; +} + .md-tabs__link { font-size: 0.8rem; } diff --git a/docs/assets/overrides/partials/copyright.html b/docs/assets/overrides/partials/copyright.html index 77aa5078..b92cf5e3 100644 --- a/docs/assets/overrides/partials/copyright.html +++ b/docs/assets/overrides/partials/copyright.html @@ -4,7 +4,7 @@ - Not open source LICENSE + Apache-2.0 LICENSE {% endif %} diff --git a/docs/dev/arch_decision.md b/docs/dev/arch_decision.md new file mode 100644 index 00000000..e7bcbbe4 --- /dev/null +++ b/docs/dev/arch_decision.md @@ -0,0 +1,7 @@ +# Architecture Decision Records + +The intention is to document deviations from a standard Model View Controller (MVC) design. + +!!! warning "Developer Note - Remove Me!" + Optional page, remove if not applicable. + For examples see [Golden Config](https://github.com/nautobot/nautobot-plugin-golden-config/tree/develop/docs/dev/dev_adr.md) and [nautobot-plugin-reservation](https://github.com/networktocode/nautobot-plugin-reservation/blob/develop/docs/dev/dev_adr.md). diff --git a/docs/dev/code_reference/api.md b/docs/dev/code_reference/api.md new file mode 100644 index 00000000..462c3467 --- /dev/null +++ b/docs/dev/code_reference/api.md @@ -0,0 +1,5 @@ +# Nautobot Design Builder API Package + +::: nautobot_design_builder.api + options: + show_submodules: True diff --git a/docs/dev/code_reference/index.md b/docs/dev/code_reference/index.md index 473f2c40..ebe9ff7d 100644 --- a/docs/dev/code_reference/index.md +++ b/docs/dev/code_reference/index.md @@ -1,3 +1,6 @@ # Code Reference Auto-generated code reference documentation from docstrings. + +!!! warning "Developer Note - Remove Me!" + Uses [mkdocstrings](https://mkdocstrings.github.io/) syntax to auto-generate code documentation from docstrings. Two example pages are provided ([api](api.md) and [package](package.md)), add new stubs for each module or package that you think has relevant documentation. diff --git a/docs/dev/code_reference/package.md b/docs/dev/code_reference/package.md new file mode 100644 index 00000000..5cbc0eb0 --- /dev/null +++ b/docs/dev/code_reference/package.md @@ -0,0 +1 @@ +::: nautobot_design_builder diff --git a/docs/dev/contributing.md b/docs/dev/contributing.md index 2d239fb3..2337f740 100644 --- a/docs/dev/contributing.md +++ b/docs/dev/contributing.md @@ -1,8 +1,7 @@ # Contributing to the App -Contributions are encouraged and we are always delighted in any form of work. We are always looking for feedback both in the development of code as well as documentation, use cases, and examples. To contribute to this project, please use the following guidlines: - -## Code Development +!!! warning "Developer Note - Remove Me!" + Information on how to contribute fixes, functionality, or documentation changes back to the project. The project is packaged with a light [development environment](dev_environment.md) based on `docker-compose` to help with the local development of the project and to run tests. @@ -14,18 +13,12 @@ The project is following Network to Code software development guidelines and is Documentation is built using [mkdocs](https://www.mkdocs.org/). The [Docker based development environment](dev_environment.md#docker-development-environment) automatically starts a container hosting a live version of the documentation website on [http://localhost:8001](http://localhost:8001) that auto-refreshes when you make any changes to your local files. -## Documentation - -Code documentation follows the [Google docstring](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) style. Where possible, include a description, argument documentation and examples. - -The user and developer documentation is located in the top level `docs/` directory. The documenation is written in markdown format and is rendered using MkDocs. - -Example designs should be placed in the top level `examples/` directory, as appropriate. - ## Branching Policy -The active branch in Design Builder is the `develop` branch. However, commits are not allowed directly to this branch. Instead, fork the code and open a pull request to `develop`. +!!! warning "Developer Note - Remove Me!" + What branching policy is used for this project and where contributions should be made. ## Release Policy -There is no set release schedule for this App. New releases will be published as appropriate when new features and/or bug fixes are ready. +!!! warning "Developer Note - Remove Me!" + How new versions are released. diff --git a/docs/dev/dev_environment.md b/docs/dev/dev_environment.md index a1463c6c..30393c27 100644 --- a/docs/dev/dev_environment.md +++ b/docs/dev/dev_environment.md @@ -13,14 +13,14 @@ This is a quick reference guide if you're already familiar with the development The [Invoke](http://www.pyinvoke.org/) library is used to provide some helper commands based on the environment. There are a few configuration parameters which can be passed to Invoke to override the default configuration: -- `nautobot_ver`: the version of Nautobot to use as a base for any built docker containers (default: latest) -- `project_name`: the default docker compose project name (default: `nautobot_design_builder`) -- `python_ver`: the version of Python to use as a base for any built docker containers (default: 3.8) +- `nautobot_ver`: the version of Nautobot to use as a base for any built docker containers (default: 1.6.8) +- `project_name`: the default docker compose project name (default: `nautobot-design-builder`) +- `python_ver`: the version of Python to use as a base for any built docker containers (default: 3.11) - `local`: a boolean flag indicating if invoke tasks should be run on the host or inside the docker containers (default: False, commands will be run in docker containers) - `compose_dir`: the full path to a directory containing the project compose files - `compose_files`: a list of compose files applied in order (see [Multiple Compose files](https://docs.docker.com/compose/extends/#multiple-compose-files) for more information) -Using **Invoke** these configuration options can be overridden using [several methods](https://docs.pyinvoke.org/en/stable/concepts/configuration.html). Perhaps the simplest is setting an environment variable `INVOKE_DESIGN_BUILDER_VARIABLE_NAME` where `VARIABLE_NAME` is the variable you are trying to override. The only exception is `compose_files`, because it is a list it must be overridden in a YAML file. There is an example `invoke.yml` (`invoke.example.yml`) in this directory which can be used as a starting point. +Using **Invoke** these configuration options can be overridden using [several methods](https://docs.pyinvoke.org/en/stable/concepts/configuration.html). Perhaps the simplest is setting an environment variable `INVOKE_NAUTOBOT_DESIGN_BUILDER_VARIABLE_NAME` where `VARIABLE_NAME` is the variable you are trying to override. The only exception is `compose_files`, because it is a list it must be overridden in a YAML file. There is an example `invoke.yml` (`invoke.example.yml`) in this directory which can be used as a starting point. ### Docker Development Environment @@ -55,10 +55,8 @@ To either stop or destroy the development environment use the following options. ```yaml --- -design_builder: +nautobot_design_builder: local: true - compose_files: - - "docker-compose.requirements.yml" ``` Run the following commands: @@ -66,7 +64,7 @@ Run the following commands: ```shell poetry shell poetry install --extras nautobot -export $(cat development/dev.env | xargs) +export $(cat development/development.env | xargs) export $(cat development/creds.env | xargs) invoke start && sleep 5 nautobot-server migrate @@ -101,9 +99,6 @@ The project features a CLI helper based on [Invoke](https://www.pyinvoke.org/) t Each command can be executed with `invoke `. All commands support the arguments `--nautobot-ver` and `--python-ver` if you want to manually define the version of Python and Nautobot to use. Each command also has its own help `invoke --help` -!!! note - To run the mysql (mariadb) development environment, set the environment variable as such `export NAUTOBOT_USE_MYSQL=1`. - #### Local Development Environment ``` @@ -136,7 +131,6 @@ Each command can be executed with `invoke `. All commands support the a unittest Run Django unit tests for the plugin. ``` - ## Project Overview This project provides the ability to develop and manage the Nautobot server locally (with supporting services being *Dockerized*) or by using only Docker containers to manage Nautobot. The main difference between the two environments is the ability to debug and use **pdb** when developing locally. Debugging with **pdb** within the Docker container is more complicated, but can still be accomplished by either entering into the container (via `docker exec`) or attaching your IDE to the container and running the Nautobot service manually within the container. @@ -155,7 +149,7 @@ Poetry is used in lieu of the "virtualenv" commands and is leveraged in both env The `pyproject.toml` file outlines all of the relevant dependencies for the project: - `tool.poetry.dependencies` - the main list of dependencies. -- `tool.poetry.dev-dependencies` - development dependencies, to facilitate linting, testing, and documentation building. +- `tool.poetry.group.dev.dependencies` - development dependencies, to facilitate linting, testing, and documentation building. The `poetry shell` command is used to create and enable a virtual environment managed by Poetry, so all commands ran going forward are executed within the virtual environment. This is similar to running the `source venv/bin/activate` command with virtualenvs. To install project dependencies in the virtual environment, you should run `poetry install` - this will install **both** project and development dependencies. @@ -185,7 +179,7 @@ The first thing you need to do is build the necessary Docker image for Nautobot #14 exporting layers #14 exporting layers 1.2s done #14 writing image sha256:2d524bc1665327faa0d34001b0a9d2ccf450612bf8feeb969312e96a2d3e3503 done -#14 naming to docker.io/design-builder/nautobot:latest-py3.7 done +#14 naming to docker.io/nautobot-design-builder/nautobot:1.6.8-py3.11 done ``` ### Invoke - Starting the Development Environment @@ -196,18 +190,18 @@ Next, you need to start up your Docker containers. ➜ invoke start Starting Nautobot in detached mode... Running docker-compose command "up --detach" -Creating network "design_builder_default" with the default driver -Creating volume "design_builder_postgres_data" with default driver -Creating design_builder_redis_1 ... -Creating design_builder_docs_1 ... -Creating design_builder_postgres_1 ... -Creating design_builder_postgres_1 ... done -Creating design_builder_redis_1 ... done -Creating design_builder_nautobot_1 ... -Creating design_builder_docs_1 ... done -Creating design_builder_nautobot_1 ... done -Creating design_builder_worker_1 ... -Creating design_builder_worker_1 ... done +Creating network "nautobot_design_builder_default" with the default driver +Creating volume "nautobot_design_builder_postgres_data" with default driver +Creating nautobot_design_builder_redis_1 ... +Creating nautobot_design_builder_docs_1 ... +Creating nautobot_design_builder_postgres_1 ... +Creating nautobot_design_builder_postgres_1 ... done +Creating nautobot_design_builder_redis_1 ... done +Creating nautobot_design_builder_nautobot_1 ... +Creating nautobot_design_builder_docs_1 ... done +Creating nautobot_design_builder_nautobot_1 ... done +Creating nautobot_design_builder_worker_1 ... +Creating nautobot_design_builder_worker_1 ... done Docker Compose is now in the Docker CLI, try `docker compose up` ``` @@ -216,11 +210,11 @@ This will start all of the Docker containers used for hosting Nautobot. You shou ```bash ➜ docker ps ****CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES -ee90fbfabd77 design-builder/nautobot:latest-py3.7 "nautobot-server rqw…" 16 seconds ago Up 13 seconds design_builder_worker_1 -b8adb781d013 design-builder/nautobot:latest-py3.7 "/docker-entrypoint.…" 20 seconds ago Up 15 seconds 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp design_builder_nautobot_1 -d64ebd60675d design-builder/nautobot:latest-py3.7 "mkdocs serve -v -a …" 25 seconds ago Up 18 seconds 0.0.0.0:8001->8080/tcp, :::8001->8080/tcp design_builder_docs_1 -e72d63129b36 postgres:13-alpine "docker-entrypoint.s…" 25 seconds ago Up 19 seconds 0.0.0.0:5432->5432/tcp, :::5432->5432/tcp design_builder_postgres_1 -96c6ff66997c redis:6-alpine "docker-entrypoint.s…" 25 seconds ago Up 21 seconds 0.0.0.0:6379->6379/tcp, :::6379->6379/tcp design_builder_redis_1 +ee90fbfabd77 nautobot-design-builder/nautobot:1.6.8-py3.11 "nautobot-server rqw…" 16 seconds ago Up 13 seconds nautobot_design_builder_worker_1 +b8adb781d013 nautobot-design-builder/nautobot:1.6.8-py3.11 "/docker-entrypoint.…" 20 seconds ago Up 15 seconds 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp nautobot_design_builder_nautobot_1 +d64ebd60675d nautobot-design-builder/nautobot:1.6.8-py3.11 "mkdocs serve -v -a …" 25 seconds ago Up 18 seconds 0.0.0.0:8001->8080/tcp, :::8001->8080/tcp nautobot_design_builder_docs_1 +e72d63129b36 postgres:13-alpine "docker-entrypoint.s…" 25 seconds ago Up 19 seconds 0.0.0.0:5432->5432/tcp, :::5432->5432/tcp nautobot_design_builder_postgres_1 +96c6ff66997c redis:6-alpine "docker-entrypoint.s…" 25 seconds ago Up 21 seconds 0.0.0.0:6379->6379/tcp, :::6379->6379/tcp nautobot_design_builder_redis_1 ``` Once the containers are fully up, you should be able to open up a web browser, and view: @@ -264,27 +258,27 @@ The last command to know for now is `invoke stop`. ➜ invoke stop Stopping Nautobot... Running docker-compose command "down" -Stopping design_builder_worker_1 ... -Stopping design_builder_nautobot_1 ... -Stopping design_builder_docs_1 ... -Stopping design_builder_redis_1 ... -Stopping design_builder_postgres_1 ... -Stopping design_builder_worker_1 ... done -Stopping design_builder_nautobot_1 ... done -Stopping design_builder_postgres_1 ... done -Stopping design_builder_redis_1 ... done -Stopping design_builder_docs_1 ... done -Removing design_builder_worker_1 ... -Removing design_builder_nautobot_1 ... -Removing design_builder_docs_1 ... -Removing design_builder_redis_1 ... -Removing design_builder_postgres_1 ... -Removing design_builder_postgres_1 ... done -Removing design_builder_docs_1 ... done -Removing design_builder_worker_1 ... done -Removing design_builder_redis_1 ... done -Removing design_builder_nautobot_1 ... done -Removing network design_builder_default +Stopping nautobot_design_builder_worker_1 ... +Stopping nautobot_design_builder_nautobot_1 ... +Stopping nautobot_design_builder_docs_1 ... +Stopping nautobot_design_builder_redis_1 ... +Stopping nautobot_design_builder_postgres_1 ... +Stopping nautobot_design_builder_worker_1 ... done +Stopping nautobot_design_builder_nautobot_1 ... done +Stopping nautobot_design_builder_postgres_1 ... done +Stopping nautobot_design_builder_redis_1 ... done +Stopping nautobot_design_builder_docs_1 ... done +Removing nautobot_design_builder_worker_1 ... +Removing nautobot_design_builder_nautobot_1 ... +Removing nautobot_design_builder_docs_1 ... +Removing nautobot_design_builder_redis_1 ... +Removing nautobot_design_builder_postgres_1 ... +Removing nautobot_design_builder_postgres_1 ... done +Removing nautobot_design_builder_docs_1 ... done +Removing nautobot_design_builder_worker_1 ... done +Removing nautobot_design_builder_redis_1 ... done +Removing nautobot_design_builder_nautobot_1 ... done +Removing network nautobot_design_builder_default ``` This will safely shut down all of your running Docker containers for this project. When you are ready to spin containers back up, it is as simple as running `invoke start` again [as seen previously](#invoke-starting-the-development-environment). @@ -319,7 +313,10 @@ When trying to debug an issue, one helpful thing you can look at are the logs wi !!! note The `-f` tag will keep the logs open, and output them in realtime as they are generated. -So for example, our plugin is named `design-builder`, the command would most likely be `docker logs design_builder_nautobot_1 -f`. You can find the name of all running containers via `docker ps`. +!!! info + Want to limit the log output even further? Use the `--tail <#>` command line argument in conjunction with `-f`. + +So for example, our plugin is named `nautobot-design-builder`, the command would most likely be `docker logs nautobot_design_builder_nautobot_1 -f`. You can find the name of all running containers via `docker ps`. If you want to view the logs specific to the worker container, simply use the name of that container instead. @@ -389,38 +386,38 @@ Once the containers are up and running, you should now see the new plugin instal To update the Python version, you can update it within `tasks.py`. ```python -namespace = Collection("design_builder") +namespace = Collection("nautobot_design_builder") namespace.configure( { - "design_builder": { + "nautobot_design_builder": { ... - "python_ver": "3.7", + "python_ver": "3.11", ... } } ) ``` -Or set the `INVOKE_NAUTOBOT_GOLDEN_CONFIG_PYTHON_VER` variable. +Or set the `INVOKE_NAUTOBOT_DESIGN_BUILDER_PYTHON_VER` variable. ### Updating Nautobot Version To update the Nautobot version, you can update it within `tasks.py`. ```python -namespace = Collection("design_builder") +namespace = Collection("nautobot_design_builder") namespace.configure( { - "design_builder": { + "nautobot_design_builder": { ... - "nautobot_ver": "1.0.2", + "nautobot_ver": "1.6.8", ... } } ) ``` -Or set the `INVOKE_DESIGN_BUILDER_NAUTOBOT_VER` variable. +Or set the `INVOKE_NAUTOBOT_DESIGN_BUILDER_NAUTOBOT_VER` variable. ## Other Miscellaneous Commands To Know diff --git a/docs/dev/extending.md b/docs/dev/extending.md index a9952735..49b89f46 100644 --- a/docs/dev/extending.md +++ b/docs/dev/extending.md @@ -1,39 +1,6 @@ # Extending the App -Design builder is primarily extended by creating new action tags. These action tags can be provided by a design repository or they can be contributed to the upstream Design Builder project for consumption by the community. Upstreaming these extensions is welcome, however it is best to open an issue first, to ensure that a PR would be accepted and makes sense in terms of features and design. +!!! warning "Developer Note - Remove Me!" + Information on how to extend the App functionality. -## Action Tag Extensions - -The action tags in Design Builder are provided by `design.Builder`. This component reads a design and then executes instructions that are specified in the design. Basic functions, provided out of the box, are -`create`, `create_or_update` and `update`. These actions are self explanatory (for details on syntax see [this document](../user//design_development.md#special-syntax)). Two additional actions are provided, these are the `ref` and `git_context` actions. These two actions are provided as extensions to the builder. - -Extensions specify attribute and/or value actions to the object creator. Within a design template, these extensions can be used by specifying an exclamation point (!) followed by the extensions attribute or value tag. For instance, the `ref` extension implements both an attribute and a value extension. This extension can be used by specifying `!ref`. Extensions can add behavior to the object creator that is not supplied by the standard create and update actions. - -### Attribute Extensions - -Attribute extensions provide some functionality when specified as a YAMl attribute. For instance: - -```yaml -devices: - name: My New Device - "!my_attribute_extension": "some data passed to the extensions" -``` - -In this case, when the object creator encountered `!my_attribute_extension` it will look for an extension that specifies an attribute_tag `my_attribute_extension` and will call the associated `attribute` method on that extension. The `attribute` method will be given the object that is being worked on (the device "My New Device" in this case) as well as the value assigned to the attribute (the string "some data ..." in this case). Values can be any supported YAML type including strings, dictionaries and lists. It is up to the extension to determine if the provided value is valid or not. - -### Value Extensions - -Value extensions can be used to assign a value to an attribute. For instance: - -```yaml -device: - name: "!device_name" -``` - -In this case, when `!device_name` is encountered the object creator will look for an extension that implements the `device_name` value tag. If found, the corresponding `value` method will be called on the extension. Whatever `value` returns will be assigned to the attribute (`name` in this case). For a concrete example of an extension that implements both `attribute` and `value` see the [API docs](./code_reference/ext.md#design_builder.ext.ReferenceExtension) for the ReferenceExtension. - -### Writing a New Extension - -Adding functionality to `design.Builder` is as simple extending the [Extension](./code_reference/ext.md#design_builder.ext.Extension) class and supplying `attribute_tag` and/or `value_tag` class variables as well as the corresponding `attribute` and `value` instance methods. Extensions are singletons within a Builder instance. When an extension's tag is encountered an instance of the extension is created. Subsequent calls to the extension will use the instance created the first time. - -Each extension may optionally implement `commit` or `roll_back` methods. The `commit` method is called once all of a design's objects have been created and updated in the database. Conversely, `roll_back` is called if any error occurs and the database transaction is aborted. These methods provide a means for an extension to perform additional work, or cleanup, based on the outcome of a design's database actions. +Extending the application is welcome, however it is best to open an issue first, to ensure that a PR would be accepted and makes sense in terms of features and design. diff --git a/docs/images/icon-nautobot-design-builder.png b/docs/images/icon-nautobot-design-builder.png new file mode 100644 index 0000000000000000000000000000000000000000..7e00cf6ae0ee76324adab30d68d64206678a85e1 GIT binary patch literal 74601 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
@5hb{hC=~cCz;d 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 literal 0 HcmV?d00001 diff --git a/docs/requirements.txt b/docs/requirements.txt index 4a079cd3..d168c88f 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,5 @@ mkdocs==1.5.2 -mkdocs-material==9.2.4 +mkdocs-material==9.1.15 mkdocs-version-annotations==1.0.0 mkdocstrings-python==1.5.2 mkdocstrings==0.22.0 diff --git a/docs/user/app_getting_started.md b/docs/user/app_getting_started.md index 8dc87972..24ea0099 100644 --- a/docs/user/app_getting_started.md +++ b/docs/user/app_getting_started.md @@ -8,28 +8,12 @@ To install the App, please follow the instructions detailed in the [Installation ## First steps with the App -The easiest way to experience Design Builder is to run it in a local environment. To start a local environment, clone the design builder git repository and start the application stack. The only requirements for starting a local environment are `docker`, `docker-compose` and [invoke](https://www.pyinvoke.org/installing.html). Once the dependent tools have been installed you'll need to build the docker image by running `invoke build`. At that point, simply run the command `invoke start`. This will start the entire application stack using docker compose. Once the application stack is up and running, navigate to and login. +!!! warning "Developer Note - Remove Me!" + What (with screenshots preferably) does it look like to perform the simplest workflow within the App once installed? ## What are the next steps? -The Design Builder application ships with some sample designs to demonstrate capabilities. Once the application stack is ready, you should have two designs listed under the "Jobs" -> "Jobs" menu item. +!!! warning "Developer Note - Remove Me!" + After taking the first steps, what else could the users look at doing. -![Jobs list](../images/screenshots/sample-design-jobs-list.png) - -Note that both jobs are disabled. Nautobot automatically marks jobs as disabled when they are first loaded. In order to run these jobs, click the edit button ![edit button](../images/screenshots/edit-button.png) and check the "enabled" checkbox: - -![enabled checkbox](../images/screenshots/job-enabled-checkbox.png) - -Once you click `save`, the jobs should be runnable. - -To implement any design, click the run button [run button](../images/screenshots/run-button.png). For example, run the "Initial Data" job, which will add a manufacturer, a device type, a device role, several regions and several sites. Additionally, each site will have two devices. Here is the design template for this design: - -```jinja ---8<-- "examples/backbone_design/designs/core_site/designs/0001_design.yaml.j2" -``` - -If you run the job you should see output in the job result that shows the various objects being created: - -![design job result](../images/screenshots/design-job-result.png) - -Once the initial data job has been run, try enabling and running the "Backbone Site Design" job to create a new site with racks and routers. +You can check out the [Use Cases](app_use_cases.md) section for more examples. diff --git a/docs/user/app_overview.md b/docs/user/app_overview.md index d7f79768..06ff5d32 100644 --- a/docs/user/app_overview.md +++ b/docs/user/app_overview.md @@ -7,18 +7,24 @@ This document provides an overview of the App including critical information and ## Description -Design Builder provides a system where standardized network designs can be developed to produce collections of objects within Nautobot. These designs are text based templates that can create and update hierarchical data structures within Nautobot. ## Audience (User Personas) - Who should use this App? -- Network engineers who want to have reproducible sets of Nautobot objects based on some standard design. -- Automation engineers who want to be able to automate the creation of Nautobot objects based on a set of standard designs. +!!! warning "Developer Note - Remove Me!" + Who is this meant for/ who is the common user of this app? ## Authors and Maintainers -- Andrew Bates (@abates) -- Mzb (@mzbroch) +!!! warning "Developer Note - Remove Me!" + Add the team and/or the main individuals maintaining this project. Include historical maintainers as well. ## Nautobot Features Used -This application interacts directly with Nautobot's Object Relational Mapping (ORM) system. +!!! warning "Developer Note - Remove Me!" + What is shown today in the Installed Plugins page in Nautobot. What parts of Nautobot does it interact with, what does it add etc. ? + +### Extras + +!!! warning "Developer Note - Remove Me!" + Custom Fields - things like which CFs are created by this app? + Jobs - are jobs, if so, which ones, installed by this app? diff --git a/docs/user/app_use_cases.md b/docs/user/app_use_cases.md new file mode 100644 index 00000000..dc06944f --- /dev/null +++ b/docs/user/app_use_cases.md @@ -0,0 +1,12 @@ +# Using the App + +This document describes common use-cases and scenarios for this App. + +## General Usage + +## Use-cases and common workflows + +## Screenshots + +!!! warning "Developer Note - Remove Me!" + Ideally captures every view exposed by the App. Should include a relevant dataset. diff --git a/docs/user/external_interactions.md b/docs/user/external_interactions.md new file mode 100644 index 00000000..eaba5b56 --- /dev/null +++ b/docs/user/external_interactions.md @@ -0,0 +1,17 @@ +# External Interactions + +This document describes external dependencies and prerequisites for this App to operate, including system requirements, API endpoints, interconnection or integrations to other applications or services, and similar topics. + +!!! warning "Developer Note - Remove Me!" + Optional page, remove if not applicable. + +## External System Integrations + +### From the App to Other Systems + +### From Other Systems to the App + +## Nautobot REST API endpoints + +!!! warning "Developer Note - Remove Me!" + API documentation in this doc - including python request examples, curl examples, postman collections referred etc. diff --git a/docs/user/faq.md b/docs/user/faq.md index 346f565b..318b08dc 100644 --- a/docs/user/faq.md +++ b/docs/user/faq.md @@ -1,5 +1 @@ # Frequently Asked Questions - -## When importing designs from git using the Nautobot Git Repositories feature, what should I select for the `Provides` field? - -Design builder design's are an extension of the existing Nautobot Job's functionality. Therefore, any repository containing design jobs should select the `jobs` option in the `Provides` field. diff --git a/invoke.example.yml b/invoke.example.yml index ff6e7ff6..9a4fa928 100644 --- a/invoke.example.yml +++ b/invoke.example.yml @@ -1,9 +1,9 @@ --- -design_builder: - project_name: "design-builder" - nautobot_ver: "latest" +nautobot_design_builder: + project_name: "nautobot-design-builder" + nautobot_ver: "1.6.8" local: false - python_ver: "3.8" + python_ver: "3.11" compose_dir: "development" compose_files: - "docker-compose.base.yml" diff --git a/invoke.mysql.yml b/invoke.mysql.yml index b66d6eac..62ffa9f2 100644 --- a/invoke.mysql.yml +++ b/invoke.mysql.yml @@ -1,9 +1,9 @@ --- -design_builder: - project_name: "design-builder" - nautobot_ver: "latest" +nautobot_design_builder: + project_name: "nautobot-design-builder" + nautobot_ver: "1.6.8" local: false - python_ver: "3.8" + python_ver: "3.11" compose_dir: "development" compose_files: - "docker-compose.base.yml" diff --git a/mkdocs.yml b/mkdocs.yml index 30865e5a..45425f61 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,8 +1,8 @@ --- dev_addr: "127.0.0.1:8001" -edit_uri: "edit/develop/docs" +edit_uri: "edit/main/nautobot-app-design-builder/docs" site_dir: "nautobot_design_builder/static/nautobot_design_builder/docs" -site_name: "Design Builder Documentation" +site_name: "Nautobot Design Builder Documentation" site_url: "https://docs.nautobot.com/projects/nautobot-design-builder/en/latest/" repo_url: "https://github.com/nautobot/nautobot-app-design-builder" copyright: "Copyright © The Authors" @@ -14,14 +14,17 @@ theme: - "django" - "yaml" features: - - "navigation.tracking" + - "content.action.edit" + - "content.action.view" + - "content.code.copy" + - "navigation.footer" + - "navigation.indexes" - "navigation.tabs" - "navigation.tabs.sticky" - - "search.suggest" + - "navigation.tracking" - "search.highlight" - "search.share" - - "navigation.indexes" - - "content.tooltips" + - "search.suggest" favicon: "assets/favicon.ico" logo: "assets/nautobot_logo.svg" palette: @@ -69,7 +72,6 @@ extra: link: "https://twitter.com/networktocode" name: "Network to Code Twitter" markdown_extensions: - - "abbr" - "admonition" - "toc": permalink: true @@ -78,9 +80,7 @@ markdown_extensions: - "pymdownx.highlight": anchor_linenums: true - "pymdownx.inlinehilite" - - "pymdownx.snippets": - auto_append: - - "docs/assets/abbreviations.md" + - "pymdownx.snippets" - "pymdownx.superfences" - "footnotes" plugins: @@ -90,7 +90,7 @@ plugins: default_handler: "python" handlers: python: - paths: ["nautobot_design_builder"] + paths: ["."] options: show_root_heading: true watch: @@ -101,9 +101,9 @@ nav: - User Guide: - App Overview: "user/app_overview.md" - Getting Started: "user/app_getting_started.md" - - Design Quick Start: "user/design_quickstart.md" - - Design Development: "user/design_development.md" + - Using the App: "user/app_use_cases.md" - Frequently Asked Questions: "user/faq.md" + - External Interactions: "user/external_interactions.md" - Administrator Guide: - Install and Configure: "admin/install.md" - Upgrade: "admin/upgrade.md" @@ -116,12 +116,9 @@ nav: - Extending the App: "dev/extending.md" - Contributing to the App: "dev/contributing.md" - Development Environment: "dev/dev_environment.md" + - Architecture Decision Records: "dev/arch_decision.md" - Code Reference: - "dev/code_reference/index.md" - - Design Job: "dev/code_reference/design_job.md" - - Context: "dev/code_reference/context.md" - - Design Builder: "dev/code_reference/design.md" - - Jinja Rendering: "dev/code_reference/jinja2.md" - - Template Extensions: "dev/code_reference/ext.md" - - Util: "dev/code_reference/util.md" + - Package: "dev/code_reference/package.md" + - API: "dev/code_reference/api.md" - Nautobot Docs Home ↗︎: "https://docs.nautobot.com" diff --git a/nautobot_design_builder/__init__.py b/nautobot_design_builder/__init__.py index 21450d85..79d1d6c2 100644 --- a/nautobot_design_builder/__init__.py +++ b/nautobot_design_builder/__init__.py @@ -1,39 +1,26 @@ -"""App declaration for Nautobot Design Builder.""" -from django.conf import settings -from django.utils.functional import classproperty - -from nautobot.apps import NautobotAppConfig - +"""Plugin declaration for nautobot_design_builder.""" # Metadata is inherited from Nautobot. If not including Nautobot in the environment, this should be added -try: - from importlib import metadata -except ImportError: - # Python version < 3.8 - import importlib_metadata as metadata +from importlib import metadata __version__ = metadata.version(__name__) +from nautobot.extras.plugins import NautobotAppConfig -class DesignBuilderConfig(NautobotAppConfig): - """App configuration for the nautobot_design_builder app.""" + +class NautobotDesignBuilderConfig(NautobotAppConfig): + """Plugin configuration for the nautobot_design_builder plugin.""" name = "nautobot_design_builder" - verbose_name = "Design Builder" + verbose_name = "Nautobot Design Builder" version = __version__ author = "Network to Code, LLC" - description = "Design Builder." + description = "Nautobot app that uses design templates to easily create data objects in Nautobot with minimal input from a user.." base_url = "design-builder" required_settings = [] - min_version = "1.6.0" + min_version = "1.6.8" max_version = "2.9999" default_settings = {} caching_config = {} - # pylint: disable=no-self-argument - @classproperty - def context_repository(cls): - """Retrieve the Git Repository slug that has been configured for the Design Builder.""" - return settings.PLUGINS_CONFIG[cls.name]["context_repository"] - -config = DesignBuilderConfig # pylint:disable=invalid-name +config = NautobotDesignBuilderConfig # pylint:disable=invalid-name diff --git a/nautobot_design_builder/api/__init__.py b/nautobot_design_builder/api/__init__.py new file mode 100644 index 00000000..e61c518c --- /dev/null +++ b/nautobot_design_builder/api/__init__.py @@ -0,0 +1 @@ +"""REST API module for nautobot_design_builder plugin.""" diff --git a/nautobot_design_builder/migrations/__init__.py b/nautobot_design_builder/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/nautobot_design_builder/tests/__init__.py b/nautobot_design_builder/tests/__init__.py index 0a946241..78db10c4 100644 --- a/nautobot_design_builder/tests/__init__.py +++ b/nautobot_design_builder/tests/__init__.py @@ -1,79 +1 @@ -"""Unit tests for design_builder app.""" - -import logging -import shutil -import tempfile -from os import path -from typing import Type -from unittest import mock -from unittest.mock import PropertyMock, patch - -from django.test import TestCase - -from nautobot_design_builder.design_job import DesignJob -from nautobot_design_builder.util import nautobot_version - -logging.disable(logging.CRITICAL) - - -class DesignTestCase(TestCase): - """DesignTestCase aides in creating unit tests for design jobs and templates.""" - - def setUp(self): - """Setup a mock git repo to watch for config context creation.""" - super().setUp() - self.logged_messages = [] - self.git_patcher = patch("nautobot_design_builder.ext.GitRepo") - self.git_mock = self.git_patcher.start() - - self.git_path = tempfile.mkdtemp() - git_instance_mock = PropertyMock() - git_instance_mock.return_value.path = self.git_path - self.git_mock.side_effect = git_instance_mock - - def get_mocked_job(self, design_class: Type[DesignJob]): - """Create an instance of design_class and properly mock request and job_result for testing.""" - job = design_class() - job.job_result = mock.Mock() - if nautobot_version < "2.0.0": - job.request = mock.Mock() - else: - job.job_result.data = {} - old_run = job.run - - def new_run(data, commit): - kwargs = {**data} - kwargs["dryrun"] = not commit - old_run(**kwargs) - - job.run = new_run - self.logged_messages = [] - - def record_log(message, obj, level_choice, grouping=None, logger=None): # pylint: disable=unused-argument - self.logged_messages.append( - { - "message": message, - "obj": obj, - "level_choice": level_choice, - "grouping": grouping, - } - ) - - job.job_result.log.side_effect = record_log - return job - - def assert_context_files_created(self, *filenames): - """Confirm that the list of filenames were created as part of the design implementation.""" - for filename in filenames: - self.assertTrue(path.exists(path.join(self.git_path, filename)), f"{filename} was not created") - - def assertJobSuccess(self, job): # pylint: disable=invalid-name - """Assert that a mocked job has completed successfully.""" - if job.failed: - self.fail(f"Job failed with {self.logged_messages[-1]}") - - def tearDown(self): - """Remove temporary files.""" - self.git_patcher.stop() - shutil.rmtree(self.git_path) - super().tearDown() +"""Unit tests for nautobot_design_builder plugin.""" diff --git a/nautobot_design_builder/tests/test_api.py b/nautobot_design_builder/tests/test_api.py new file mode 100644 index 00000000..ff93d198 --- /dev/null +++ b/nautobot_design_builder/tests/test_api.py @@ -0,0 +1,28 @@ +"""Unit tests for nautobot_design_builder.""" +from django.contrib.auth import get_user_model +from django.test import TestCase +from django.urls import reverse +from rest_framework import status +from rest_framework.test import APIClient + +from nautobot.users.models import Token + +User = get_user_model() + + +class PlaceholderAPITest(TestCase): + """Test the NautobotDesignBuilder API.""" + + def setUp(self): + """Create a superuser and token for API calls.""" + self.user = User.objects.create(username="testuser", is_superuser=True) + self.token = Token.objects.create(user=self.user) + self.client = APIClient() + self.client.credentials(HTTP_AUTHORIZATION=f"Token {self.token.key}") + + def test_placeholder(self): + """Verify that devices can be listed.""" + url = reverse("dcim-api:device-list") + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["count"], 0) diff --git a/nautobot_design_builder/tests/test_basic.py b/nautobot_design_builder/tests/test_basic.py new file mode 100644 index 00000000..9b52639c --- /dev/null +++ b/nautobot_design_builder/tests/test_basic.py @@ -0,0 +1,34 @@ +"""Basic tests that do not require Django.""" +import unittest +import os +import toml + +from nautobot_design_builder import __version__ as project_version + + +class TestVersion(unittest.TestCase): + """Test Version is the same.""" + + def test_version(self): + """Verify that pyproject.toml version is same as version specified in the package.""" + parent_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) + poetry_version = toml.load(os.path.join(parent_path, "pyproject.toml"))["tool"]["poetry"]["version"] + self.assertEqual(project_version, poetry_version) + + +class TestDocsPackaging(unittest.TestCase): + """Test Version in doc requirements is the same pyproject.""" + + def test_version(self): + """Verify that pyproject.toml dev dependencies have the same versions as in the docs requirements.txt.""" + parent_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) + poetry_path = os.path.join(parent_path, "pyproject.toml") + poetry_details = toml.load(poetry_path)["tool"]["poetry"]["group"]["dev"]["dependencies"] + with open(f"{parent_path}/docs/requirements.txt", "r", encoding="utf-8") as file: + requirements = [line for line in file.read().splitlines() if (len(line) > 0 and not line.startswith("#"))] + for pkg in requirements: + if len(pkg.split("==")) == 2: + pkg, version = pkg.split("==") + else: + version = "*" + self.assertEqual(poetry_details[pkg], version) diff --git a/pyproject.toml b/pyproject.toml index 659b29c3..0ddb3455 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,24 @@ [tool.poetry] name = "nautobot-design-builder" -version = "0.4.4" +version = "0.1.0" description = "Nautobot app that uses design templates to easily create data objects in Nautobot with minimal input from a user." -authors = ["Network to Code, LLC "] +authors = ["Network to Code, LLC "] +license = "Apache-2.0" readme = "README.md" homepage = "https://github.com/nautobot/nautobot-app-design-builder" repository = "https://github.com/nautobot/nautobot-app-design-builder" keywords = ["nautobot", "nautobot-plugin"] +classifiers = [ + "Intended Audience :: Developers", + "Development Status :: 5 - Production/Stable", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] include = [ + "LICENSE", "README.md", ] packages = [ @@ -19,21 +30,20 @@ python = ">=3.8,<3.12" # Used for local development nautobot = ">=1.6.0,<=2.9999" -[tool.poetry.dev-dependencies] +[tool.poetry.group.dev.dependencies] bandit = "*" black = "*" coverage = "*" django-debug-toolbar = "*" -# we need to pin flake8 because of package dependencies that cause it to downgrade and -# therefore cause issues with linting since older versions do not take .flake8 as config -flake8 = "^3.9.2" +flake8 = "*" invoke = "*" ipython = "*" pydocstyle = "*" pylint = "*" pylint-django = "*" -pytest = "*" +pylint-nautobot = "*" yamllint = "*" +toml = "*" Markdown = "*" toml = "*" @@ -42,21 +52,16 @@ nautobot-bgp-models = "*" # Rendering docs to HTML mkdocs = "1.5.2" # Material for MkDocs theme -mkdocs-material = "9.2.4" +mkdocs-material = "9.1.15" # Render custom markdown for version added/changed/remove notes mkdocs-version-annotations = "1.0.0" # Automatic documentation from sources, for MkDocs mkdocstrings = "0.22.0" mkdocstrings-python = "1.5.2" - -[tool.poetry.extras] -nautobot = ["nautobot"] -# bgp_models = ["nautobot-bgp-models"] - [tool.black] line-length = 120 -target-version = ['py37'] +target-version = ['py38', 'py39', 'py310', 'py311'] include = '\.pyi?$' exclude = ''' ( @@ -79,23 +84,19 @@ exclude = ''' [tool.pylint.master] # Include the pylint_django plugin to avoid spurious warnings about Django patterns -load-plugins = "pylint_django" -ignore = ".venv" +load-plugins="pylint_django, pylint_nautobot" +ignore=".venv" [tool.pylint.basic] # No docstrings required for private methods (Pylint default), or for test_ functions, or for inner Meta classes. -no-docstring-rgx = "^(_|test_|Test|Meta$)" +no-docstring-rgx="^(_|test_|Meta$)" [tool.pylint.messages_control] # Line length is enforced by Black, so pylint doesn't need to check it. # Pylint and Black disagree about how to format multi-line arrays; Black wins. disable = """, - line-too-long, - duplicate-code, - too-many-lines, - too-many-ancestors, - raise-missing-from, -""" + line-too-long + """ [tool.pylint.miscellaneous] # Don't flag TODO as a failure, let us commit with things that still need to be done in the code @@ -104,6 +105,11 @@ notes = """, XXX, """ +[tool.pylint-nautobot] +supported_nautobot_versions = [ + "1.6.8" +] + [tool.pydocstyle] convention = "google" inherit = false @@ -119,7 +125,3 @@ add_ignore = "D212" [build-system] requires = ["poetry_core>=1.0.0"] build-backend = "poetry.core.masonry.api" - -[tool.pytest.ini_options] -testpaths = ["tests"] -addopts = "-vv --doctest-modules" diff --git a/tasks.py b/tasks.py index 45fcc144..cdafbe3b 100644 --- a/tasks.py +++ b/tasks.py @@ -1,6 +1,6 @@ """Tasks for use with Invoke. -(c) 2020-2021 Network To Code +Copyright (c) 2023, Network to Code, LLC Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -12,10 +12,11 @@ limitations under the License. """ -from distutils.util import strtobool -from invoke import Collection, task as invoke_task import os +from invoke.collection import Collection +from invoke.tasks import task as invoke_task + def is_truthy(arg): """Convert "truthy" strings into Booleans. @@ -29,7 +30,14 @@ def is_truthy(arg): """ if isinstance(arg, bool): return arg - return bool(strtobool(arg)) + + val = str(arg).lower() + if val in ("y", "yes", "t", "true", "on", "1"): + return True + elif val in ("n", "no", "f", "false", "off", "0"): + return False + else: + raise ValueError(f"Invalid truthy value: `{arg}`") # Use pyinvoke configuration for default values, see http://docs.pyinvoke.org/en/stable/concepts/configuration.html @@ -38,9 +46,9 @@ def is_truthy(arg): namespace.configure( { "nautobot_design_builder": { - "nautobot_ver": "1.6", - "project_name": "nautobot_design_builder", - "python_ver": "3.8", + "nautobot_ver": "1.6.8", + "project_name": "nautobot-design-builder", + "python_ver": "3.11", "local": False, "compose_dir": os.path.join(os.path.dirname(__file__), "development"), "compose_files": [ @@ -55,6 +63,10 @@ def is_truthy(arg): ) +def _is_compose_included(context, name): + return f"docker-compose.{name}.yml" in context.nautobot_design_builder.compose_files + + def task(function=None, *args, **kwargs): """Task decorator to override the default Invoke task decorator and add each task to the invoke namespace.""" @@ -88,13 +100,28 @@ def docker_compose(context, command, **kwargs): "COMPOSE_HTTP_TIMEOUT": context.nautobot_design_builder.compose_http_timeout, "NAUTOBOT_VER": context.nautobot_design_builder.nautobot_ver, "PYTHON_VER": context.nautobot_design_builder.python_ver, + **kwargs.pop("env", {}), } - compose_command = f'docker compose --project-name {context.nautobot_design_builder.project_name} --project-directory "{context.nautobot_design_builder.compose_dir}"' + compose_command_tokens = [ + "docker compose", + f"--project-name {context.nautobot_design_builder.project_name}", + f'--project-directory "{context.nautobot_design_builder.compose_dir}"', + ] + for compose_file in context.nautobot_design_builder.compose_files: compose_file_path = os.path.join(context.nautobot_design_builder.compose_dir, compose_file) - compose_command += f' -f "{compose_file_path}"' - compose_command += f" {command}" + compose_command_tokens.append(f' -f "{compose_file_path}"') + + compose_command_tokens.append(command) + + # If `service` was passed as a kwarg, add it to the end. + service = kwargs.pop("service", None) + if service is not None: + compose_command_tokens.append(service) + print(f'Running docker compose command "{command}"') + compose_command = " ".join(compose_command_tokens) + return context.run(compose_command, env=build_env, **kwargs) @@ -109,9 +136,11 @@ def run_command(context, command, **kwargs): if "nautobot" in results.stdout: compose_command = f"exec nautobot {command}" else: - compose_command = f"run --entrypoint '{command}' nautobot" + compose_command = f"run --rm --entrypoint '{command}' nautobot" + + pty = kwargs.pop("pty", True) - docker_compose(context, compose_command, pty=kwargs.pop("pty", True), **kwargs) + docker_compose(context, compose_command, pty=pty, **kwargs) # ------------------------------------------------------------------------------ @@ -143,42 +172,73 @@ def generate_packages(context): run_command(context, command) +@task( + help={ + "check": ( + "If enabled, check for outdated dependencies in the poetry.lock file, " + "instead of generating a new one. (default: disabled)" + ) + } +) +def lock(context, check=False): + """Generate poetry.lock inside the Nautobot container.""" + run_command(context, f"poetry {'check' if check else 'lock --no-update'}") + + # ------------------------------------------------------------------------------ # START / STOP / DEBUG # ------------------------------------------------------------------------------ -@task -def debug(context): - """Start Nautobot and its dependencies in debug mode.""" - print("Starting Nautobot in debug mode...") - docker_compose(context, "up") +@task(help={"service": "If specified, only affect this service."}) +def debug(context, service=""): + """Start specified or all services and its dependencies in debug mode.""" + print(f"Starting {service} in debug mode...") + docker_compose(context, "up", service=service) -@task -def start(context): - """Start Nautobot and its dependencies in detached mode.""" +@task(help={"service": "If specified, only affect this service."}) +def start(context, service=""): + """Start specified or all services and its dependencies in detached mode.""" print("Starting Nautobot in detached mode...") - docker_compose(context, "up --detach") + docker_compose(context, "up --detach", service=service) -@task -def restart(context): - """Gracefully restart all containers.""" +@task(help={"service": "If specified, only affect this service."}) +def restart(context, service=""): + """Gracefully restart specified or all services.""" print("Restarting Nautobot...") - docker_compose(context, "restart") + docker_compose(context, "restart", service=service) -@task -def stop(context): - """Stop Nautobot and its dependencies.""" +@task(help={"service": "If specified, only affect this service."}) +def stop(context, service=""): + """Stop specified or all services, if service is not specified, remove all containers.""" print("Stopping Nautobot...") - docker_compose(context, "down") + docker_compose(context, "stop" if service else "down --remove-orphans", service=service) @task def destroy(context): """Destroy all containers and volumes.""" print("Destroying Nautobot...") - docker_compose(context, "down --volumes") + docker_compose(context, "down --remove-orphans --volumes") + + +@task +def export(context): + """Export docker compose configuration to `compose.yaml` file. + + Useful to: + + - Debug docker compose configuration. + - Allow using `docker compose` command directly without invoke. + """ + docker_compose(context, "convert > compose.yaml") + + +@task(name="ps", help={"all": "Show all, including stopped containers"}) +def ps_task(context, all=False): + """List containers.""" + docker_compose(context, f"ps {'--all' if all else ''}") @task @@ -191,12 +251,12 @@ def vscode(context): @task( help={ - "service": "docker compose service name to view (default: nautobot)", - "follow": "Follow logs", - "tail": "Tail N number of lines or 'all'", + "service": "If specified, only display logs for this service (default: all)", + "follow": "Flag to follow logs (default: False)", + "tail": "Tail N number of lines (default: all)", } ) -def logs(context, service="nautobot", follow=False, tail=None): +def logs(context, service="", follow=False, tail=0): """View the logs of a docker compose service.""" command = "logs " @@ -205,18 +265,21 @@ def logs(context, service="nautobot", follow=False, tail=None): if tail: command += f"--tail={tail} " - command += service - docker_compose(context, command) + docker_compose(context, command, service=service) # ------------------------------------------------------------------------------ # ACTIONS # ------------------------------------------------------------------------------ -@task -def nbshell(context): +@task(help={"file": "Python file to execute"}) +def nbshell(context, file=""): """Launch an interactive nbshell session.""" - command = "nautobot-server nbshell" - run_command(context, command) + command = [ + "nautobot-server", + "nbshell", + f"< '{file}'" if file else "", + ] + run_command(context, " ".join(command), pty=not bool(file)) @task @@ -228,7 +291,7 @@ def shell_plus(context): @task def cli(context): - """Launch a bash shell inside the running Nautobot container.""" + """Launch a bash shell inside the Nautobot container.""" run_command(context, "bash") @@ -286,6 +349,165 @@ def post_upgrade(context): run_command(context, command) +@task( + help={ + "service": "Docker compose service name to run command in (default: nautobot).", + "command": "Command to run (default: bash).", + "file": "File to run command with (default: empty)", + }, +) +def exec(context, service="nautobot", command="bash", file=""): + """Launch a command inside the running container (defaults to bash shell inside nautobot container).""" + command = [ + "exec", + "--", + service, + command, + f"< '{file}'" if file else "", + ] + docker_compose(context, " ".join(command), pty=not bool(file)) + + +@task( + help={ + "db-name": "Database name (default: Nautobot database)", + "input-file": "SQL file to execute and quit (default: empty, start interactive CLI)", + "output-file": "Ouput file, overwrite if exists (default: empty, output to stdout)", + "query": "SQL command to execute and quit (default: empty)", + } +) +def dbshell(context, db_name="", input_file="", output_file="", query=""): + """Start database CLI inside the running `db` container. + + Doesn't use `nautobot-server dbshell`, using started `db` service container only. + """ + if input_file and query: + raise ValueError("Cannot specify both, `input_file` and `query` arguments") + if output_file and not (input_file or query): + raise ValueError("`output_file` argument requires `input_file` or `query` argument") + + env = {} + if query: + env["_SQL_QUERY"] = query + + command = [ + "exec", + "--env=_SQL_QUERY" if query else "", + "-- db sh -c '", + ] + + if _is_compose_included(context, "mysql"): + command += [ + "mysql", + "--user=$MYSQL_USER", + "--password=$MYSQL_PASSWORD", + f"--database={db_name or '$MYSQL_DATABASE'}", + ] + elif _is_compose_included(context, "postgres"): + command += [ + "psql", + "--username=$POSTGRES_USER", + f"--dbname={db_name or '$POSTGRES_DB'}", + ] + else: + raise ValueError("Unsupported database backend.") + + command += [ + "'", + '<<<"$_SQL_QUERY"' if query else "", + f"< '{input_file}'" if input_file else "", + f"> '{output_file}'" if output_file else "", + ] + + docker_compose(context, " ".join(command), env=env, pty=not (input_file or output_file or query)) + + +@task( + help={ + "input-file": "SQL dump file to replace the existing database with. This can be generated using `invoke backup-db` (default: `dump.sql`).", + } +) +def import_db(context, input_file="dump.sql"): + """Stop Nautobot containers and replace the current database with the dump into the running `db` container.""" + docker_compose(context, "stop -- nautobot worker") + + command = ["exec -- db sh -c '"] + + if _is_compose_included(context, "mysql"): + command += [ + "mysql", + "--database=$MYSQL_DATABASE", + "--user=$MYSQL_USER", + "--password=$MYSQL_PASSWORD", + ] + elif _is_compose_included(context, "postgres"): + command += [ + "psql", + "--username=$POSTGRES_USER", + "postgres", + ] + else: + raise ValueError("Unsupported database backend.") + + command += [ + "'", + f"< '{input_file}'", + ] + + docker_compose(context, " ".join(command), pty=False) + + print("Database import complete, you can start Nautobot now: `invoke start`") + + +@task( + help={ + "db-name": "Database name to backup (default: Nautobot database)", + "output-file": "Ouput file, overwrite if exists (default: `dump.sql`)", + "readable": "Flag to dump database data in more readable format (default: `True`)", + } +) +def backup_db(context, db_name="", output_file="dump.sql", readable=True): + """Dump database into `output_file` file from running `db` container.""" + command = ["exec -- db sh -c '"] + + if _is_compose_included(context, "mysql"): + command += [ + "mysqldump", + "--user=root", + "--password=$MYSQL_ROOT_PASSWORD", + "--add-drop-database", + "--skip-extended-insert" if readable else "", + "--databases", + db_name if db_name else "$MYSQL_DATABASE", + ] + elif _is_compose_included(context, "postgres"): + command += [ + "pg_dump", + "--clean", + "--create", + "--if-exists", + "--username=$POSTGRES_USER", + f"--dbname={db_name or '$POSTGRES_DB'}", + "--inserts" if readable else "", + ] + else: + raise ValueError("Unsupported database backend.") + + command += [ + "'", + f"> '{output_file}'", + ] + + docker_compose(context, " ".join(command), pty=False) + + print(50 * "=") + print("The database backup has been successfully completed and saved to the following file:") + print(output_file) + print("You can import this database backup with the following command:") + print(f"invoke import-db --input-file '{output_file}'") + print(50 * "=") + + # ------------------------------------------------------------------------------ # DOCS # ------------------------------------------------------------------------------ @@ -295,10 +517,10 @@ def docs(context): command = "mkdocs serve -v" if is_truthy(context.nautobot_design_builder.local): - print("Serving Documentation...") + print(">>> Serving Documentation at http://localhost:8001") run_command(context, command) else: - print("Only used when developing locally (i.e. context.nautobot_design_builder.local=True)!") + start(context, service="docs") @task @@ -378,7 +600,7 @@ def bandit(context): @task def yamllint(context): - """Run yamllint to validate formating adheres to NTC defined YAML standards. + """Run yamllint to validate formatting adheres to NTC defined YAML standards. Args: context (obj): Used to run specific commands @@ -390,7 +612,7 @@ def yamllint(context): @task def check_migrations(context): """Check for missing migrations.""" - command = "nautobot-server --config=nautobot/core/tests/nautobot_config.py makemigrations --dry-run --check" + command = "nautobot-server makemigrations --dry-run --check" run_command(context, command) @@ -401,9 +623,19 @@ def check_migrations(context): "label": "specify a directory or module to test instead of running all Nautobot tests", "failfast": "fail as soon as a single test fails don't run the entire test suite", "buffer": "Discard output from passing tests", + "pattern": "Run specific test methods, classes, or modules instead of all tests", + "verbose": "Enable verbose test output.", } ) -def unittest(context, keepdb=False, label="nautobot_design_builder", failfast=False, buffer=True): +def unittest( + context, + keepdb=False, + label="nautobot_design_builder", + failfast=False, + buffer=True, + pattern="", + verbose=False, +): """Run Nautobot unit tests.""" command = f"coverage run --module nautobot.core.cli test {label}" @@ -413,6 +645,11 @@ def unittest(context, keepdb=False, label="nautobot_design_builder", failfast=Fa command += " --failfast" if buffer: command += " --buffer" + if pattern: + command += f" -k='{pattern}'" + if verbose: + command += " --verbosity 2" + run_command(context, command) @@ -426,10 +663,12 @@ def unittest_coverage(context): @task( help={ - "failfast": "fail as soon as a single test fails don't run the entire test suite", + "failfast": "fail as soon as a single test fails don't run the entire test suite. (default: False)", + "keepdb": "Save and re-use test database between test runs for faster re-testing. (default: False)", + "lint-only": "Only run linters; unit tests will be excluded. (default: False)", } ) -def tests(context, failfast=False): +def tests(context, failfast=False, keepdb=False, lint_only=False): """Run all tests for this plugin.""" # If we are not running locally, start the docker containers so we don't have to for each test if not is_truthy(context.nautobot_design_builder.local): @@ -446,9 +685,16 @@ def tests(context, failfast=False): pydocstyle(context) print("Running yamllint...") yamllint(context) + print("Running poetry check...") + lock(context, check=True) + print("Running migrations check...") + check_migrations(context) print("Running pylint...") pylint(context) - print("Running unit tests...") - unittest(context, failfast=failfast) + print("Running mkdocs...") + build_and_check_docs(context) + if not lint_only: + print("Running unit tests...") + unittest(context, failfast=failfast, keepdb=keepdb) + unittest_coverage(context) print("All tests have passed!") - unittest_coverage(context) From 321eb817552207dcd956cf139db14544d27a6259 Mon Sep 17 00:00:00 2001 From: Jan Snasel Date: Tue, 9 Jan 2024 12:42:54 +0000 Subject: [PATCH 2/8] chore: Manual fixes --- .github/workflows/ci.yml | 2 +- README.md | 30 +------ development/development.env | 1 + development/docker-compose.dev.yml | 4 + development/nautobot_config.py | 17 ++-- docs/admin/compatibility_matrix.md | 2 +- docs/admin/install.md | 19 +---- docs/admin/release_notes/version_1.0.md | 43 +--------- docs/admin/uninstall.md | 3 - docs/admin/upgrade.md | 7 +- docs/dev/arch_decision.md | 7 -- docs/dev/code_reference/index.md | 3 - docs/dev/contributing.md | 19 +++-- docs/dev/extending.md | 39 ++++++++- docs/images/icon-design-builder.png | Bin 74601 -> 0 bytes docs/requirements.txt | 2 +- docs/user/app_getting_started.md | 26 ++++-- docs/user/app_overview.md | 18 ++-- docs/user/app_use_cases.md | 12 --- docs/user/external_interactions.md | 17 ---- docs/user/faq.md | 4 + mkdocs.yml | 11 ++- nautobot_design_builder/__init__.py | 8 ++ nautobot_design_builder/api/__init__.py | 1 - .../migrations/__init__.py | 0 nautobot_design_builder/tests/__init__.py | 78 ++++++++++++++++++ nautobot_design_builder/tests/test_api.py | 28 ------- pyproject.toml | 8 +- 28 files changed, 209 insertions(+), 200 deletions(-) delete mode 100644 docs/dev/arch_decision.md delete mode 100644 docs/images/icon-design-builder.png delete mode 100644 docs/user/app_use_cases.md delete mode 100644 docs/user/external_interactions.md delete mode 100644 nautobot_design_builder/api/__init__.py delete mode 100644 nautobot_design_builder/migrations/__init__.py delete mode 100644 nautobot_design_builder/tests/test_api.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a163c061..8bb24e72 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -243,7 +243,7 @@ jobs: - name: "Upload binaries to release" uses: "svenstaro/upload-release-action@v2" with: - repo_token: "${{ secrets.NTC_GITHUB_TOKEN }}" # use GH_NAUTOBOT_BOT_TOKEN for Nautobot Org repos. + repo_token: "${{ secrets.GH_NAUTOBOT_BOT_TOKEN }}" file: "dist/*" tag: "${{ github.ref }}" overwrite: true diff --git a/README.md b/README.md index f0f50847..e77e3149 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,5 @@ # Nautobot Design Builder - -


@@ -23,25 +13,7 @@ To avoid extra work and temporary links, make sure that publishing docs (or merg ## Overview -> Developer Note: Add a long (2-3 paragraphs) description of what the App does, what problems it solves, what functionality it adds to Nautobot, what external systems it works with etc. - -### Screenshots - -> Developer Note: Add any representative screenshots of the App in action. These images should also be added to the `docs/user/app_use_cases.md` section. - -> Developer Note: Place the files in the `docs/images/` folder and link them using only full URLs from GitHub, for example: `![Overview](https://raw.githubusercontent.com/nautobot/nautobot-app-design-builder/develop/docs/images/plugin-overview.png)`. This absolute static linking is required to ensure the README renders properly in GitHub, the docs site, and any other external sites like PyPI. - -More screenshots can be found in the [Using the App](https://docs.nautobot.com/projects/nautobot-design-builder/en/latest/user/app_use_cases/) page in the documentation. Here's a quick overview of some of the plugin's added functionality: - -![](https://raw.githubusercontent.com/nautobot/nautobot-app-design-builder/develop/docs/images/placeholder.png) - -## Try it out! - -> Developer Note: Only keep this section if appropriate. Update link to correct sandbox. - -This App is installed in the Nautobot Community Sandbox found over at [demo.nautobot.com](https://demo.nautobot.com/)! - -> For a full list of all the available always-on sandbox environments, head over to the main page on [networktocode.com](https://www.networktocode.com/nautobot/sandbox-environments/). +Design Builder is a Nautobot application for easily populating data within Nautobot using standardized design files. These design files are just Jinja templates that describe the Nautobot objects to be created or updated. ## Documentation diff --git a/development/development.env b/development/development.env index 54f0b870..7613c1e2 100644 --- a/development/development.env +++ b/development/development.env @@ -11,6 +11,7 @@ NAUTOBOT_LOG_LEVEL=DEBUG NAUTOBOT_METRICS_ENABLED=True NAUTOBOT_NAPALM_TIMEOUT=5 NAUTOBOT_MAX_PAGE_SIZE=0 +NAUTOBOT_INSTALLATION_METRICS_ENABLED = False # Redis Configuration Environment Variables NAUTOBOT_REDIS_HOST=redis diff --git a/development/docker-compose.dev.yml b/development/docker-compose.dev.yml index 2201007b..28916504 100644 --- a/development/docker-compose.dev.yml +++ b/development/docker-compose.dev.yml @@ -12,6 +12,8 @@ services: volumes: - "./nautobot_config.py:/opt/nautobot/nautobot_config.py" - "../:/source" + - "../examples/backbone_design/designs:/opt/nautobot/designs:cached" + - "../examples/backbone_design/jobs:/opt/nautobot/jobs:cached" healthcheck: test: ["CMD", "true"] # Due to layering, disable: true won't work. Instead, change the test docs: @@ -32,6 +34,8 @@ services: volumes: - "./nautobot_config.py:/opt/nautobot/nautobot_config.py" - "../:/source" + - "../examples/backbone_design/designs:/opt/nautobot/designs:cached" + - "../examples/backbone_design/jobs:/opt/nautobot/jobs:cached" healthcheck: test: ["CMD", "true"] # Due to layering, disable: true won't work. Instead, change the test # To expose postgres or redis to the host uncomment the following diff --git a/development/nautobot_config.py b/development/nautobot_config.py index d09d2fea..e9959dc2 100644 --- a/development/nautobot_config.py +++ b/development/nautobot_config.py @@ -4,6 +4,8 @@ from nautobot.core.settings import * # noqa: F403 # pylint: disable=wildcard-import,unused-wildcard-import from nautobot.core.settings_funcs import is_truthy, parse_redis_connection +from importlib import metadata +from packaging.version import Version # # Debug @@ -133,9 +135,12 @@ # Apps configuration settings. These settings are used by various Apps that the user may have installed. # Each key in the dictionary is the name of an installed App and its value is a dictionary of settings. -# PLUGINS_CONFIG = { -# 'nautobot_design_builder': { -# 'foo': 'bar', -# 'buzz': 'bazz' -# } -# } + +# TODO: The following is necessary only until BGP models plugin +# is officially supported in 2.0 +nautobot_version = Version(Version(metadata.version("nautobot")).base_version) + +if nautobot_version < Version("2.0"): + PLUGINS.append("nautobot_bgp_models") + +PLUGINS_CONFIG = {"design_builder": {"context_repository": os.getenv("DESIGN_BUILDER_CONTEXT_REPO_SLUG", None)}} diff --git a/docs/admin/compatibility_matrix.md b/docs/admin/compatibility_matrix.md index 697069a1..4a032595 100644 --- a/docs/admin/compatibility_matrix.md +++ b/docs/admin/compatibility_matrix.md @@ -5,4 +5,4 @@ | Nautobot Design Builder Version | Nautobot First Support Version | Nautobot Last Support Version | | ------------- | -------------------- | ------------- | -| 1.0.X | 1.6.8 | 1.99.99 | +| 1.0.X | 1.6.8 | 2.0.X | diff --git a/docs/admin/install.md b/docs/admin/install.md index 4695cdd2..f9b94932 100644 --- a/docs/admin/install.md +++ b/docs/admin/install.md @@ -2,9 +2,6 @@ Here you will find detailed instructions on how to **install** and **configure** the App within your Nautobot environment. -!!! warning "Developer Note - Remove Me!" - Detailed instructions on installing the App. You will need to update this section based on any additional dependencies or prerequisites. - ## Prerequisites - The plugin is compatible with Nautobot 1.6.8 and higher. @@ -15,8 +12,7 @@ Here you will find detailed instructions on how to **install** and **configure** ### Access Requirements -!!! warning "Developer Note - Remove Me!" - What external systems (if any) it needs access to in order to work. +Design Builder does not necessarily require any external system access. However, if design jobs will be loaded from a git repository, then the Nautobot instances will need access to the git repo. ## Install Guide @@ -66,16 +62,3 @@ Then restart (if necessary) the Nautobot services which may include: ```shell sudo systemctl restart nautobot nautobot-worker nautobot-scheduler ``` - -## App Configuration - -!!! warning "Developer Note - Remove Me!" - Any configuration required to get the App set up. Edit the table below as per the examples provided. - -The plugin behavior can be controlled with the following list of settings: - -| Key | Example | Default | Description | -| ------- | ------ | -------- | ------------------------------------- | -| `enable_backup` | `True` | `True` | A boolean to represent whether or not to run backup configurations within the plugin. | -| `platform_slug_map` | `{"cisco_wlc": "cisco_aireos"}` | `None` | A dictionary in which the key is the platform slug and the value is what netutils uses in any "network_os" parameter. | -| `per_feature_bar_width` | `0.15` | `0.15` | The width of the table bar within the overview report | diff --git a/docs/admin/release_notes/version_1.0.md b/docs/admin/release_notes/version_1.0.md index e2342da4..9076e6ae 100644 --- a/docs/admin/release_notes/version_1.0.md +++ b/docs/admin/release_notes/version_1.0.md @@ -1,48 +1,11 @@ # v1.0 Release Notes -!!! warning "Developer Note - Remove Me!" - Guiding Principles: - - - Changelogs are for humans, not machines. - - There should be an entry for every single version. - - The same types of changes should be grouped. - - Versions and sections should be linkable. - - The latest version comes first. - - The release date of each version is displayed. - - Mention whether you follow Semantic Versioning. - - Types of changes: - - - `Added` for new features. - - `Changed` for changes in existing functionality. - - `Deprecated` for soon-to-be removed features. - - `Removed` for now removed features. - - `Fixed` for any bug fixes. - - `Security` in case of vulnerabilities. - - This document describes all new features and changes in the release `1.0`. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Release Overview -- Major features or milestones -- Achieved in this `x.y` release -- Changes to compatibility with Nautobot and/or other plugins, libraries etc. - -## [v1.0.1] - 2021-09-08 - -### Added - -### Changed - -### Fixed - -- [#123](https://github.com/nautobot/nautobot-app-design-builder/issues/123) Fixed Tag filtering not working in job launch form - -## [v1.0.0] - 2021-08-03 - -### Added +Initial Public Release -### Changed +## [v1.0.0] - 2023-11-01 -### Fixed +Initial Public Release diff --git a/docs/admin/uninstall.md b/docs/admin/uninstall.md index 3481dce0..63cda675 100644 --- a/docs/admin/uninstall.md +++ b/docs/admin/uninstall.md @@ -10,9 +10,6 @@ Prior to removing the plugin from the `nautobot_config.py`, run the following co nautobot-server migrate nautobot_app_design_builder zero ``` -!!! warning "Developer Note - Remove Me!" - Any other cleanup operations to ensure the database is clean after the app is removed. Is there anything else that needs cleaning up, such as CFs, relationships, etc. if they're no longer desired? - ## Remove App configuration Remove the configuration you added in `nautobot_config.py` from `PLUGINS` & `PLUGINS_CONFIG`. diff --git a/docs/admin/upgrade.md b/docs/admin/upgrade.md index a9ba697a..73ca07d2 100644 --- a/docs/admin/upgrade.md +++ b/docs/admin/upgrade.md @@ -4,7 +4,10 @@ Here you will find any steps necessary to upgrade the App in your Nautobot envir ## Upgrade Guide -!!! warning "Developer Note - Remove Me!" - Add more detailed steps on how the app is upgraded in an existing Nautobot setup and any version specifics (such as upgrading between major versions with breaking changes). +Since Design Builder does not currently include any custom data models the only requirement for updating is to update the `nautobot-design-builder` package using the `pip` command: + +```python +pip install --upgrade nautobot-design-builder +``` When a new release comes out it may be necessary to run a migration of the database to account for any changes in the data models used by this plugin. Execute the command `nautobot-server post-upgrade` within the runtime environment of your Nautobot installation after updating the `nautobot-design-builder` package via `pip`. diff --git a/docs/dev/arch_decision.md b/docs/dev/arch_decision.md deleted file mode 100644 index e7bcbbe4..00000000 --- a/docs/dev/arch_decision.md +++ /dev/null @@ -1,7 +0,0 @@ -# Architecture Decision Records - -The intention is to document deviations from a standard Model View Controller (MVC) design. - -!!! warning "Developer Note - Remove Me!" - Optional page, remove if not applicable. - For examples see [Golden Config](https://github.com/nautobot/nautobot-plugin-golden-config/tree/develop/docs/dev/dev_adr.md) and [nautobot-plugin-reservation](https://github.com/networktocode/nautobot-plugin-reservation/blob/develop/docs/dev/dev_adr.md). diff --git a/docs/dev/code_reference/index.md b/docs/dev/code_reference/index.md index ebe9ff7d..473f2c40 100644 --- a/docs/dev/code_reference/index.md +++ b/docs/dev/code_reference/index.md @@ -1,6 +1,3 @@ # Code Reference Auto-generated code reference documentation from docstrings. - -!!! warning "Developer Note - Remove Me!" - Uses [mkdocstrings](https://mkdocstrings.github.io/) syntax to auto-generate code documentation from docstrings. Two example pages are provided ([api](api.md) and [package](package.md)), add new stubs for each module or package that you think has relevant documentation. diff --git a/docs/dev/contributing.md b/docs/dev/contributing.md index 2337f740..2d239fb3 100644 --- a/docs/dev/contributing.md +++ b/docs/dev/contributing.md @@ -1,7 +1,8 @@ # Contributing to the App -!!! warning "Developer Note - Remove Me!" - Information on how to contribute fixes, functionality, or documentation changes back to the project. +Contributions are encouraged and we are always delighted in any form of work. We are always looking for feedback both in the development of code as well as documentation, use cases, and examples. To contribute to this project, please use the following guidlines: + +## Code Development The project is packaged with a light [development environment](dev_environment.md) based on `docker-compose` to help with the local development of the project and to run tests. @@ -13,12 +14,18 @@ The project is following Network to Code software development guidelines and is Documentation is built using [mkdocs](https://www.mkdocs.org/). The [Docker based development environment](dev_environment.md#docker-development-environment) automatically starts a container hosting a live version of the documentation website on [http://localhost:8001](http://localhost:8001) that auto-refreshes when you make any changes to your local files. +## Documentation + +Code documentation follows the [Google docstring](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) style. Where possible, include a description, argument documentation and examples. + +The user and developer documentation is located in the top level `docs/` directory. The documenation is written in markdown format and is rendered using MkDocs. + +Example designs should be placed in the top level `examples/` directory, as appropriate. + ## Branching Policy -!!! warning "Developer Note - Remove Me!" - What branching policy is used for this project and where contributions should be made. +The active branch in Design Builder is the `develop` branch. However, commits are not allowed directly to this branch. Instead, fork the code and open a pull request to `develop`. ## Release Policy -!!! warning "Developer Note - Remove Me!" - How new versions are released. +There is no set release schedule for this App. New releases will be published as appropriate when new features and/or bug fixes are ready. diff --git a/docs/dev/extending.md b/docs/dev/extending.md index 49b89f46..a9952735 100644 --- a/docs/dev/extending.md +++ b/docs/dev/extending.md @@ -1,6 +1,39 @@ # Extending the App -!!! warning "Developer Note - Remove Me!" - Information on how to extend the App functionality. +Design builder is primarily extended by creating new action tags. These action tags can be provided by a design repository or they can be contributed to the upstream Design Builder project for consumption by the community. Upstreaming these extensions is welcome, however it is best to open an issue first, to ensure that a PR would be accepted and makes sense in terms of features and design. -Extending the application is welcome, however it is best to open an issue first, to ensure that a PR would be accepted and makes sense in terms of features and design. +## Action Tag Extensions + +The action tags in Design Builder are provided by `design.Builder`. This component reads a design and then executes instructions that are specified in the design. Basic functions, provided out of the box, are +`create`, `create_or_update` and `update`. These actions are self explanatory (for details on syntax see [this document](../user//design_development.md#special-syntax)). Two additional actions are provided, these are the `ref` and `git_context` actions. These two actions are provided as extensions to the builder. + +Extensions specify attribute and/or value actions to the object creator. Within a design template, these extensions can be used by specifying an exclamation point (!) followed by the extensions attribute or value tag. For instance, the `ref` extension implements both an attribute and a value extension. This extension can be used by specifying `!ref`. Extensions can add behavior to the object creator that is not supplied by the standard create and update actions. + +### Attribute Extensions + +Attribute extensions provide some functionality when specified as a YAMl attribute. For instance: + +```yaml +devices: + name: My New Device + "!my_attribute_extension": "some data passed to the extensions" +``` + +In this case, when the object creator encountered `!my_attribute_extension` it will look for an extension that specifies an attribute_tag `my_attribute_extension` and will call the associated `attribute` method on that extension. The `attribute` method will be given the object that is being worked on (the device "My New Device" in this case) as well as the value assigned to the attribute (the string "some data ..." in this case). Values can be any supported YAML type including strings, dictionaries and lists. It is up to the extension to determine if the provided value is valid or not. + +### Value Extensions + +Value extensions can be used to assign a value to an attribute. For instance: + +```yaml +device: + name: "!device_name" +``` + +In this case, when `!device_name` is encountered the object creator will look for an extension that implements the `device_name` value tag. If found, the corresponding `value` method will be called on the extension. Whatever `value` returns will be assigned to the attribute (`name` in this case). For a concrete example of an extension that implements both `attribute` and `value` see the [API docs](./code_reference/ext.md#design_builder.ext.ReferenceExtension) for the ReferenceExtension. + +### Writing a New Extension + +Adding functionality to `design.Builder` is as simple extending the [Extension](./code_reference/ext.md#design_builder.ext.Extension) class and supplying `attribute_tag` and/or `value_tag` class variables as well as the corresponding `attribute` and `value` instance methods. Extensions are singletons within a Builder instance. When an extension's tag is encountered an instance of the extension is created. Subsequent calls to the extension will use the instance created the first time. + +Each extension may optionally implement `commit` or `roll_back` methods. The `commit` method is called once all of a design's objects have been created and updated in the database. Conversely, `roll_back` is called if any error occurs and the database transaction is aborted. These methods provide a means for an extension to perform additional work, or cleanup, based on the outcome of a design's database actions. diff --git a/docs/images/icon-design-builder.png b/docs/images/icon-design-builder.png deleted file mode 100644 index 7e00cf6ae0ee76324adab30d68d64206678a85e1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 74601 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

@5hb{hC=~cCz;d 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 diff --git a/docs/requirements.txt b/docs/requirements.txt index d168c88f..4a079cd3 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,5 @@ mkdocs==1.5.2 -mkdocs-material==9.1.15 +mkdocs-material==9.2.4 mkdocs-version-annotations==1.0.0 mkdocstrings-python==1.5.2 mkdocstrings==0.22.0 diff --git a/docs/user/app_getting_started.md b/docs/user/app_getting_started.md index 24ea0099..8dc87972 100644 --- a/docs/user/app_getting_started.md +++ b/docs/user/app_getting_started.md @@ -8,12 +8,28 @@ To install the App, please follow the instructions detailed in the [Installation ## First steps with the App -!!! warning "Developer Note - Remove Me!" - What (with screenshots preferably) does it look like to perform the simplest workflow within the App once installed? +The easiest way to experience Design Builder is to run it in a local environment. To start a local environment, clone the design builder git repository and start the application stack. The only requirements for starting a local environment are `docker`, `docker-compose` and [invoke](https://www.pyinvoke.org/installing.html). Once the dependent tools have been installed you'll need to build the docker image by running `invoke build`. At that point, simply run the command `invoke start`. This will start the entire application stack using docker compose. Once the application stack is up and running, navigate to and login. ## What are the next steps? -!!! warning "Developer Note - Remove Me!" - After taking the first steps, what else could the users look at doing. +The Design Builder application ships with some sample designs to demonstrate capabilities. Once the application stack is ready, you should have two designs listed under the "Jobs" -> "Jobs" menu item. -You can check out the [Use Cases](app_use_cases.md) section for more examples. +![Jobs list](../images/screenshots/sample-design-jobs-list.png) + +Note that both jobs are disabled. Nautobot automatically marks jobs as disabled when they are first loaded. In order to run these jobs, click the edit button ![edit button](../images/screenshots/edit-button.png) and check the "enabled" checkbox: + +![enabled checkbox](../images/screenshots/job-enabled-checkbox.png) + +Once you click `save`, the jobs should be runnable. + +To implement any design, click the run button [run button](../images/screenshots/run-button.png). For example, run the "Initial Data" job, which will add a manufacturer, a device type, a device role, several regions and several sites. Additionally, each site will have two devices. Here is the design template for this design: + +```jinja +--8<-- "examples/backbone_design/designs/core_site/designs/0001_design.yaml.j2" +``` + +If you run the job you should see output in the job result that shows the various objects being created: + +![design job result](../images/screenshots/design-job-result.png) + +Once the initial data job has been run, try enabling and running the "Backbone Site Design" job to create a new site with racks and routers. diff --git a/docs/user/app_overview.md b/docs/user/app_overview.md index 06ff5d32..d7f79768 100644 --- a/docs/user/app_overview.md +++ b/docs/user/app_overview.md @@ -7,24 +7,18 @@ This document provides an overview of the App including critical information and ## Description +Design Builder provides a system where standardized network designs can be developed to produce collections of objects within Nautobot. These designs are text based templates that can create and update hierarchical data structures within Nautobot. ## Audience (User Personas) - Who should use this App? -!!! warning "Developer Note - Remove Me!" - Who is this meant for/ who is the common user of this app? +- Network engineers who want to have reproducible sets of Nautobot objects based on some standard design. +- Automation engineers who want to be able to automate the creation of Nautobot objects based on a set of standard designs. ## Authors and Maintainers -!!! warning "Developer Note - Remove Me!" - Add the team and/or the main individuals maintaining this project. Include historical maintainers as well. +- Andrew Bates (@abates) +- Mzb (@mzbroch) ## Nautobot Features Used -!!! warning "Developer Note - Remove Me!" - What is shown today in the Installed Plugins page in Nautobot. What parts of Nautobot does it interact with, what does it add etc. ? - -### Extras - -!!! warning "Developer Note - Remove Me!" - Custom Fields - things like which CFs are created by this app? - Jobs - are jobs, if so, which ones, installed by this app? +This application interacts directly with Nautobot's Object Relational Mapping (ORM) system. diff --git a/docs/user/app_use_cases.md b/docs/user/app_use_cases.md deleted file mode 100644 index dc06944f..00000000 --- a/docs/user/app_use_cases.md +++ /dev/null @@ -1,12 +0,0 @@ -# Using the App - -This document describes common use-cases and scenarios for this App. - -## General Usage - -## Use-cases and common workflows - -## Screenshots - -!!! warning "Developer Note - Remove Me!" - Ideally captures every view exposed by the App. Should include a relevant dataset. diff --git a/docs/user/external_interactions.md b/docs/user/external_interactions.md deleted file mode 100644 index eaba5b56..00000000 --- a/docs/user/external_interactions.md +++ /dev/null @@ -1,17 +0,0 @@ -# External Interactions - -This document describes external dependencies and prerequisites for this App to operate, including system requirements, API endpoints, interconnection or integrations to other applications or services, and similar topics. - -!!! warning "Developer Note - Remove Me!" - Optional page, remove if not applicable. - -## External System Integrations - -### From the App to Other Systems - -### From Other Systems to the App - -## Nautobot REST API endpoints - -!!! warning "Developer Note - Remove Me!" - API documentation in this doc - including python request examples, curl examples, postman collections referred etc. diff --git a/docs/user/faq.md b/docs/user/faq.md index 318b08dc..346f565b 100644 --- a/docs/user/faq.md +++ b/docs/user/faq.md @@ -1 +1,5 @@ # Frequently Asked Questions + +## When importing designs from git using the Nautobot Git Repositories feature, what should I select for the `Provides` field? + +Design builder design's are an extension of the existing Nautobot Job's functionality. Therefore, any repository containing design jobs should select the `jobs` option in the `Provides` field. diff --git a/mkdocs.yml b/mkdocs.yml index 45425f61..92e79e9a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -101,9 +101,9 @@ nav: - User Guide: - App Overview: "user/app_overview.md" - Getting Started: "user/app_getting_started.md" - - Using the App: "user/app_use_cases.md" + - Design Quick Start: "user/design_quickstart.md" + - Design Development: "user/design_development.md" - Frequently Asked Questions: "user/faq.md" - - External Interactions: "user/external_interactions.md" - Administrator Guide: - Install and Configure: "admin/install.md" - Upgrade: "admin/upgrade.md" @@ -116,9 +116,14 @@ nav: - Extending the App: "dev/extending.md" - Contributing to the App: "dev/contributing.md" - Development Environment: "dev/dev_environment.md" - - Architecture Decision Records: "dev/arch_decision.md" - Code Reference: - "dev/code_reference/index.md" - Package: "dev/code_reference/package.md" - API: "dev/code_reference/api.md" + - Design Job: "dev/code_reference/design_job.md" + - Context: "dev/code_reference/context.md" + - Design Builder: "dev/code_reference/design.md" + - Jinja Rendering: "dev/code_reference/jinja2.md" + - Template Extensions: "dev/code_reference/ext.md" + - Util: "dev/code_reference/util.md" - Nautobot Docs Home ↗︎: "https://docs.nautobot.com" diff --git a/nautobot_design_builder/__init__.py b/nautobot_design_builder/__init__.py index 79d1d6c2..e6c1dd23 100644 --- a/nautobot_design_builder/__init__.py +++ b/nautobot_design_builder/__init__.py @@ -1,4 +1,6 @@ """Plugin declaration for nautobot_design_builder.""" +from django.conf import settings +from django.utils.functional import classproperty # Metadata is inherited from Nautobot. If not including Nautobot in the environment, this should be added from importlib import metadata @@ -22,5 +24,11 @@ class NautobotDesignBuilderConfig(NautobotAppConfig): default_settings = {} caching_config = {} + # pylint: disable=no-self-argument + @classproperty + def context_repository(cls): + """Retrieve the Git Repository slug that has been configured for the Design Builder.""" + return settings.PLUGINS_CONFIG[cls.name]["context_repository"] + config = NautobotDesignBuilderConfig # pylint:disable=invalid-name diff --git a/nautobot_design_builder/api/__init__.py b/nautobot_design_builder/api/__init__.py deleted file mode 100644 index e61c518c..00000000 --- a/nautobot_design_builder/api/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""REST API module for nautobot_design_builder plugin.""" diff --git a/nautobot_design_builder/migrations/__init__.py b/nautobot_design_builder/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/nautobot_design_builder/tests/__init__.py b/nautobot_design_builder/tests/__init__.py index 78db10c4..0b0a2d30 100644 --- a/nautobot_design_builder/tests/__init__.py +++ b/nautobot_design_builder/tests/__init__.py @@ -1 +1,79 @@ """Unit tests for nautobot_design_builder plugin.""" + +import logging +import shutil +import tempfile +from os import path +from typing import Type +from unittest import mock +from unittest.mock import PropertyMock, patch + +from django.test import TestCase + +from nautobot_design_builder.design_job import DesignJob +from nautobot_design_builder.util import nautobot_version + +logging.disable(logging.CRITICAL) + + +class DesignTestCase(TestCase): + """DesignTestCase aides in creating unit tests for design jobs and templates.""" + + def setUp(self): + """Setup a mock git repo to watch for config context creation.""" + super().setUp() + self.logged_messages = [] + self.git_patcher = patch("nautobot_design_builder.ext.GitRepo") + self.git_mock = self.git_patcher.start() + + self.git_path = tempfile.mkdtemp() + git_instance_mock = PropertyMock() + git_instance_mock.return_value.path = self.git_path + self.git_mock.side_effect = git_instance_mock + + def get_mocked_job(self, design_class: Type[DesignJob]): + """Create an instance of design_class and properly mock request and job_result for testing.""" + job = design_class() + job.job_result = mock.Mock() + if nautobot_version < "2.0.0": + job.request = mock.Mock() + else: + job.job_result.data = {} + old_run = job.run + + def new_run(data, commit): + kwargs = {**data} + kwargs["dryrun"] = not commit + old_run(**kwargs) + + job.run = new_run + self.logged_messages = [] + + def record_log(message, obj, level_choice, grouping=None, logger=None): # pylint: disable=unused-argument + self.logged_messages.append( + { + "message": message, + "obj": obj, + "level_choice": level_choice, + "grouping": grouping, + } + ) + + job.job_result.log.side_effect = record_log + return job + + def assert_context_files_created(self, *filenames): + """Confirm that the list of filenames were created as part of the design implementation.""" + for filename in filenames: + self.assertTrue(path.exists(path.join(self.git_path, filename)), f"{filename} was not created") + + def assertJobSuccess(self, job): # pylint: disable=invalid-name + """Assert that a mocked job has completed successfully.""" + if job.failed: + self.fail(f"Job failed with {self.logged_messages[-1]}") + + def tearDown(self): + """Remove temporary files.""" + self.git_patcher.stop() + shutil.rmtree(self.git_path) + super().tearDown() diff --git a/nautobot_design_builder/tests/test_api.py b/nautobot_design_builder/tests/test_api.py deleted file mode 100644 index ff93d198..00000000 --- a/nautobot_design_builder/tests/test_api.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Unit tests for nautobot_design_builder.""" -from django.contrib.auth import get_user_model -from django.test import TestCase -from django.urls import reverse -from rest_framework import status -from rest_framework.test import APIClient - -from nautobot.users.models import Token - -User = get_user_model() - - -class PlaceholderAPITest(TestCase): - """Test the NautobotDesignBuilder API.""" - - def setUp(self): - """Create a superuser and token for API calls.""" - self.user = User.objects.create(username="testuser", is_superuser=True) - self.token = Token.objects.create(user=self.user) - self.client = APIClient() - self.client.credentials(HTTP_AUTHORIZATION=f"Token {self.token.key}") - - def test_placeholder(self): - """Verify that devices can be listed.""" - url = reverse("dcim-api:device-list") - response = self.client.get(url) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data["count"], 0) diff --git a/pyproject.toml b/pyproject.toml index 0ddb3455..6ff8caf8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "nautobot-design-builder" -version = "0.1.0" +version = "0.4.4" description = "Nautobot app that uses design templates to easily create data objects in Nautobot with minimal input from a user." authors = ["Network to Code, LLC "] license = "Apache-2.0" @@ -52,13 +52,17 @@ nautobot-bgp-models = "*" # Rendering docs to HTML mkdocs = "1.5.2" # Material for MkDocs theme -mkdocs-material = "9.1.15" +mkdocs-material = "9.2.4" # Render custom markdown for version added/changed/remove notes mkdocs-version-annotations = "1.0.0" # Automatic documentation from sources, for MkDocs mkdocstrings = "0.22.0" mkdocstrings-python = "1.5.2" +[tool.poetry.extras] +nautobot = ["nautobot"] +# bgp_models = ["nautobot-bgp-models"] + [tool.black] line-length = 120 target-version = ['py38', 'py39', 'py310', 'py311'] From 4dd1e20af3aff71293be5e4bd1f7774ccb1ed7c4 Mon Sep 17 00:00:00 2001 From: Jan Snasel Date: Tue, 9 Jan 2024 12:43:29 +0000 Subject: [PATCH 3/8] chore: Poetry lock --- poetry.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 2f5a6f2b..2b2bfb9c 100755 --- a/poetry.lock +++ b/poetry.lock @@ -3712,4 +3712,4 @@ nautobot = ["nautobot"] [metadata] lock-version = "2.0" python-versions = ">=3.8,<3.12" -content-hash = "8ec4ecf63625568afcbfb1ff021f998969cf00efa320323c380084253a5323f3" +content-hash = "8ec4ecf63625568afcbfb1ff021f998969cf00efa320323c380084253a5323f3" \ No newline at end of file From 5ed10a1a2de592e40ccf7eb64dc5f8b69d394d5f Mon Sep 17 00:00:00 2001 From: Jan Snasel Date: Tue, 9 Jan 2024 13:02:53 +0000 Subject: [PATCH 4/8] fix: Docs --- docs/dev/code_reference/api.md | 5 ----- mkdocs.yml | 7 +++++-- 2 files changed, 5 insertions(+), 7 deletions(-) delete mode 100644 docs/dev/code_reference/api.md diff --git a/docs/dev/code_reference/api.md b/docs/dev/code_reference/api.md deleted file mode 100644 index 462c3467..00000000 --- a/docs/dev/code_reference/api.md +++ /dev/null @@ -1,5 +0,0 @@ -# Nautobot Design Builder API Package - -::: nautobot_design_builder.api - options: - show_submodules: True diff --git a/mkdocs.yml b/mkdocs.yml index 92e79e9a..67ff2df1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -72,6 +72,7 @@ extra: link: "https://twitter.com/networktocode" name: "Network to Code Twitter" markdown_extensions: + - "abbr" - "admonition" - "toc": permalink: true @@ -80,7 +81,9 @@ markdown_extensions: - "pymdownx.highlight": anchor_linenums: true - "pymdownx.inlinehilite" - - "pymdownx.snippets" + - "pymdownx.snippets": + auto_append: + - "docs/assets/abbreviations.md" - "pymdownx.superfences" - "footnotes" plugins: @@ -104,6 +107,7 @@ nav: - Design Quick Start: "user/design_quickstart.md" - Design Development: "user/design_development.md" - Frequently Asked Questions: "user/faq.md" + - Git-based Config Context: "user/git_config_context.md" - Administrator Guide: - Install and Configure: "admin/install.md" - Upgrade: "admin/upgrade.md" @@ -119,7 +123,6 @@ nav: - Code Reference: - "dev/code_reference/index.md" - Package: "dev/code_reference/package.md" - - API: "dev/code_reference/api.md" - Design Job: "dev/code_reference/design_job.md" - Context: "dev/code_reference/context.md" - Design Builder: "dev/code_reference/design.md" From f4c86de7ed8aa99dbb8a84553940a155fe98cefa Mon Sep 17 00:00:00 2001 From: Jan Snasel Date: Tue, 9 Jan 2024 13:03:21 +0000 Subject: [PATCH 5/8] fix: pylint --- nautobot_design_builder/__init__.py | 5 +++-- nautobot_design_builder/contrib/ext.py | 5 +++++ nautobot_design_builder/contrib/tests/test_ext.py | 10 ++++++++++ nautobot_design_builder/design.py | 1 + nautobot_design_builder/ext.py | 5 +++-- nautobot_design_builder/fields.py | 2 ++ nautobot_design_builder/jinja2.py | 3 +++ .../management/commands/build_design.py | 1 + nautobot_design_builder/tests/test_context.py | 8 ++++++++ nautobot_design_builder/tests/test_data_sources.py | 6 ++++++ nautobot_design_builder/tests/test_design_job.py | 4 ++++ nautobot_design_builder/tests/test_errors.py | 6 ++++++ nautobot_design_builder/tests/test_ext.py | 6 ++++++ nautobot_design_builder/util.py | 1 + 14 files changed, 59 insertions(+), 4 deletions(-) diff --git a/nautobot_design_builder/__init__.py b/nautobot_design_builder/__init__.py index e6c1dd23..0a908ca3 100644 --- a/nautobot_design_builder/__init__.py +++ b/nautobot_design_builder/__init__.py @@ -1,9 +1,10 @@ """Plugin declaration for nautobot_design_builder.""" -from django.conf import settings -from django.utils.functional import classproperty # Metadata is inherited from Nautobot. If not including Nautobot in the environment, this should be added from importlib import metadata +from django.conf import settings +from django.utils.functional import classproperty + __version__ = metadata.version(__name__) from nautobot.extras.plugins import NautobotAppConfig diff --git a/nautobot_design_builder/contrib/ext.py b/nautobot_design_builder/contrib/ext.py index 72898db9..d025d171 100644 --- a/nautobot_design_builder/contrib/ext.py +++ b/nautobot_design_builder/contrib/ext.py @@ -45,6 +45,7 @@ def lookup_by_content_type(self, app_label, model_name, query): model_class = content_type.model_class() queryset = model_class.objects except ContentType.DoesNotExist: + # pylint: disable=raise-missing-from raise DesignImplementationError(f"Could not find model class for {model_class}") return self.lookup(queryset, query) @@ -117,8 +118,10 @@ def lookup(self, queryset, query, parent=None): try: return queryset.get(**query) except ObjectDoesNotExist: + # pylint: disable=raise-missing-from raise DoesNotExistError(queryset.model, query_filter=query, parent=parent) except MultipleObjectsReturned: + # pylint: disable=raise-missing-from raise MultipleObjectsReturnedError(queryset.model, query=query, parent=parent) @@ -274,6 +277,7 @@ def attribute(self, value, model_instance) -> None: remote_instance = self.lookup(query_managers.pop(0), termination_query) except (DoesNotExistError, FieldError): if not query_managers: + # pylint:disable=raise-missing-from raise DoesNotExistError(model_instance.model_class, query_filter=termination_query) cable_attributes.update( @@ -467,6 +471,7 @@ def __init__(self, builder: Builder): self.PeerEndpoint = PeerEndpoint # pylint:disable=invalid-name self.Peering = Peering # pylint:disable=invalid-name except ModuleNotFoundError: + # pylint:disable=raise-missing-from raise DesignImplementationError( "the `bgp_peering` tag can only be used when the bgp models app is installed." ) diff --git a/nautobot_design_builder/contrib/tests/test_ext.py b/nautobot_design_builder/contrib/tests/test_ext.py index 4fab2c46..f156d2b7 100644 --- a/nautobot_design_builder/contrib/tests/test_ext.py +++ b/nautobot_design_builder/contrib/tests/test_ext.py @@ -21,6 +21,8 @@ class TestLookupExtension(TestCase): + """Test Lookup Extension.""" + def test_lookup_by_dict(self): design_template = """ manufacturers: @@ -54,6 +56,8 @@ def test_lookup_by_single_attribute(self): class TestCableConnectionExtension(TestCase): + """Test Cable Connection Extension.""" + def test_connect_cable(self): design_template_v1 = """ sites: @@ -191,6 +195,8 @@ def setUp(self) -> None: class TestNextPrefixExtension(PrefixExtensionTests): + """Test Next Prefix Extension.""" + def test_next_prefix_lookup(self): extension = NextPrefixExtension(None) want = "10.0.4.0/24" @@ -263,6 +269,8 @@ def test_lookup_by_role_and_tenant(self): class TestChildPrefixExtension(PrefixExtensionTests): + """Test Child Prefix Extension.""" + def test_creation(self): design_template = """ prefixes: @@ -290,6 +298,8 @@ def test_creation(self): class TestBGPExtension(TestCase): + """Test BGP extension.""" + def setUp(self): # TODO: Remove this when BGP models is migrated to 2.0 if nautobot_version >= "2.0.0": diff --git a/nautobot_design_builder/design.py b/nautobot_design_builder/design.py index 1ae187cc..fae663dd 100644 --- a/nautobot_design_builder/design.py +++ b/nautobot_design_builder/design.py @@ -277,6 +277,7 @@ def _load_instance(self): return except ObjectDoesNotExist: if self.action == "update": + # pylint: disable=raise-missing-from raise errors.DesignImplementationError(f"No match with {query_filter}", self.model_class) self.created = True # since the object was not found, we need to diff --git a/nautobot_design_builder/ext.py b/nautobot_design_builder/ext.py index b03d52b7..147cb3a3 100644 --- a/nautobot_design_builder/ext.py +++ b/nautobot_design_builder/ext.py @@ -9,7 +9,7 @@ from types import ModuleType import yaml -from nautobot_design_builder import DesignBuilderConfig +from nautobot_design_builder import NautobotDesignBuilderConfig from nautobot_design_builder.errors import DesignImplementationError from nautobot_design_builder.git import GitRepo @@ -190,6 +190,7 @@ def value(self, key) -> "ModelInstance": try: model_instance = self._env[key] except KeyError: + # pylint: disable=raise-missing-from raise DesignImplementationError(f"No ref named {key} has been saved in the design.") if model_instance.instance and not model_instance.instance._state.adding: # pylint: disable=protected-access model_instance.instance.refresh_from_db() @@ -225,7 +226,7 @@ class GitContextExtension(AttributeExtension): def __init__(self, builder: "Builder"): # noqa: D107 super().__init__(builder) - slug = DesignBuilderConfig.context_repository + slug = NautobotDesignBuilderConfig.context_repository self.context_repo = GitRepo(slug, builder.job_result) self._env = {} self._reset() diff --git a/nautobot_design_builder/fields.py b/nautobot_design_builder/fields.py index b81c8b39..74e733c2 100644 --- a/nautobot_design_builder/fields.py +++ b/nautobot_design_builder/fields.py @@ -153,11 +153,13 @@ def set_value(self, value): # noqa:D102 value.save() value = value.instance.pk except MultipleObjectsReturned: + # pylint: disable=raise-missing-from raise DesignImplementationError( f"Expected exactly 1 object for {self.model.__name__}({value}) but got more than one" ) except ObjectDoesNotExist: query = ",".join([f'{k}="{v}"' for k, v in value.items()]) + # pylint: disable=raise-missing-from raise DesignImplementationError(f"Could not find {self.model.__name__}: {query}") elif hasattr(value, "instance"): value = value.instance.pk diff --git a/nautobot_design_builder/jinja2.py b/nautobot_design_builder/jinja2.py index ad897b6a..73722f18 100644 --- a/nautobot_design_builder/jinja2.py +++ b/nautobot_design_builder/jinja2.py @@ -77,11 +77,13 @@ def network_offset(prefix: str, offset: str) -> IPNetwork: try: prefix = IPNetwork(prefix) except AddrFormatError: + # pylint: disable=raise-missing-from raise AddrFormatError(f"Invalid prefix {prefix}") try: offset = IPNetwork(offset) except AddrFormatError: + # pylint: disable=raise-missing-from raise AddrFormatError(f"Invalid offset {offset}") # netaddr overloads the + operator to sum @@ -100,6 +102,7 @@ def _json_default(value): try: return value.data except AttributeError: + # pylint: disable=raise-missing-from raise TypeError(f"Object of type {value.__class__.__name__} is not JSON serializable") diff --git a/nautobot_design_builder/management/commands/build_design.py b/nautobot_design_builder/management/commands/build_design.py index 24937ca9..8f0c6b58 100644 --- a/nautobot_design_builder/management/commands/build_design.py +++ b/nautobot_design_builder/management/commands/build_design.py @@ -14,6 +14,7 @@ def _load_file(filename): with open(filename) as file: # pylint: disable=unspecified-encoding return yaml.safe_load(file) except FileNotFoundError as ex: + # pylint: disable=raise-missing-from raise CommandError(str(ex)) diff --git a/nautobot_design_builder/tests/test_context.py b/nautobot_design_builder/tests/test_context.py index dcc3d3b7..565b98b7 100644 --- a/nautobot_design_builder/tests/test_context.py +++ b/nautobot_design_builder/tests/test_context.py @@ -7,6 +7,8 @@ class TestContext(unittest.TestCase): + """Test context.""" + def test_load(self): data = {"var1": "val1", "var2": "val2"} context = Context.load(data) @@ -55,6 +57,8 @@ def test_nested_list(self): class TestUpdateDictNode(unittest.TestCase): + """Test dict node.""" + def test_simple_update(self): data1 = {"var1": "val1"} data2 = {"var1": "val2"} @@ -112,6 +116,8 @@ def test_nested_update(self): class TestRootNode(unittest.TestCase): + """Test root node.""" + def test_simple_struct(self): data = {"var1": "val1"} want = {"var1": "val1"} @@ -222,6 +228,8 @@ def test_something_other_than_a_string(self): class TestContextDecorator(unittest.TestCase): + """Test context decorator.""" + def test_context_file(self): base_files = [ (BaseContext, "base_context_file"), diff --git a/nautobot_design_builder/tests/test_data_sources.py b/nautobot_design_builder/tests/test_data_sources.py index ebf70ccf..9c335ed2 100644 --- a/nautobot_design_builder/tests/test_data_sources.py +++ b/nautobot_design_builder/tests/test_data_sources.py @@ -60,6 +60,8 @@ def _create_module(path, content=""): class TestBase(TestCase): + """Base class for tests.""" + def setUp(self) -> None: super().setUp() self.repo_class = namedtuple("GitRepository", "provided_contents current_head filesystem_path slug") @@ -95,6 +97,8 @@ def get_repo( class TestModuleLoading(TestBase): + """Test that designs are loaded correctly.""" + def test_load_design_package(self): package_name = "design_builder_designs.module_loading" repo = self.get_repo(DATASOURCE_IDENTIFIER, "module-loading") @@ -149,6 +153,8 @@ def test_module_not_found(self): class TestDesignDiscovery(TestBase): + """Test that designs are discovered correctly.""" + def test_single_design_in_one_file(self): repo = self.get_repo(DATASOURCE_IDENTIFIER, "single-design-one-file") _create_file(os.path.join(repo.filesystem_path, "designs", "single_design_one_file.py"), DESIGN_FILE_1) diff --git a/nautobot_design_builder/tests/test_design_job.py b/nautobot_design_builder/tests/test_design_job.py index 97c5b594..bca2fdd6 100644 --- a/nautobot_design_builder/tests/test_design_job.py +++ b/nautobot_design_builder/tests/test_design_job.py @@ -11,6 +11,8 @@ class TestDesignJob(DesignTestCase): + """Test running design jobs.""" + @patch("nautobot_design_builder.design_job.Builder") def test_simple_design_commit(self, object_creator: Mock): job = self.get_mocked_job(test_designs.SimpleDesign) @@ -59,6 +61,8 @@ def test_custom_extensions(self, builder_patch: Mock): class TestDesignJobLogging(DesignTestCase): + """Test that the design job logs errors correctly.""" + @patch("nautobot_design_builder.design_job.Builder") def test_simple_design_implementation_error(self, object_creator: Mock): object_creator.return_value.implement_design.side_effect = DesignImplementationError("Broken") diff --git a/nautobot_design_builder/tests/test_errors.py b/nautobot_design_builder/tests/test_errors.py index 8eb4a318..cf78994b 100644 --- a/nautobot_design_builder/tests/test_errors.py +++ b/nautobot_design_builder/tests/test_errors.py @@ -7,7 +7,11 @@ class TestDesignModelError(unittest.TestCase): + """Test DesignModelError.""" + class TestModel: # pylint:disable=too-few-public-methods + """A test model.""" + def __init__(self, title="", parent=None): self.title = title self.instance = self @@ -64,6 +68,8 @@ def test_explicit_parent(self): class TestDesignValidationError(unittest.TestCase): + """Test DesignValidationError.""" + def test_single_string(self): want = "Error Message failed validation" got = str(DesignValidationError("Error Message")) diff --git a/nautobot_design_builder/tests/test_ext.py b/nautobot_design_builder/tests/test_ext.py index 31a1f47a..f8b5dd3c 100644 --- a/nautobot_design_builder/tests/test_ext.py +++ b/nautobot_design_builder/tests/test_ext.py @@ -22,6 +22,8 @@ class NotExtension: # pylint: disable=too-few-public-methods class TestExtensionDiscovery(TestCase): + """Test that extensions are discovered correctly.""" + def test_is_extension(self): self.assertTrue(ext.is_extension(Extension)) self.assertFalse(ext.is_extension(NotExtension)) @@ -39,6 +41,8 @@ def test_extensions(self): class TestCustomExtensions(TestCase): + """Test that custom extensions are loaded correctly.""" + def test_builder_called_with_custom_extensions(self): builder = Builder(extensions=[Extension]) self.assertEqual( @@ -51,6 +55,8 @@ def test_builder_called_with_invalid_extensions(self): class TestExtensionCommitRollback(TestCase): + """Test that extensions are called correctly.""" + @staticmethod def run_test(design, commit): """Implement a design and return wether or not `commit` and `roll_back` were called.""" diff --git a/nautobot_design_builder/util.py b/nautobot_design_builder/util.py index 42f367ce..9bdd8e1c 100644 --- a/nautobot_design_builder/util.py +++ b/nautobot_design_builder/util.py @@ -91,6 +91,7 @@ def load_design_package(path: str, package_name: str) -> Type[ModuleType]: package_spec.loader.exec_module(package) return package except FileNotFoundError: + # pylint: disable=raise-missing-from raise ModuleNotFoundError(f"no module named '{package_name}' at {path}") From 43cd74654aa38e67f4f0971b2302947df4147997 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Tue, 9 Jan 2024 11:08:07 -0500 Subject: [PATCH 6/8] Update docs/admin/compatibility_matrix.md --- docs/admin/compatibility_matrix.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/admin/compatibility_matrix.md b/docs/admin/compatibility_matrix.md index 4a032595..2a57658e 100644 --- a/docs/admin/compatibility_matrix.md +++ b/docs/admin/compatibility_matrix.md @@ -5,4 +5,4 @@ | Nautobot Design Builder Version | Nautobot First Support Version | Nautobot Last Support Version | | ------------- | -------------------- | ------------- | -| 1.0.X | 1.6.8 | 2.0.X | +| 1.0.X | 1.6.8 | 2.9999 | From 64d7580830cd6aab42e3095fab87edb534ed1a9d Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Tue, 9 Jan 2024 11:08:12 -0500 Subject: [PATCH 7/8] Update docs/admin/compatibility_matrix.md --- docs/admin/compatibility_matrix.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/admin/compatibility_matrix.md b/docs/admin/compatibility_matrix.md index 2a57658e..bb8c8c81 100644 --- a/docs/admin/compatibility_matrix.md +++ b/docs/admin/compatibility_matrix.md @@ -1,7 +1,5 @@ # Compatibility Matrix -!!! warning "Developer Note - Remove Me!" - Explain how the release models of the plugin and of Nautobot work together, how releases are supported, how features and older releases are deprecated etc. | Nautobot Design Builder Version | Nautobot First Support Version | Nautobot Last Support Version | | ------------- | -------------------- | ------------- | From 39d4fe9ca3fa420cb6f907be4634f6608f2373b9 Mon Sep 17 00:00:00 2001 From: itdependsnetworks Date: Sat, 20 Jan 2024 20:30:37 -0500 Subject: [PATCH 8/8] Update drift manager for min version and uninstall update --- .github/workflows/ci.yml | 7 +- development/Dockerfile | 4 +- docs/admin/compatibility_matrix.md | 2 +- docs/admin/install.md | 2 +- docs/admin/uninstall.md | 8 - docs/dev/dev_environment.md | 12 +- invoke.example.yml | 2 +- invoke.mysql.yml | 2 +- mkdocs.yml | 2 +- nautobot_design_builder/__init__.py | 2 +- nautobot_design_builder/tests/test_basic.py | 34 -- poetry.lock | 557 ++++++++++++-------- pyproject.toml | 2 +- tasks.py | 2 +- 14 files changed, 355 insertions(+), 283 deletions(-) delete mode 100644 nautobot_design_builder/tests/test_basic.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8bb24e72..bb02bcb1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -95,7 +95,7 @@ jobs: fail-fast: true matrix: python-version: ["3.11"] - nautobot-version: ["1.6.8"] + nautobot-version: ["1.6.0"] env: INVOKE_NAUTOBOT_DESIGN_BUILDER_PYTHON_VER: "${{ matrix.python-version }}" INVOKE_NAUTOBOT_DESIGN_BUILDER_NAUTOBOT_VER: "${{ matrix.nautobot-version }}" @@ -138,7 +138,7 @@ jobs: fail-fast: true matrix: python-version: ["3.11"] - nautobot-version: ["1.6.8"] + nautobot-version: ["1.6.0"] env: INVOKE_NAUTOBOT_DESIGN_BUILDER_PYTHON_VER: "${{ matrix.python-version }}" INVOKE_NAUTOBOT_DESIGN_BUILDER_NAUTOBOT_VER: "${{ matrix.nautobot-version }}" @@ -175,14 +175,13 @@ jobs: strategy: fail-fast: true matrix: - python-version: ["3.8", "3.11"] python-version: ["3.8", "3.11"] db-backend: ["postgresql"] nautobot-version: ["1.6", "stable"] include: - python-version: "3.11" db-backend: "postgresql" - nautobot-version: "1.6.8" + nautobot-version: "1.6.0" - python-version: "3.11" db-backend: "mysql" nautobot-version: "stable" diff --git a/development/Dockerfile b/development/Dockerfile index ee399e3c..b7b58482 100644 --- a/development/Dockerfile +++ b/development/Dockerfile @@ -6,8 +6,8 @@ # ------------------------------------------------------------------------------------- # !!! USE CAUTION WHEN MODIFYING LINES BELOW -# Accepts a desired Nautobot version as build argument, default to 1.6.8 -ARG NAUTOBOT_VER="1.6.8" +# Accepts a desired Nautobot version as build argument, default to 1.6.0 +ARG NAUTOBOT_VER="1.6.0" # Accepts a desired Python version as build argument, default to 3.11 ARG PYTHON_VER="3.11" diff --git a/docs/admin/compatibility_matrix.md b/docs/admin/compatibility_matrix.md index bb8c8c81..1d00ccf9 100644 --- a/docs/admin/compatibility_matrix.md +++ b/docs/admin/compatibility_matrix.md @@ -3,4 +3,4 @@ | Nautobot Design Builder Version | Nautobot First Support Version | Nautobot Last Support Version | | ------------- | -------------------- | ------------- | -| 1.0.X | 1.6.8 | 2.9999 | +| 1.0.X | 1.6.0 | 2.9999 | diff --git a/docs/admin/install.md b/docs/admin/install.md index f9b94932..5d99d843 100644 --- a/docs/admin/install.md +++ b/docs/admin/install.md @@ -4,7 +4,7 @@ Here you will find detailed instructions on how to **install** and **configure** ## Prerequisites -- The plugin is compatible with Nautobot 1.6.8 and higher. +- The plugin is compatible with Nautobot 1.6.0 and higher. - Databases supported: PostgreSQL, MySQL !!! note diff --git a/docs/admin/uninstall.md b/docs/admin/uninstall.md index 63cda675..db13946e 100644 --- a/docs/admin/uninstall.md +++ b/docs/admin/uninstall.md @@ -2,14 +2,6 @@ Here you will find any steps necessary to cleanly remove the App from your Nautobot environment. -## Database Cleanup - -Prior to removing the plugin from the `nautobot_config.py`, run the following command to roll back any migration specific to this plugin. - -```shell -nautobot-server migrate nautobot_app_design_builder zero -``` - ## Remove App configuration Remove the configuration you added in `nautobot_config.py` from `PLUGINS` & `PLUGINS_CONFIG`. diff --git a/docs/dev/dev_environment.md b/docs/dev/dev_environment.md index 30393c27..7be6e05c 100644 --- a/docs/dev/dev_environment.md +++ b/docs/dev/dev_environment.md @@ -13,7 +13,7 @@ This is a quick reference guide if you're already familiar with the development The [Invoke](http://www.pyinvoke.org/) library is used to provide some helper commands based on the environment. There are a few configuration parameters which can be passed to Invoke to override the default configuration: -- `nautobot_ver`: the version of Nautobot to use as a base for any built docker containers (default: 1.6.8) +- `nautobot_ver`: the version of Nautobot to use as a base for any built docker containers (default: 1.6.0) - `project_name`: the default docker compose project name (default: `nautobot-design-builder`) - `python_ver`: the version of Python to use as a base for any built docker containers (default: 3.11) - `local`: a boolean flag indicating if invoke tasks should be run on the host or inside the docker containers (default: False, commands will be run in docker containers) @@ -179,7 +179,7 @@ The first thing you need to do is build the necessary Docker image for Nautobot #14 exporting layers #14 exporting layers 1.2s done #14 writing image sha256:2d524bc1665327faa0d34001b0a9d2ccf450612bf8feeb969312e96a2d3e3503 done -#14 naming to docker.io/nautobot-design-builder/nautobot:1.6.8-py3.11 done +#14 naming to docker.io/nautobot-design-builder/nautobot:1.6.0-py3.11 done ``` ### Invoke - Starting the Development Environment @@ -210,9 +210,9 @@ This will start all of the Docker containers used for hosting Nautobot. You shou ```bash ➜ docker ps ****CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES -ee90fbfabd77 nautobot-design-builder/nautobot:1.6.8-py3.11 "nautobot-server rqw…" 16 seconds ago Up 13 seconds nautobot_design_builder_worker_1 -b8adb781d013 nautobot-design-builder/nautobot:1.6.8-py3.11 "/docker-entrypoint.…" 20 seconds ago Up 15 seconds 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp nautobot_design_builder_nautobot_1 -d64ebd60675d nautobot-design-builder/nautobot:1.6.8-py3.11 "mkdocs serve -v -a …" 25 seconds ago Up 18 seconds 0.0.0.0:8001->8080/tcp, :::8001->8080/tcp nautobot_design_builder_docs_1 +ee90fbfabd77 nautobot-design-builder/nautobot:1.6.0-py3.11 "nautobot-server rqw…" 16 seconds ago Up 13 seconds nautobot_design_builder_worker_1 +b8adb781d013 nautobot-design-builder/nautobot:1.6.0-py3.11 "/docker-entrypoint.…" 20 seconds ago Up 15 seconds 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp nautobot_design_builder_nautobot_1 +d64ebd60675d nautobot-design-builder/nautobot:1.6.0-py3.11 "mkdocs serve -v -a …" 25 seconds ago Up 18 seconds 0.0.0.0:8001->8080/tcp, :::8001->8080/tcp nautobot_design_builder_docs_1 e72d63129b36 postgres:13-alpine "docker-entrypoint.s…" 25 seconds ago Up 19 seconds 0.0.0.0:5432->5432/tcp, :::5432->5432/tcp nautobot_design_builder_postgres_1 96c6ff66997c redis:6-alpine "docker-entrypoint.s…" 25 seconds ago Up 21 seconds 0.0.0.0:6379->6379/tcp, :::6379->6379/tcp nautobot_design_builder_redis_1 ``` @@ -410,7 +410,7 @@ namespace.configure( { "nautobot_design_builder": { ... - "nautobot_ver": "1.6.8", + "nautobot_ver": "1.6.0", ... } } diff --git a/invoke.example.yml b/invoke.example.yml index 9a4fa928..9132ee94 100644 --- a/invoke.example.yml +++ b/invoke.example.yml @@ -1,7 +1,7 @@ --- nautobot_design_builder: project_name: "nautobot-design-builder" - nautobot_ver: "1.6.8" + nautobot_ver: "1.6.0" local: false python_ver: "3.11" compose_dir: "development" diff --git a/invoke.mysql.yml b/invoke.mysql.yml index 62ffa9f2..dd1881cc 100644 --- a/invoke.mysql.yml +++ b/invoke.mysql.yml @@ -1,7 +1,7 @@ --- nautobot_design_builder: project_name: "nautobot-design-builder" - nautobot_ver: "1.6.8" + nautobot_ver: "1.6.0" local: false python_ver: "3.11" compose_dir: "development" diff --git a/mkdocs.yml b/mkdocs.yml index 67ff2df1..14bf4cd5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -123,9 +123,9 @@ nav: - Code Reference: - "dev/code_reference/index.md" - Package: "dev/code_reference/package.md" - - Design Job: "dev/code_reference/design_job.md" - Context: "dev/code_reference/context.md" - Design Builder: "dev/code_reference/design.md" + - Design Job: "dev/code_reference/design_job.md" - Jinja Rendering: "dev/code_reference/jinja2.md" - Template Extensions: "dev/code_reference/ext.md" - Util: "dev/code_reference/util.md" diff --git a/nautobot_design_builder/__init__.py b/nautobot_design_builder/__init__.py index 0a908ca3..a45a8c71 100644 --- a/nautobot_design_builder/__init__.py +++ b/nautobot_design_builder/__init__.py @@ -20,7 +20,7 @@ class NautobotDesignBuilderConfig(NautobotAppConfig): description = "Nautobot app that uses design templates to easily create data objects in Nautobot with minimal input from a user.." base_url = "design-builder" required_settings = [] - min_version = "1.6.8" + min_version = "1.6.0" max_version = "2.9999" default_settings = {} caching_config = {} diff --git a/nautobot_design_builder/tests/test_basic.py b/nautobot_design_builder/tests/test_basic.py deleted file mode 100644 index 9b52639c..00000000 --- a/nautobot_design_builder/tests/test_basic.py +++ /dev/null @@ -1,34 +0,0 @@ -"""Basic tests that do not require Django.""" -import unittest -import os -import toml - -from nautobot_design_builder import __version__ as project_version - - -class TestVersion(unittest.TestCase): - """Test Version is the same.""" - - def test_version(self): - """Verify that pyproject.toml version is same as version specified in the package.""" - parent_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) - poetry_version = toml.load(os.path.join(parent_path, "pyproject.toml"))["tool"]["poetry"]["version"] - self.assertEqual(project_version, poetry_version) - - -class TestDocsPackaging(unittest.TestCase): - """Test Version in doc requirements is the same pyproject.""" - - def test_version(self): - """Verify that pyproject.toml dev dependencies have the same versions as in the docs requirements.txt.""" - parent_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) - poetry_path = os.path.join(parent_path, "pyproject.toml") - poetry_details = toml.load(poetry_path)["tool"]["poetry"]["group"]["dev"]["dependencies"] - with open(f"{parent_path}/docs/requirements.txt", "r", encoding="utf-8") as file: - requirements = [line for line in file.read().splitlines() if (len(line) > 0 and not line.startswith("#"))] - for pkg in requirements: - if len(pkg.split("==")) == 2: - pkg, version = pkg.split("==") - else: - version = "*" - self.assertEqual(poetry_details[pkg], version) diff --git a/poetry.lock b/poetry.lock index 2b2bfb9c..58a9b455 100755 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand. +# This file is automatically @generated by Poetry and should not be changed by hand. [[package]] name = "amqp" @@ -59,18 +59,23 @@ tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] [[package]] name = "astroid" -version = "3.0.2" +version = "2.15.8" description = "An abstract syntax tree for Python with inference support." category = "dev" optional = false -python-versions = ">=3.8.0" +python-versions = ">=3.7.2" files = [ - {file = "astroid-3.0.2-py3-none-any.whl", hash = "sha256:d6e62862355f60e716164082d6b4b041d38e2a8cf1c7cd953ded5108bac8ff5c"}, - {file = "astroid-3.0.2.tar.gz", hash = "sha256:4a61cf0a59097c7bb52689b0fd63717cd2a8a14dc9f1eee97b82d814881c8c91"}, + {file = "astroid-2.15.8-py3-none-any.whl", hash = "sha256:1aa149fc5c6589e3d0ece885b4491acd80af4f087baafa3fb5203b113e68cd3c"}, + {file = "astroid-2.15.8.tar.gz", hash = "sha256:6c107453dffee9055899705de3c9ead36e74119cee151e5a9aaf7f0b0e020a6a"}, ] [package.dependencies] +lazy-object-proxy = ">=1.4.0" typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} +wrapt = [ + {version = ">=1.11,<2", markers = "python_version < \"3.11\""}, + {version = ">=1.14,<2", markers = "python_version >= \"3.11\""}, +] [[package]] name = "asttokens" @@ -211,20 +216,23 @@ yaml = ["PyYAML"] [[package]] name = "beautifulsoup4" -version = "4.12.2" +version = "4.12.3" description = "Screen-scraping library" category = "dev" optional = false python-versions = ">=3.6.0" files = [ - {file = "beautifulsoup4-4.12.2-py3-none-any.whl", hash = "sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a"}, - {file = "beautifulsoup4-4.12.2.tar.gz", hash = "sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da"}, + {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, + {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, ] [package.dependencies] soupsieve = ">1.2" [package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] html5lib = ["html5lib"] lxml = ["lxml"] @@ -751,14 +759,14 @@ files = [ [[package]] name = "defusedxml" -version = "0.7.1" +version = "0.8.0rc2" description = "XML bomb protection for Python stdlib modules" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.6" files = [ - {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, - {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, + {file = "defusedxml-0.8.0rc2-py2.py3-none-any.whl", hash = "sha256:1c812964311154c3bf4aaf3bc1443b31ee13530b7f255eaaa062c0553c76103d"}, + {file = "defusedxml-0.8.0rc2.tar.gz", hash = "sha256:138c7d540a78775182206c7c97fe65b246a2f40b29471e1a2f1b0da76e7a3942"}, ] [[package]] @@ -1285,21 +1293,6 @@ uritemplate = ">=3.0.0" coreapi = ["coreapi (>=2.3.3)", "coreschema (>=0.0.4)"] validation = ["swagger-spec-validator (>=2.1.0)"] -[[package]] -name = "exceptiongroup" -version = "1.2.0" -description = "Backport of PEP 654 (exception groups)" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, - {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, -] - -[package.extras] -test = ["pytest (>=6)"] - [[package]] name = "executing" version = "2.0.1" @@ -1317,20 +1310,20 @@ tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipyth [[package]] name = "flake8" -version = "3.9.2" +version = "5.0.4" description = "the modular source code checker: pep8 pyflakes and co" category = "dev" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +python-versions = ">=3.6.1" files = [ - {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, - {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, + {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, + {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, ] [package.dependencies] -mccabe = ">=0.6.0,<0.7.0" -pycodestyle = ">=2.7.0,<2.8.0" -pyflakes = ">=2.3.0,<2.4.0" +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.9.0,<2.10.0" +pyflakes = ">=2.5.0,<2.6.0" [[package]] name = "funcy" @@ -1379,21 +1372,21 @@ smmap = ">=3.0.1,<6" [[package]] name = "gitpython" -version = "3.1.40" +version = "3.1.41" description = "GitPython is a Python library used to interact with Git repositories" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "GitPython-3.1.40-py3-none-any.whl", hash = "sha256:cf14627d5a8049ffbf49915732e5eddbe8134c3bdb9d476e6182b676fc573f8a"}, - {file = "GitPython-3.1.40.tar.gz", hash = "sha256:22b126e9ffb671fdd0c129796343a02bf67bf2994b35449ffc9321aa755e18a4"}, + {file = "GitPython-3.1.41-py3-none-any.whl", hash = "sha256:c36b6634d069b3f719610175020a9aed919421c87552185b085e04fbbdb10b7c"}, + {file = "GitPython-3.1.41.tar.gz", hash = "sha256:ed66e624884f76df22c8e16066d567aaa5a37d5b5fa19db2c6df6f7156db9048"}, ] [package.dependencies] gitdb = ">=4.0.1,<5" [package.extras] -test = ["black", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest", "pytest-cov", "pytest-instafail", "pytest-subtests", "pytest-sugar"] +test = ["black", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "sumtypes"] [[package]] name = "graphene" @@ -1494,14 +1487,14 @@ six = ">=1.12" [[package]] name = "griffe" -version = "0.38.1" +version = "0.39.1" description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "griffe-0.38.1-py3-none-any.whl", hash = "sha256:334c79d3b5964ade65c05dfcaf53518c576dedd387aaba5c9fd71212f34f1483"}, - {file = "griffe-0.38.1.tar.gz", hash = "sha256:bd68d7da7f3d87bc57eb9962b250db123efd9bbcc06c11c1a91b6e583b2a9361"}, + {file = "griffe-0.39.1-py3-none-any.whl", hash = "sha256:6ce4ecffcf0d2f96362c5974b3f7df812da8f8d4cfcc5ebc8202ef72656fc087"}, + {file = "griffe-0.39.1.tar.gz", hash = "sha256:ead8dfede6e6531cce6bf69090a4f3c6d36fdf923c43f8e85aa530552cef0c09"}, ] [package.dependencies] @@ -1541,22 +1534,22 @@ testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs [[package]] name = "importlib-resources" -version = "6.1.1" +version = "5.13.0" description = "Read resources from Python packages" category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_resources-6.1.1-py3-none-any.whl", hash = "sha256:e8bf90d8213b486f428c9c39714b920041cb02c184686a3dee24905aaa8105d6"}, - {file = "importlib_resources-6.1.1.tar.gz", hash = "sha256:3893a00122eafde6894c59914446a512f728a0c1a45f9bb9b63721b6bacf0b4a"}, + {file = "importlib_resources-5.13.0-py3-none-any.whl", hash = "sha256:9f7bd0c97b79972a6cce36a366356d16d5e13b09679c11a58f1014bfdf8e64b2"}, + {file = "importlib_resources-5.13.0.tar.gz", hash = "sha256:82d5c6cca930697dbbd86c93333bb2c2e72861d4789a11c2662b933e5ad2b528"}, ] [package.dependencies] zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff", "zipp (>=3.17)"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff"] [[package]] name = "inflection" @@ -1570,18 +1563,6 @@ files = [ {file = "inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417"}, ] -[[package]] -name = "iniconfig" -version = "2.0.0" -description = "brain-dead simple config-ini parsing" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, -] - [[package]] name = "invoke" version = "2.2.0" @@ -1671,14 +1652,14 @@ testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] [[package]] name = "jinja2" -version = "3.1.2" +version = "3.1.3" description = "A very fast and expressive template engine." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, - {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, + {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, + {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, ] [package.dependencies] @@ -1711,14 +1692,14 @@ format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339- [[package]] name = "kombu" -version = "5.3.4" +version = "5.3.5" description = "Messaging library for Python." category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "kombu-5.3.4-py3-none-any.whl", hash = "sha256:63bb093fc9bb80cfb3a0972336a5cec1fa7ac5f9ef7e8237c6bf8dda9469313e"}, - {file = "kombu-5.3.4.tar.gz", hash = "sha256:0bb2e278644d11dea6272c17974a3dbb9688a949f3bb60aeb5b791329c44fadc"}, + {file = "kombu-5.3.5-py3-none-any.whl", hash = "sha256:0eac1bbb464afe6fb0924b21bf79460416d25d8abc52546d4f16cad94f789488"}, + {file = "kombu-5.3.5.tar.gz", hash = "sha256:30e470f1a6b49c70dc6f6d13c3e4cc4e178aa6c469ceb6bcd55645385fc84b93"}, ] [package.dependencies] @@ -1744,6 +1725,53 @@ sqs = ["boto3 (>=1.26.143)", "pycurl (>=7.43.0.5)", "urllib3 (>=1.26.16)"] yaml = ["PyYAML (>=3.10)"] zookeeper = ["kazoo (>=2.8.0)"] +[[package]] +name = "lazy-object-proxy" +version = "1.10.0" +description = "A fast and thorough lazy object proxy." +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "lazy-object-proxy-1.10.0.tar.gz", hash = "sha256:78247b6d45f43a52ef35c25b5581459e85117225408a4128a3daf8bf9648ac69"}, + {file = "lazy_object_proxy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:855e068b0358ab916454464a884779c7ffa312b8925c6f7401e952dcf3b89977"}, + {file = "lazy_object_proxy-1.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab7004cf2e59f7c2e4345604a3e6ea0d92ac44e1c2375527d56492014e690c3"}, + {file = "lazy_object_proxy-1.10.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc0d2fc424e54c70c4bc06787e4072c4f3b1aa2f897dfdc34ce1013cf3ceef05"}, + {file = "lazy_object_proxy-1.10.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e2adb09778797da09d2b5ebdbceebf7dd32e2c96f79da9052b2e87b6ea495895"}, + {file = "lazy_object_proxy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b1f711e2c6dcd4edd372cf5dec5c5a30d23bba06ee012093267b3376c079ec83"}, + {file = "lazy_object_proxy-1.10.0-cp310-cp310-win32.whl", hash = "sha256:76a095cfe6045c7d0ca77db9934e8f7b71b14645f0094ffcd842349ada5c5fb9"}, + {file = "lazy_object_proxy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:b4f87d4ed9064b2628da63830986c3d2dca7501e6018347798313fcf028e2fd4"}, + {file = "lazy_object_proxy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fec03caabbc6b59ea4a638bee5fce7117be8e99a4103d9d5ad77f15d6f81020c"}, + {file = "lazy_object_proxy-1.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02c83f957782cbbe8136bee26416686a6ae998c7b6191711a04da776dc9e47d4"}, + {file = "lazy_object_proxy-1.10.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:009e6bb1f1935a62889ddc8541514b6a9e1fcf302667dcb049a0be5c8f613e56"}, + {file = "lazy_object_proxy-1.10.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75fc59fc450050b1b3c203c35020bc41bd2695ed692a392924c6ce180c6f1dc9"}, + {file = "lazy_object_proxy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:782e2c9b2aab1708ffb07d4bf377d12901d7a1d99e5e410d648d892f8967ab1f"}, + {file = "lazy_object_proxy-1.10.0-cp311-cp311-win32.whl", hash = "sha256:edb45bb8278574710e68a6b021599a10ce730d156e5b254941754a9cc0b17d03"}, + {file = "lazy_object_proxy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:e271058822765ad5e3bca7f05f2ace0de58a3f4e62045a8c90a0dfd2f8ad8cc6"}, + {file = "lazy_object_proxy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e98c8af98d5707dcdecc9ab0863c0ea6e88545d42ca7c3feffb6b4d1e370c7ba"}, + {file = "lazy_object_proxy-1.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:952c81d415b9b80ea261d2372d2a4a2332a3890c2b83e0535f263ddfe43f0d43"}, + {file = "lazy_object_proxy-1.10.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80b39d3a151309efc8cc48675918891b865bdf742a8616a337cb0090791a0de9"}, + {file = "lazy_object_proxy-1.10.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e221060b701e2aa2ea991542900dd13907a5c90fa80e199dbf5a03359019e7a3"}, + {file = "lazy_object_proxy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:92f09ff65ecff3108e56526f9e2481b8116c0b9e1425325e13245abfd79bdb1b"}, + {file = "lazy_object_proxy-1.10.0-cp312-cp312-win32.whl", hash = "sha256:3ad54b9ddbe20ae9f7c1b29e52f123120772b06dbb18ec6be9101369d63a4074"}, + {file = "lazy_object_proxy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:127a789c75151db6af398b8972178afe6bda7d6f68730c057fbbc2e96b08d282"}, + {file = "lazy_object_proxy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9e4ed0518a14dd26092614412936920ad081a424bdcb54cc13349a8e2c6d106a"}, + {file = "lazy_object_proxy-1.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ad9e6ed739285919aa9661a5bbed0aaf410aa60231373c5579c6b4801bd883c"}, + {file = "lazy_object_proxy-1.10.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fc0a92c02fa1ca1e84fc60fa258458e5bf89d90a1ddaeb8ed9cc3147f417255"}, + {file = "lazy_object_proxy-1.10.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0aefc7591920bbd360d57ea03c995cebc204b424524a5bd78406f6e1b8b2a5d8"}, + {file = "lazy_object_proxy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5faf03a7d8942bb4476e3b62fd0f4cf94eaf4618e304a19865abf89a35c0bbee"}, + {file = "lazy_object_proxy-1.10.0-cp38-cp38-win32.whl", hash = "sha256:e333e2324307a7b5d86adfa835bb500ee70bfcd1447384a822e96495796b0ca4"}, + {file = "lazy_object_proxy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:cb73507defd385b7705c599a94474b1d5222a508e502553ef94114a143ec6696"}, + {file = "lazy_object_proxy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:366c32fe5355ef5fc8a232c5436f4cc66e9d3e8967c01fb2e6302fd6627e3d94"}, + {file = "lazy_object_proxy-1.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2297f08f08a2bb0d32a4265e98a006643cd7233fb7983032bd61ac7a02956b3b"}, + {file = "lazy_object_proxy-1.10.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18dd842b49456aaa9a7cf535b04ca4571a302ff72ed8740d06b5adcd41fe0757"}, + {file = "lazy_object_proxy-1.10.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:217138197c170a2a74ca0e05bddcd5f1796c735c37d0eee33e43259b192aa424"}, + {file = "lazy_object_proxy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9a3a87cf1e133e5b1994144c12ca4aa3d9698517fe1e2ca82977781b16955658"}, + {file = "lazy_object_proxy-1.10.0-cp39-cp39-win32.whl", hash = "sha256:30b339b2a743c5288405aa79a69e706a06e02958eab31859f7f3c04980853b70"}, + {file = "lazy_object_proxy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:a899b10e17743683b293a729d3a11f2f399e8a90c73b089e29f5d0fe3509f0dd"}, + {file = "lazy_object_proxy-1.10.0-pp310.pp311.pp312.pp38.pp39-none-any.whl", hash = "sha256:80fa48bd89c8f2f456fc0765c11c23bf5af827febacd2f523ca5bc1893fcc09d"}, +] + [[package]] name = "lxml" version = "5.1.0" @@ -1752,6 +1780,7 @@ category = "dev" optional = false python-versions = ">=3.6" files = [ + {file = "lxml-5.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:704f5572ff473a5f897745abebc6df40f22d4133c1e0a1f124e4f2bd3330ff7e"}, {file = "lxml-5.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9d3c0f8567ffe7502d969c2c1b809892dc793b5d0665f602aad19895f8d508da"}, {file = "lxml-5.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5fcfbebdb0c5d8d18b84118842f31965d59ee3e66996ac842e21f957eb76138c"}, {file = "lxml-5.1.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f37c6d7106a9d6f0708d4e164b707037b7380fcd0b04c5bd9cae1fb46a856fb"}, @@ -1761,6 +1790,7 @@ files = [ {file = "lxml-5.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:82bddf0e72cb2af3cbba7cec1d2fd11fda0de6be8f4492223d4a268713ef2147"}, {file = "lxml-5.1.0-cp310-cp310-win32.whl", hash = "sha256:b66aa6357b265670bb574f050ffceefb98549c721cf28351b748be1ef9577d93"}, {file = "lxml-5.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:4946e7f59b7b6a9e27bef34422f645e9a368cb2be11bf1ef3cafc39a1f6ba68d"}, + {file = "lxml-5.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:14deca1460b4b0f6b01f1ddc9557704e8b365f55c63070463f6c18619ebf964f"}, {file = "lxml-5.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ed8c3d2cd329bf779b7ed38db176738f3f8be637bb395ce9629fc76f78afe3d4"}, {file = "lxml-5.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:436a943c2900bb98123b06437cdd30580a61340fbdb7b28aaf345a459c19046a"}, {file = "lxml-5.1.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:acb6b2f96f60f70e7f34efe0c3ea34ca63f19ca63ce90019c6cbca6b676e81fa"}, @@ -1770,6 +1800,7 @@ files = [ {file = "lxml-5.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f4c9bda132ad108b387c33fabfea47866af87f4ea6ffb79418004f0521e63204"}, {file = "lxml-5.1.0-cp311-cp311-win32.whl", hash = "sha256:bc64d1b1dab08f679fb89c368f4c05693f58a9faf744c4d390d7ed1d8223869b"}, {file = "lxml-5.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:a5ab722ae5a873d8dcee1f5f45ddd93c34210aed44ff2dc643b5025981908cda"}, + {file = "lxml-5.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9aa543980ab1fbf1720969af1d99095a548ea42e00361e727c58a40832439114"}, {file = "lxml-5.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6f11b77ec0979f7e4dc5ae081325a2946f1fe424148d3945f943ceaede98adb8"}, {file = "lxml-5.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a36c506e5f8aeb40680491d39ed94670487ce6614b9d27cabe45d94cd5d63e1e"}, {file = "lxml-5.1.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f643ffd2669ffd4b5a3e9b41c909b72b2a1d5e4915da90a77e119b8d48ce867a"}, @@ -1795,8 +1826,8 @@ files = [ {file = "lxml-5.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8f52fe6859b9db71ee609b0c0a70fea5f1e71c3462ecf144ca800d3f434f0764"}, {file = "lxml-5.1.0-cp37-cp37m-win32.whl", hash = "sha256:d42e3a3fc18acc88b838efded0e6ec3edf3e328a58c68fbd36a7263a874906c8"}, {file = "lxml-5.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:eac68f96539b32fce2c9b47eb7c25bb2582bdaf1bbb360d25f564ee9e04c542b"}, + {file = "lxml-5.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ae15347a88cf8af0949a9872b57a320d2605ae069bcdf047677318bc0bba45b1"}, {file = "lxml-5.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c26aab6ea9c54d3bed716b8851c8bfc40cb249b8e9880e250d1eddde9f709bf5"}, - {file = "lxml-5.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cfbac9f6149174f76df7e08c2e28b19d74aed90cad60383ad8671d3af7d0502f"}, {file = "lxml-5.1.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:342e95bddec3a698ac24378d61996b3ee5ba9acfeb253986002ac53c9a5f6f84"}, {file = "lxml-5.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:725e171e0b99a66ec8605ac77fa12239dbe061482ac854d25720e2294652eeaa"}, {file = "lxml-5.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d184e0d5c918cff04cdde9dbdf9600e960161d773666958c9d7b565ccc60c45"}, @@ -1804,6 +1835,7 @@ files = [ {file = "lxml-5.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d48fc57e7c1e3df57be5ae8614bab6d4e7b60f65c5457915c26892c41afc59e"}, {file = "lxml-5.1.0-cp38-cp38-win32.whl", hash = "sha256:7ec465e6549ed97e9f1e5ed51c657c9ede767bc1c11552f7f4d022c4df4a977a"}, {file = "lxml-5.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:b21b4031b53d25b0858d4e124f2f9131ffc1530431c6d1321805c90da78388d1"}, + {file = "lxml-5.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:52427a7eadc98f9e62cb1368a5079ae826f94f05755d2d567d93ee1bc3ceb354"}, {file = "lxml-5.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6a2a2c724d97c1eb8cf966b16ca2915566a4904b9aad2ed9a09c748ffe14f969"}, {file = "lxml-5.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:843b9c835580d52828d8f69ea4302537337a21e6b4f1ec711a52241ba4a824f3"}, {file = "lxml-5.1.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9b99f564659cfa704a2dd82d0684207b1aadf7d02d33e54845f9fc78e06b7581"}, @@ -1896,62 +1928,72 @@ wavedrom = ["wavedrom"] [[package]] name = "markupsafe" -version = "2.1.3" +version = "2.1.4" description = "Safely add untrusted strings to HTML/XML markup." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, - {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, + {file = "MarkupSafe-2.1.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de8153a7aae3835484ac168a9a9bdaa0c5eee4e0bc595503c95d53b942879c84"}, + {file = "MarkupSafe-2.1.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e888ff76ceb39601c59e219f281466c6d7e66bd375b4ec1ce83bcdc68306796b"}, + {file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0b838c37ba596fcbfca71651a104a611543077156cb0a26fe0c475e1f152ee8"}, + {file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac1ebf6983148b45b5fa48593950f90ed6d1d26300604f321c74a9ca1609f8e"}, + {file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fbad3d346df8f9d72622ac71b69565e621ada2ce6572f37c2eae8dacd60385d"}, + {file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5291d98cd3ad9a562883468c690a2a238c4a6388ab3bd155b0c75dd55ece858"}, + {file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a7cc49ef48a3c7a0005a949f3c04f8baa5409d3f663a1b36f0eba9bfe2a0396e"}, + {file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b83041cda633871572f0d3c41dddd5582ad7d22f65a72eacd8d3d6d00291df26"}, + {file = "MarkupSafe-2.1.4-cp310-cp310-win32.whl", hash = "sha256:0c26f67b3fe27302d3a412b85ef696792c4a2386293c53ba683a89562f9399b0"}, + {file = "MarkupSafe-2.1.4-cp310-cp310-win_amd64.whl", hash = "sha256:a76055d5cb1c23485d7ddae533229039b850db711c554a12ea64a0fd8a0129e2"}, + {file = "MarkupSafe-2.1.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9e9e3c4020aa2dc62d5dd6743a69e399ce3de58320522948af6140ac959ab863"}, + {file = "MarkupSafe-2.1.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0042d6a9880b38e1dd9ff83146cc3c9c18a059b9360ceae207805567aacccc69"}, + {file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d03fea4c4e9fd0ad75dc2e7e2b6757b80c152c032ea1d1de487461d8140efc"}, + {file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ab3a886a237f6e9c9f4f7d272067e712cdb4efa774bef494dccad08f39d8ae6"}, + {file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abf5ebbec056817057bfafc0445916bb688a255a5146f900445d081db08cbabb"}, + {file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e1a0d1924a5013d4f294087e00024ad25668234569289650929ab871231668e7"}, + {file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e7902211afd0af05fbadcc9a312e4cf10f27b779cf1323e78d52377ae4b72bea"}, + {file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c669391319973e49a7c6230c218a1e3044710bc1ce4c8e6eb71f7e6d43a2c131"}, + {file = "MarkupSafe-2.1.4-cp311-cp311-win32.whl", hash = "sha256:31f57d64c336b8ccb1966d156932f3daa4fee74176b0fdc48ef580be774aae74"}, + {file = "MarkupSafe-2.1.4-cp311-cp311-win_amd64.whl", hash = "sha256:54a7e1380dfece8847c71bf7e33da5d084e9b889c75eca19100ef98027bd9f56"}, + {file = "MarkupSafe-2.1.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:a76cd37d229fc385738bd1ce4cba2a121cf26b53864c1772694ad0ad348e509e"}, + {file = "MarkupSafe-2.1.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:987d13fe1d23e12a66ca2073b8d2e2a75cec2ecb8eab43ff5624ba0ad42764bc"}, + {file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5244324676254697fe5c181fc762284e2c5fceeb1c4e3e7f6aca2b6f107e60dc"}, + {file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78bc995e004681246e85e28e068111a4c3f35f34e6c62da1471e844ee1446250"}, + {file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4d176cfdfde84f732c4a53109b293d05883e952bbba68b857ae446fa3119b4f"}, + {file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f9917691f410a2e0897d1ef99619fd3f7dd503647c8ff2475bf90c3cf222ad74"}, + {file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f06e5a9e99b7df44640767842f414ed5d7bedaaa78cd817ce04bbd6fd86e2dd6"}, + {file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:396549cea79e8ca4ba65525470d534e8a41070e6b3500ce2414921099cb73e8d"}, + {file = "MarkupSafe-2.1.4-cp312-cp312-win32.whl", hash = "sha256:f6be2d708a9d0e9b0054856f07ac7070fbe1754be40ca8525d5adccdbda8f475"}, + {file = "MarkupSafe-2.1.4-cp312-cp312-win_amd64.whl", hash = "sha256:5045e892cfdaecc5b4c01822f353cf2c8feb88a6ec1c0adef2a2e705eef0f656"}, + {file = "MarkupSafe-2.1.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7a07f40ef8f0fbc5ef1000d0c78771f4d5ca03b4953fc162749772916b298fc4"}, + {file = "MarkupSafe-2.1.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d18b66fe626ac412d96c2ab536306c736c66cf2a31c243a45025156cc190dc8a"}, + {file = "MarkupSafe-2.1.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:698e84142f3f884114ea8cf83e7a67ca8f4ace8454e78fe960646c6c91c63bfa"}, + {file = "MarkupSafe-2.1.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49a3b78a5af63ec10d8604180380c13dcd870aba7928c1fe04e881d5c792dc4e"}, + {file = "MarkupSafe-2.1.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:15866d7f2dc60cfdde12ebb4e75e41be862348b4728300c36cdf405e258415ec"}, + {file = "MarkupSafe-2.1.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6aa5e2e7fc9bc042ae82d8b79d795b9a62bd8f15ba1e7594e3db243f158b5565"}, + {file = "MarkupSafe-2.1.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:54635102ba3cf5da26eb6f96c4b8c53af8a9c0d97b64bdcb592596a6255d8518"}, + {file = "MarkupSafe-2.1.4-cp37-cp37m-win32.whl", hash = "sha256:3583a3a3ab7958e354dc1d25be74aee6228938312ee875a22330c4dc2e41beb0"}, + {file = "MarkupSafe-2.1.4-cp37-cp37m-win_amd64.whl", hash = "sha256:d6e427c7378c7f1b2bef6a344c925b8b63623d3321c09a237b7cc0e77dd98ceb"}, + {file = "MarkupSafe-2.1.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:bf1196dcc239e608605b716e7b166eb5faf4bc192f8a44b81e85251e62584bd2"}, + {file = "MarkupSafe-2.1.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4df98d4a9cd6a88d6a585852f56f2155c9cdb6aec78361a19f938810aa020954"}, + {file = "MarkupSafe-2.1.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b835aba863195269ea358cecc21b400276747cc977492319fd7682b8cd2c253d"}, + {file = "MarkupSafe-2.1.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23984d1bdae01bee794267424af55eef4dfc038dc5d1272860669b2aa025c9e3"}, + {file = "MarkupSafe-2.1.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c98c33ffe20e9a489145d97070a435ea0679fddaabcafe19982fe9c971987d5"}, + {file = "MarkupSafe-2.1.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9896fca4a8eb246defc8b2a7ac77ef7553b638e04fbf170bff78a40fa8a91474"}, + {file = "MarkupSafe-2.1.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b0fe73bac2fed83839dbdbe6da84ae2a31c11cfc1c777a40dbd8ac8a6ed1560f"}, + {file = "MarkupSafe-2.1.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c7556bafeaa0a50e2fe7dc86e0382dea349ebcad8f010d5a7dc6ba568eaaa789"}, + {file = "MarkupSafe-2.1.4-cp38-cp38-win32.whl", hash = "sha256:fc1a75aa8f11b87910ffd98de62b29d6520b6d6e8a3de69a70ca34dea85d2a8a"}, + {file = "MarkupSafe-2.1.4-cp38-cp38-win_amd64.whl", hash = "sha256:3a66c36a3864df95e4f62f9167c734b3b1192cb0851b43d7cc08040c074c6279"}, + {file = "MarkupSafe-2.1.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:765f036a3d00395a326df2835d8f86b637dbaf9832f90f5d196c3b8a7a5080cb"}, + {file = "MarkupSafe-2.1.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:21e7af8091007bf4bebf4521184f4880a6acab8df0df52ef9e513d8e5db23411"}, + {file = "MarkupSafe-2.1.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5c31fe855c77cad679b302aabc42d724ed87c043b1432d457f4976add1c2c3e"}, + {file = "MarkupSafe-2.1.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7653fa39578957bc42e5ebc15cf4361d9e0ee4b702d7d5ec96cdac860953c5b4"}, + {file = "MarkupSafe-2.1.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47bb5f0142b8b64ed1399b6b60f700a580335c8e1c57f2f15587bd072012decc"}, + {file = "MarkupSafe-2.1.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fe8512ed897d5daf089e5bd010c3dc03bb1bdae00b35588c49b98268d4a01e00"}, + {file = "MarkupSafe-2.1.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:36d7626a8cca4d34216875aee5a1d3d654bb3dac201c1c003d182283e3205949"}, + {file = "MarkupSafe-2.1.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b6f14a9cd50c3cb100eb94b3273131c80d102e19bb20253ac7bd7336118a673a"}, + {file = "MarkupSafe-2.1.4-cp39-cp39-win32.whl", hash = "sha256:c8f253a84dbd2c63c19590fa86a032ef3d8cc18923b8049d91bcdeeb2581fbf6"}, + {file = "MarkupSafe-2.1.4-cp39-cp39-win_amd64.whl", hash = "sha256:8b570a1537367b52396e53325769608f2a687ec9a4363647af1cded8928af959"}, + {file = "MarkupSafe-2.1.4.tar.gz", hash = "sha256:3aae9af4cac263007fd6309c64c6ab4506dd2b79382d9d19a1994f9240b8db4f"}, ] [[package]] @@ -1971,14 +2013,14 @@ traitlets = "*" [[package]] name = "mccabe" -version = "0.6.1" +version = "0.7.0" description = "McCabe checker, plugin for flake8" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.6" files = [ - {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, - {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, ] [[package]] @@ -2161,14 +2203,14 @@ files = [ [[package]] name = "nautobot" -version = "1.6.8" +version = "1.6.9" description = "Source of truth and network automation platform." category = "main" optional = false python-versions = ">=3.8,<3.12" files = [ - {file = "nautobot-1.6.8-py3-none-any.whl", hash = "sha256:6e8b350d01021a88f58bec971093f57b1ed793f74e3a139cd6b20e04d11afc42"}, - {file = "nautobot-1.6.8.tar.gz", hash = "sha256:664d1f2bc1ff466007f047e89c82659b6b7e9d1d1a63758de73cfcd3a0c791e7"}, + {file = "nautobot-1.6.9-py3-none-any.whl", hash = "sha256:f011bb7d91f4aff24b8bf8903b37eea8b4826ae1b34e41a75397601b5d917831"}, + {file = "nautobot-1.6.9.tar.gz", hash = "sha256:08943c35036448b32653b054ff5adeea217a48ada645229b53d6608eb5b17c36"}, ] [package.dependencies] @@ -2469,22 +2511,6 @@ files = [ docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] -[[package]] -name = "pluggy" -version = "1.3.0" -description = "plugin and hook calling mechanisms for python" -category = "dev" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, - {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] - [[package]] name = "prometheus-client" version = "0.17.1" @@ -2566,6 +2592,7 @@ files = [ {file = "psycopg2_binary-2.9.9-cp311-cp311-win32.whl", hash = "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d"}, {file = "psycopg2_binary-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996"}, @@ -2574,6 +2601,8 @@ files = [ {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-win32.whl", hash = "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab"}, {file = "psycopg2_binary-2.9.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a"}, {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9"}, {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77"}, @@ -2640,14 +2669,14 @@ tests = ["pytest"] [[package]] name = "pycodestyle" -version = "2.7.0" +version = "2.9.1" description = "Python style guide checker" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.6" files = [ - {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, - {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, + {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, + {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, ] [[package]] @@ -2682,14 +2711,14 @@ toml = ["tomli (>=1.2.3)"] [[package]] name = "pyflakes" -version = "2.3.1" +version = "2.5.0" description = "passive checker of Python programs" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.6" files = [ - {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, - {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, + {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, + {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, ] [[package]] @@ -2728,24 +2757,24 @@ tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] [[package]] name = "pylint" -version = "3.0.3" +version = "2.17.7" description = "python code static checker" category = "dev" optional = false -python-versions = ">=3.8.0" +python-versions = ">=3.7.2" files = [ - {file = "pylint-3.0.3-py3-none-any.whl", hash = "sha256:7a1585285aefc5165db81083c3e06363a27448f6b467b3b0f30dbd0ac1f73810"}, - {file = "pylint-3.0.3.tar.gz", hash = "sha256:58c2398b0301e049609a8429789ec6edf3aabe9b6c5fec916acd18639c16de8b"}, + {file = "pylint-2.17.7-py3-none-any.whl", hash = "sha256:27a8d4c7ddc8c2f8c18aa0050148f89ffc09838142193fdbe98f172781a3ff87"}, + {file = "pylint-2.17.7.tar.gz", hash = "sha256:f4fcac7ae74cfe36bc8451e931d8438e4a476c20314b1101c458ad0f05191fad"}, ] [package.dependencies] -astroid = ">=3.0.1,<=3.1.0-dev0" +astroid = ">=2.15.8,<=2.17.0-dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = [ {version = ">=0.2", markers = "python_version < \"3.11\""}, {version = ">=0.3.6", markers = "python_version >= \"3.11\""}, ] -isort = ">=4.2.5,<5.13.0 || >5.13.0,<6" +isort = ">=4.2.5,<6" mccabe = ">=0.6,<0.8" platformdirs = ">=2.2.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} @@ -2775,6 +2804,24 @@ pylint-plugin-utils = ">=0.8" [package.extras] with-django = ["Django (>=2.2)"] +[[package]] +name = "pylint-nautobot" +version = "0.2.1" +description = "Custom Pylint Rules for Nautobot" +category = "dev" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "pylint_nautobot-0.2.1-py3-none-any.whl", hash = "sha256:6656cd571d6e997e6d7e37631308f1de25949a596a8309ab6d47a2e387c892c6"}, + {file = "pylint_nautobot-0.2.1.tar.gz", hash = "sha256:2872106a29236b0e31293efe4a2d02a66527c67f33437f3e2345251c4cf71b4d"}, +] + +[package.dependencies] +importlib-resources = ">=5.12.0,<6.0.0" +pylint = ">=2.13,<3.0" +pyyaml = ">=6.0,<7.0" +tomli = ">=2.0.1,<3.0.0" + [[package]] name = "pylint-plugin-utils" version = "0.8.2" @@ -2870,29 +2917,6 @@ files = [ {file = "pyrsistent-0.20.0.tar.gz", hash = "sha256:4c48f78f62ab596c679086084d0dd13254ae4f3d6c72a83ffdf5ebdef8f265a4"}, ] -[[package]] -name = "pytest" -version = "7.4.4" -description = "pytest: simple powerful testing with Python" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, - {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=0.12,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} - -[package.extras] -testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] - [[package]] name = "python-crontab" version = "3.0.0" @@ -2960,55 +2984,55 @@ files = [ [[package]] name = "pyuwsgi" -version = "2.0.23" +version = "2.0.23.post0" description = "The uWSGI server" category = "main" optional = false python-versions = "*" files = [ - {file = "pyuwsgi-2.0.23-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0bb538ef57960389d67bcd4a9e7ebb562ed13a4556a5596305ce5361e121fc4e"}, - {file = "pyuwsgi-2.0.23-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd9689290c3b4afec7d28f1c43ec60f9ee905abf66a501584454cbf6b620678"}, - {file = "pyuwsgi-2.0.23-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80e6fd3a9f49fad9404dd2622116db16990dd9c5061461fd700a82b429f0ee2b"}, - {file = "pyuwsgi-2.0.23-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4796cb1d35eff2cdae6ea01ffb26d2ec0ddf5c692d9f4bf5a28cab61baf78f4"}, - {file = "pyuwsgi-2.0.23-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:366dbc57eaee7b37f3e1c4039fcd7ba2a5693579e17ba07704038ffa28a8be57"}, - {file = "pyuwsgi-2.0.23-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:40ddfcb7d972cac169e62253027f932bb047a995cfbe98398c1451b137e3cf8d"}, - {file = "pyuwsgi-2.0.23-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f4dc785d94878088fd2b4b6da7a630b5538d461b92b6a767cb56401dac1373b9"}, - {file = "pyuwsgi-2.0.23-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbde1da759d1486d6b20938b8f03b84b4dfe4a1b7ba111c586b1eaed6cd85cdc"}, - {file = "pyuwsgi-2.0.23-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12568dbacacd02b22791b352c3e93a9307d565512a851b36483ffe4db69b711e"}, - {file = "pyuwsgi-2.0.23-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9db7b77bf6ee429da0583f36f168bcf1294195d7a4ac53b53d1f5d8ac8c2717"}, - {file = "pyuwsgi-2.0.23-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1de2f99dc4642aea7226889c76083884260920adc14a4a533660479941c6e6f2"}, - {file = "pyuwsgi-2.0.23-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fbad05b405630ddaaf8010822fc8bc553551bcf691df2d1ffbfd4d2204f9973f"}, - {file = "pyuwsgi-2.0.23-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:39107b8abaf488e890d53372bef7b80fdf350b703bbfa2f4ded1002eea31b198"}, - {file = "pyuwsgi-2.0.23-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:feb783ef451dc09cd37b2376ccc9e8ff28d3296542df0351e0a4502c8fac765c"}, - {file = "pyuwsgi-2.0.23-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ea11e270161e5cc8f6935778841f30e3226b0ee3b70185d88d8fa2bf0317bdc9"}, - {file = "pyuwsgi-2.0.23-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3608203a37ebf5580f3fc4901ae1295fd181caa7ec49d29b7dcc1864725049e"}, - {file = "pyuwsgi-2.0.23-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bf9d22dd5397a80cf91242f173c4bab0104c7c8b17d286b289a9582a30643cac"}, - {file = "pyuwsgi-2.0.23-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6505cb52b25eecf81338b9f17f4b47ec6288f3911eb65a5a9f3be03ed2ba0b97"}, - {file = "pyuwsgi-2.0.23-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:563270210d79a9e1a76ead34dec40b0ddf1491ac44e02e9d9fd41f8e08938f07"}, - {file = "pyuwsgi-2.0.23-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1883c08aa902dbeb7bd70c5ea319452ecbce49adc715ece4c4bef8c0acfb8523"}, - {file = "pyuwsgi-2.0.23-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:79641c8fccc507288b58805c0edb0540713b9fb65d445d703329606a3fbc2fab"}, - {file = "pyuwsgi-2.0.23-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:02a21ce1175599d0e9d63dc3bb576f7662e1ba3412b746bd9780708f55b35587"}, - {file = "pyuwsgi-2.0.23-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d72e622517522df0e8e04fc1f2aff0d1cafeececc44eecf6f83646f405ef474f"}, - {file = "pyuwsgi-2.0.23-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58cb2c48bfb34b73f5a7586c55d2e29e927a7ca6ca45153e9d860d380f4d6ef1"}, - {file = "pyuwsgi-2.0.23-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7684b4c97bb0d52f3e53f5f67a39241ed1ea234e4a8c351a7ea4a4cfd397909"}, - {file = "pyuwsgi-2.0.23-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:4983d2f201d14bf7ed6ec2f6e9449e046440476877e55b1cf6f165d2eb6d3cf4"}, - {file = "pyuwsgi-2.0.23-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:462dccd00ad01a33744a7c061fa2080b58e6b4c0f25cb95e8f9628a42d10f04f"}, - {file = "pyuwsgi-2.0.23-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:764e833b890a82cf94f60087147bd98d8d8769e133e1c1289cd7b8af4d4e19ee"}, - {file = "pyuwsgi-2.0.23-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:120ae908df0b006d1e88b43a3dfbb2f02212ac768d75baefc2a20cdf1b279b11"}, - {file = "pyuwsgi-2.0.23-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78d8ab2ac544a80bfb57a3019f1768e2ca327993f3a2e39aee92b0a70746f0bb"}, - {file = "pyuwsgi-2.0.23-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1e3151b5495e3b1882b07a72e040e7a0422e8e5e58ceafc4cc046428c781f86"}, - {file = "pyuwsgi-2.0.23-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56ba238ccf4e12de0bba0ee7d92e316e3acda22419e3403dc0d474420baf3d71"}, - {file = "pyuwsgi-2.0.23-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:173709af71a86d9efee16a702933fee2ee3e6ac6b7f80eee86414bab0c80838a"}, - {file = "pyuwsgi-2.0.23-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a35ab28beba766f89c7a0db6a6a0fcedb72d7c9ff3262f3f27418bf5b757602e"}, - {file = "pyuwsgi-2.0.23-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0fd1f679c4597641bb30887e9180c42dfabf4b3e7e2747425f4468fe93a17e51"}, - {file = "pyuwsgi-2.0.23-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7c8eca007320f91f4009eca578e3014a443e7f7b33dabb2454754971fd5df4c0"}, - {file = "pyuwsgi-2.0.23-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cebebc9a322f3d5caf19938114d66ff341852756511f99f1892fbc684120501"}, - {file = "pyuwsgi-2.0.23-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c8f2311699e2562670e3ce979bbb566302e7951e758ee80f77a42f1e13a2e221"}, - {file = "pyuwsgi-2.0.23-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf687febfb7f1cfcbedb07762f39279df8725e9e681a859448ee1c1e79a39180"}, - {file = "pyuwsgi-2.0.23-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b27a7dd26e134c134ba0ed17bc28209eb459709480bdc773ce7da5ecc016c81a"}, - {file = "pyuwsgi-2.0.23-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:447a2a72e4285a1617154c496005fbaf1fbf5b3cf6e81186a13e3627ed7b0994"}, - {file = "pyuwsgi-2.0.23-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a13932ba8079b627d6233c8a04c1544bbe2a9007ddeed7f68f46401b1d0c5d5d"}, - {file = "pyuwsgi-2.0.23.tar.gz", hash = "sha256:74ac3e9c641969a3073c67793773a73bd7968ddcc3fa810c5396415e80cc0df1"}, + {file = "pyuwsgi-2.0.23.post0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:49dfe43726f4a71d3440f7a36eb3ba5b361e04807164d34ececda138e2dc2375"}, + {file = "pyuwsgi-2.0.23.post0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65420b185003dd5b66f41a6d1aa03d63d953a18e818bd4a013fc8e9d580f11cb"}, + {file = "pyuwsgi-2.0.23.post0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7bc7c60d8e1242b3a638754d2487c505112c642010c460442993be85f3ca9ec7"}, + {file = "pyuwsgi-2.0.23.post0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1ae2abaa47cb9c0018c790935897aec8001fb709dfac54286a37ab2e0b88dca"}, + {file = "pyuwsgi-2.0.23.post0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:af376cafca1501b2d4b8184c427c55b32c1a3dcb6070dc27115ca552898c7ff8"}, + {file = "pyuwsgi-2.0.23.post0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f56a729808ed7aa1d7973d6f900a75bc36b976b7ab6c8867064f36e34cdafd4e"}, + {file = "pyuwsgi-2.0.23.post0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4270e68bb2633b0fc132aad6d415e4e0cde67093a97e64dd84bd186264a8c083"}, + {file = "pyuwsgi-2.0.23.post0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:97c940a69242dc45658dba3330e64d809f34e33d9631547b6928fd20075b4bb9"}, + {file = "pyuwsgi-2.0.23.post0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8cac396c2e8e0d199bde9bb8fc90538c82207d0c3d722d08b9a63619b41945d6"}, + {file = "pyuwsgi-2.0.23.post0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:59d6a718ad42be54b2b80c8c236b728b8b83fb93438786e95f63fc259229ccd7"}, + {file = "pyuwsgi-2.0.23.post0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c38b5bb59e1bf59030f2d43a3e67aa18e6089c8e7f43e9c5f2099567466d35f4"}, + {file = "pyuwsgi-2.0.23.post0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7199009447770812056a5b417c4847bd44db1b0230d4bb64c48a4ffacd4e96f0"}, + {file = "pyuwsgi-2.0.23.post0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f361d168cf175796fe36ab6a88dee079245a2f08e587e8190a38bd1b33238fa8"}, + {file = "pyuwsgi-2.0.23.post0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:52a45e98fe746ae9c9437c5b6f0cdb6117f979c8800f09c8e4dae2997786affd"}, + {file = "pyuwsgi-2.0.23.post0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f7455976abfa1dd43b5f3376f7f04a925c16babba1c3fc6edcdd81f5c0f24383"}, + {file = "pyuwsgi-2.0.23.post0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:508f5d84cd677cecc640d0e321badc61080c40c61843cd130b32f356729a599f"}, + {file = "pyuwsgi-2.0.23.post0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dcf93afec49f5cf29b0a68f4d2fb3e44a3ad1f205704ab2f41f9db47dacb8e13"}, + {file = "pyuwsgi-2.0.23.post0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a19ab0d5c43bc179a70cb079feb7804e39be6326bf98ec38808fcea5e7d44bd0"}, + {file = "pyuwsgi-2.0.23.post0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8c5283e38c4fd3130cd7384d57535d60435c63b81a41a6463f26f340efeda9de"}, + {file = "pyuwsgi-2.0.23.post0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:0d9dfb79bffa552e5985385bc114ecec1d4079b95ce24796f577ef0df727da06"}, + {file = "pyuwsgi-2.0.23.post0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b531ac80155b6c839215d05f95569b34e614e97aab055072c74112b1d2a45546"}, + {file = "pyuwsgi-2.0.23.post0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:eae183104f3fa26f3d9c28fe75f2ad914e3a365103a6a66e329c0f59f9e461d4"}, + {file = "pyuwsgi-2.0.23.post0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a34ab2863ff0120c6e0e75c63c9ced462bfb4777e6b8237e4e1df60fb34af51"}, + {file = "pyuwsgi-2.0.23.post0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc18481f336be63e80fc983aaa1a040e7c69c25c3145edcf93f0e6de2f1ad0d6"}, + {file = "pyuwsgi-2.0.23.post0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:245da016b424c261d148bbb83d2407aac77e6d5793cbd4e23a17f7e3a8aa061f"}, + {file = "pyuwsgi-2.0.23.post0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8de1d975be958cff9122ecc82bf393bf7f41fff6f1047e76ed972047763bbd31"}, + {file = "pyuwsgi-2.0.23.post0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d75859311605a510a6050ec622ec4beb9f2f8cce5f090e5cea70a1ff74133f8b"}, + {file = "pyuwsgi-2.0.23.post0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d3ad00212ffbb208b7146744ad3710b908734f844b5e2bf533fb09fc44726f37"}, + {file = "pyuwsgi-2.0.23.post0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:374142b106de187c4572b4441a367fa3466d9ea5aaabe475da42bb9f2202a690"}, + {file = "pyuwsgi-2.0.23.post0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:137db348bd5f585e8e5a609046d3ac9ef58483bba93de1e3c568c1a860c31b9c"}, + {file = "pyuwsgi-2.0.23.post0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52b7a837dbc8702b245481514a32c88418a42df7b5ee68d45695eba457abd3ee"}, + {file = "pyuwsgi-2.0.23.post0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfcfeb1eaca5f4dd0e6ed9194e7ec98dcb3a8ac108e8f0414ed7c28d608517ef"}, + {file = "pyuwsgi-2.0.23.post0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7887c2acc8262223ff9cdce974851da0917818c12ef3ec0f49ec11a9943731fe"}, + {file = "pyuwsgi-2.0.23.post0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:bae72689ddf8e0bdd1a974a364ed052dd19d7897f1d5c3efcf8d9010c60f56ef"}, + {file = "pyuwsgi-2.0.23.post0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9565569474f9e9f02f6fa490d96d8c5c7e3004829c01c0446cdb74c618b6a433"}, + {file = "pyuwsgi-2.0.23.post0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6ba86c6aa815635eefe7728b9b219af281a4e956bab240c5871db6c151c300a8"}, + {file = "pyuwsgi-2.0.23.post0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29ab8a02e812fbc34026ddb79f274a574c96fc488f384f320d3af37bd7edf932"}, + {file = "pyuwsgi-2.0.23.post0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4f9c0694a11d8dfbbe2814b8b242a7c4dfa143b63e01447fabce9966a90fa60"}, + {file = "pyuwsgi-2.0.23.post0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f75e45e14462cbb94fc32242378eef7bda97173de57a68a5d46e4053677a7547"}, + {file = "pyuwsgi-2.0.23.post0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e7140fc3548cd9d0f02c4511b679ba47d26593d2cceb249d2d147c9901d90022"}, + {file = "pyuwsgi-2.0.23.post0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ed348cc4c5a4964c8e8fa61ab0ef50c00f7676179a6c0cb0f55f0122db1db1c2"}, + {file = "pyuwsgi-2.0.23.post0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:17a8818ec98f92e7935cf0ff56ed4f02a069362e10554df969f70fcdf78d9199"}, + {file = "pyuwsgi-2.0.23.post0.tar.gz", hash = "sha256:04ec79c4a3acad21002ebf1479050c3208605d27cc6659008df51092951eeb8e"}, ] [[package]] @@ -3024,6 +3048,7 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -3031,8 +3056,16 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -3049,6 +3082,7 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -3056,6 +3090,7 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -3671,6 +3706,86 @@ files = [ {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, ] +[[package]] +name = "wrapt" +version = "1.16.0" +description = "Module for decorators, wrappers and monkey patching." +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, + {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, + {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, + {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, + {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, + {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, + {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, + {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, + {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"}, + {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"}, + {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"}, + {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"}, + {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"}, + {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, + {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, + {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, + {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, + {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, +] + [[package]] name = "yamllint" version = "1.33.0" @@ -3712,4 +3827,4 @@ nautobot = ["nautobot"] [metadata] lock-version = "2.0" python-versions = ">=3.8,<3.12" -content-hash = "8ec4ecf63625568afcbfb1ff021f998969cf00efa320323c380084253a5323f3" \ No newline at end of file +content-hash = "72179cef06ccc57bdf0ca30e13bce1b29d71e245aabd605b319c2e6e423f1a48" diff --git a/pyproject.toml b/pyproject.toml index 6ff8caf8..74f1a861 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -111,7 +111,7 @@ notes = """, [tool.pylint-nautobot] supported_nautobot_versions = [ - "1.6.8" + "1.6.0" ] [tool.pydocstyle] diff --git a/tasks.py b/tasks.py index cdafbe3b..f4b98cbf 100644 --- a/tasks.py +++ b/tasks.py @@ -46,7 +46,7 @@ def is_truthy(arg): namespace.configure( { "nautobot_design_builder": { - "nautobot_ver": "1.6.8", + "nautobot_ver": "1.6.0", "project_name": "nautobot-design-builder", "python_ver": "3.11", "local": False,