From a89b7ced9774150384057fb0fdcfed9e32dd6b45 Mon Sep 17 00:00:00 2001 From: bakebot Date: Tue, 7 Jan 2025 18:42:16 +0000 Subject: [PATCH 1/2] Cookie updated by NetworkToCode Cookie Drift Manager Tool Template: ``` { "template": "https://github.com/nautobot/cookiecutter-nautobot-app.git", "dir": "nautobot-app", "ref": "refs/tags/nautobot-app-v2.4.1", "path": null } ``` Cookie: ``` { "remote": "https://github.com/nautobot/nautobot-app-golden-config.git", "path": "/tmp/tmpjpcsgj8q/nautobot-app-golden-config", "repository_path": "/tmp/tmpjpcsgj8q/nautobot-app-golden-config", "dir": "", "branch_prefix": "drift-manager", "context": { "codeowner_github_usernames": "@itdependsnetworks @jeffkala @nkallergis", "full_name": "Network to Code, LLC", "email": "opensource@networktocode.com", "github_org": "nautobot", "app_name": "nautobot_golden_config", "verbose_name": "Golden Config", "app_slug": "nautobot-golden-config", "project_slug": "nautobot-app-golden-config", "repo_url": "https://github.com/nautobot/nautobot-app-golden-config", "base_url": "golden-config", "min_nautobot_version": "2.0.0", "max_nautobot_version": "2.9999", "camel_name": "NautobotGoldenConfig", "project_short_description": "An app for configuration on nautobot", "model_class_name": "ComplianceFeature", "open_source_license": "Apache-2.0", "docs_base_url": "https://docs.nautobot.com", "docs_app_url": "https://docs.nautobot.com/projects/golden-config/en/latest", "_template": "https://github.com/nautobot/cookiecutter-nautobot-app.git", "_output_dir": "/tmp/tmpjpcsgj8q", "_repo_dir": "/github/home/.cookiecutters/cookiecutter-nautobot-app/nautobot-app", "_checkout": "refs/tags/nautobot-app-v2.4.1" }, "base_branch": "develop", "remote_name": "origin", "pull_request_strategy": "PullRequestStrategy.CREATE", "post_actions": [ "PostAction.RUFF", "PostAction.POETRY" ], "baked_commit_ref": "69ab82f79f346d3e9e9cf62432bdd6d6b9c53e3f", "draft": false } ``` CLI Arguments: ``` { "cookie_dir": "", "input": false, "json_filename": "", "output_dir": "", "push": true, "template": "", "template_dir": "", "template_ref": "refs/tags/nautobot-app-v2.4.1", "pull_request": null, "post_action": [ "ruff", "poetry" ], "disable_post_actions": true, "draft": false } ``` --- .cookiecutter.json | 4 +- .../pull_request_template.md | 0 .github/workflows/ci.yml | 16 + LICENSE | 2 +- changes/+nautobot-app-v2.4.1.housekeeping | 1 + development/docker-compose.dev.yml | 6 +- development/docker-compose.mysql.yml | 7 + development/nautobot_config.py | 14 +- mkdocs.yml | 1 + nautobot_golden_config/api/serializers.py | 132 +- nautobot_golden_config/api/urls.py | 32 +- nautobot_golden_config/api/views.py | 366 +--- nautobot_golden_config/filters.py | 443 +--- nautobot_golden_config/forms.py | 619 +----- nautobot_golden_config/models.py | 844 +------- nautobot_golden_config/navigation.py | 156 +- nautobot_golden_config/tables.py | 528 +---- .../compliancefeature_retrieve.html | 33 +- nautobot_golden_config/tests/fixtures.py | 10 + .../tests/test_api_views.py | 27 + .../tests/test_filter_compliancefeature.py | 28 + .../tests/test_form_compliancefeature.py | 33 + .../tests/test_model_compliancefeature.py | 22 + nautobot_golden_config/tests/test_views.py | 405 +--- nautobot_golden_config/urls.py | 25 +- nautobot_golden_config/views.py | 597 +----- poetry.lock | 1797 +++-------------- tasks.py | 28 +- 28 files changed, 548 insertions(+), 5628 deletions(-) rename .github/{PULL_REQUEST_TEMPLATE => }/pull_request_template.md (100%) create mode 100644 changes/+nautobot-app-v2.4.1.housekeeping create mode 100644 nautobot_golden_config/tests/fixtures.py create mode 100644 nautobot_golden_config/tests/test_api_views.py create mode 100644 nautobot_golden_config/tests/test_filter_compliancefeature.py create mode 100644 nautobot_golden_config/tests/test_form_compliancefeature.py create mode 100644 nautobot_golden_config/tests/test_model_compliancefeature.py diff --git a/.cookiecutter.json b/.cookiecutter.json index cf3814078..5461cc33f 100644 --- a/.cookiecutter.json +++ b/.cookiecutter.json @@ -21,7 +21,7 @@ "_drift_manager": { "template": "https://github.com/nautobot/cookiecutter-nautobot-app.git", "template_dir": "nautobot-app", - "template_ref": "refs/tags/nautobot-app-v2.4.0", + "template_ref": "refs/tags/nautobot-app-v2.4.1", "cookie_dir": "", "branch_prefix": "drift-manager", "pull_request_strategy": "create", @@ -30,7 +30,7 @@ "poetry" ], "draft": false, - "baked_commit_ref": "69ab82f79f346d3e9e9cf62432bdd6d6b9c53e3f" + "baked_commit_ref": "baf8508b44f904c4c60e5d72dc19abc323508d8c" } } } diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/pull_request_template.md similarity index 100% rename from .github/PULL_REQUEST_TEMPLATE/pull_request_template.md rename to .github/pull_request_template.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca6509bff..a87ed1ab7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,8 @@ jobs: uses: "actions/checkout@v4" - name: "Setup environment" uses: "networktocode/gh-action-setup-poetry-environment@v6" + with: + poetry-version: "1.8.5" - name: "Linting: ruff format" run: "poetry run invoke ruff --action format" ruff-lint: @@ -37,6 +39,8 @@ jobs: uses: "actions/checkout@v4" - name: "Setup environment" uses: "networktocode/gh-action-setup-poetry-environment@v6" + with: + poetry-version: "1.8.5" - name: "Linting: ruff" run: "poetry run invoke ruff --action lint" check-docs-build: @@ -48,6 +52,8 @@ jobs: uses: "actions/checkout@v4" - name: "Setup environment" uses: "networktocode/gh-action-setup-poetry-environment@v6" + with: + poetry-version: "1.8.5" - name: "Check Docs Build" run: "poetry run invoke build-and-check-docs" poetry: @@ -59,6 +65,8 @@ jobs: uses: "actions/checkout@v4" - name: "Setup environment" uses: "networktocode/gh-action-setup-poetry-environment@v6" + with: + poetry-version: "1.8.5" - name: "Checking: poetry lock file" run: "poetry run invoke lock --check" yamllint: @@ -70,6 +78,8 @@ jobs: uses: "actions/checkout@v4" - name: "Setup environment" uses: "networktocode/gh-action-setup-poetry-environment@v6" + with: + poetry-version: "1.8.5" - name: "Linting: yamllint" run: "poetry run invoke yamllint" check-in-docker: @@ -92,6 +102,8 @@ jobs: uses: "actions/checkout@v4" - name: "Setup environment" uses: "networktocode/gh-action-setup-poetry-environment@v6" + with: + poetry-version: "1.8.5" - name: "Constrain Nautobot version and regenerate lock file" env: INVOKE_NAUTOBOT_GOLDEN_CONFIG_LOCAL: "true" @@ -147,6 +159,8 @@ jobs: uses: "actions/checkout@v4" - name: "Setup environment" uses: "networktocode/gh-action-setup-poetry-environment@v6" + with: + poetry-version: "1.8.5" - name: "Constrain Nautobot version and regenerate lock file" env: INVOKE_NAUTOBOT_GOLDEN_CONFIG_LOCAL: "true" @@ -188,6 +202,8 @@ jobs: fetch-depth: "0" - name: "Setup environment" uses: "networktocode/gh-action-setup-poetry-environment@v6" + with: + poetry-version: "1.8.5" - name: "Check for changelog entry" run: | git fetch --no-tags origin +refs/heads/${{ github.base_ref }}:refs/remotes/origin/${{ github.base_ref }} diff --git a/LICENSE b/LICENSE index bf295f493..e923d1255 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ Apache Software License 2.0 -Copyright (c) 2024, Network to Code, LLC +Copyright (c) 2025, Network to Code, LLC Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/changes/+nautobot-app-v2.4.1.housekeeping b/changes/+nautobot-app-v2.4.1.housekeeping new file mode 100644 index 000000000..317148814 --- /dev/null +++ b/changes/+nautobot-app-v2.4.1.housekeeping @@ -0,0 +1 @@ +Rebaked from the cookie `nautobot-app-v2.4.1`. diff --git a/development/docker-compose.dev.yml b/development/docker-compose.dev.yml index 3a7ec4ffc..2a1987278 100644 --- a/development/docker-compose.dev.yml +++ b/development/docker-compose.dev.yml @@ -59,10 +59,12 @@ services: volumes: - "./nautobot_config.py:/opt/nautobot/nautobot_config.py" - "../:/source" -# To expose postgres or redis to the host uncomment the following -# postgres: +# To expose postgres (5432), myql (3306) on db service or redis (6379) to the host uncomment the +# following. Ensure to match the 2 idented spaces which to have the service nested under services. +# db: # ports: # - "5432:5432" +# - "3306:3306" # redis: # ports: # - "6379:6379" diff --git a/development/docker-compose.mysql.yml b/development/docker-compose.mysql.yml index dbe31cba4..6751d7207 100644 --- a/development/docker-compose.mysql.yml +++ b/development/docker-compose.mysql.yml @@ -14,6 +14,13 @@ services: - "development.env" - "creds.env" - "development_mysql.env" + beat: + environment: + - "NAUTOBOT_DB_ENGINE=django.db.backends.mysql" + env_file: + - "development.env" + - "creds.env" + - "development_mysql.env" db: image: "mysql:8" command: diff --git a/development/nautobot_config.py b/development/nautobot_config.py index 49a529dc9..45b81386b 100644 --- a/development/nautobot_config.py +++ b/development/nautobot_config.py @@ -4,7 +4,7 @@ import sys 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 nautobot.core.settings_funcs import is_truthy # # Debug @@ -65,16 +65,8 @@ # # 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", - }, - } -} +# Inherited from nautobot.core.settings +# CACHES = {....} # # Celery settings are not defined here because they can be overloaded with diff --git a/mkdocs.yml b/mkdocs.yml index e7d70585b..555487ff2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -181,6 +181,7 @@ nav: - Extending the App: "dev/extending.md" - Contributing to the App: "dev/contributing.md" - Development Environment: "dev/dev_environment.md" + - Release Checklist: "dev/release_checklist.md" - Architecture Decision Records: "dev/arch_decision.md" - Code Reference: - "dev/code_reference/index.md" diff --git a/nautobot_golden_config/api/serializers.py b/nautobot_golden_config/api/serializers.py index 73c30a54c..f6b88ddc0 100644 --- a/nautobot_golden_config/api/serializers.py +++ b/nautobot_golden_config/api/serializers.py @@ -1,136 +1,18 @@ -"""REST API serializer capabilities for graphql app.""" +"""API serializers for nautobot_golden_config.""" -# pylint: disable=too-many-ancestors -from nautobot.core.api.serializers import NautobotModelSerializer -from nautobot.dcim.api.serializers import DeviceSerializer -from nautobot.dcim.models import Device -from nautobot.extras.api.mixins import TaggedModelSerializerMixin -from rest_framework import serializers +from nautobot.apps.api import NautobotModelSerializer, TaggedModelSerializerMixin from nautobot_golden_config import models -from nautobot_golden_config.utilities.config_postprocessing import get_config_postprocessing -class GraphQLSerializer(serializers.Serializer): # pylint: disable=abstract-method - """Serializer for a GraphQL object.""" - - data = serializers.JSONField() - - -class ComplianceFeatureSerializer(NautobotModelSerializer, TaggedModelSerializerMixin): - """Serializer for ComplianceFeature object.""" +class ComplianceFeatureSerializer(NautobotModelSerializer, TaggedModelSerializerMixin): # pylint: disable=too-many-ancestors + """ComplianceFeature Serializer.""" class Meta: - """Set Meta Data for ComplianceFeature, will serialize all fields.""" + """Meta attributes.""" model = models.ComplianceFeature fields = "__all__" - -class ComplianceRuleSerializer(NautobotModelSerializer, TaggedModelSerializerMixin): - """Serializer for ComplianceRule object.""" - - class Meta: - """Set Meta Data for ComplianceRule, will serialize all fields.""" - - model = models.ComplianceRule - fields = "__all__" - - -class ConfigComplianceSerializer(NautobotModelSerializer, TaggedModelSerializerMixin): - """Serializer for ConfigCompliance object.""" - - class Meta: - """Set Meta Data for ConfigCompliance, will serialize fields.""" - - model = models.ConfigCompliance - fields = "__all__" - - -class GoldenConfigSerializer(NautobotModelSerializer, TaggedModelSerializerMixin): - """Serializer for GoldenConfig object.""" - - class Meta: - """Set Meta Data for GoldenConfig, will serialize all fields.""" - - model = models.GoldenConfig - fields = "__all__" - - -class GoldenConfigSettingSerializer(NautobotModelSerializer, TaggedModelSerializerMixin): - """Serializer for GoldenConfigSetting object.""" - - class Meta: - """Set Meta Data for GoldenConfigSetting, will serialize all fields.""" - - model = models.GoldenConfigSetting - fields = "__all__" - - -class ConfigRemoveSerializer(NautobotModelSerializer, TaggedModelSerializerMixin): - """Serializer for ConfigRemove object.""" - - class Meta: - """Set Meta Data for ConfigRemove, will serialize all fields.""" - - model = models.ConfigRemove - fields = "__all__" - - -class ConfigReplaceSerializer(NautobotModelSerializer, TaggedModelSerializerMixin): - """Serializer for ConfigReplace object.""" - - class Meta: - """Set Meta Data for ConfigReplace, will serialize all fields.""" - - model = models.ConfigReplace - fields = "__all__" - - -class ConfigToPushSerializer(DeviceSerializer): # pylint: disable=nb-sub-class-name - """Serializer for ConfigToPush view.""" - - config = serializers.SerializerMethodField() - - class Meta(DeviceSerializer.Meta): - """Extend the Device serializer with the configuration after postprocessing.""" - - fields = "__all__" - model = Device - - def get_config(self, obj): - """Provide the intended configuration ready after postprocessing to the config field.""" - request = self.context.get("request") - config_details = models.GoldenConfig.objects.get(device=obj) - return get_config_postprocessing(config_details, request) - - -class RemediationSettingSerializer(NautobotModelSerializer, TaggedModelSerializerMixin): - """Serializer for RemediationSetting object.""" - - class Meta: - """Set Meta Data for RemediationSetting, will serialize all fields.""" - - model = models.RemediationSetting - fields = "__all__" - - -class ConfigPlanSerializer(NautobotModelSerializer, TaggedModelSerializerMixin): - """Serializer for ConfigPlan object.""" - - class Meta: - """Set Meta Data for ConfigPlan, will serialize all fields.""" - - model = models.ConfigPlan - fields = "__all__" - read_only_fields = ["device", "plan_type", "feature", "config_set"] - - -class GenerateIntendedConfigSerializer(serializers.Serializer): # pylint: disable=abstract-method - """Serializer for GenerateIntendedConfigView.""" - - intended_config = serializers.CharField(read_only=True) - intended_config_lines = serializers.ListField(read_only=True, child=serializers.CharField()) - graphql_data = serializers.JSONField(read_only=True) - diff = serializers.CharField(read_only=True) - diff_lines = serializers.ListField(read_only=True, child=serializers.CharField()) + # Option for disabling write for certain fields: + # read_only_fields = [] diff --git a/nautobot_golden_config/api/urls.py b/nautobot_golden_config/api/urls.py index d6b201c89..6ba20c08e 100644 --- a/nautobot_golden_config/api/urls.py +++ b/nautobot_golden_config/api/urls.py @@ -1,33 +1,11 @@ -"""API for Custom Jobs .""" +"""Django API urlpatterns declaration for nautobot_golden_config app.""" -from django.urls import path -from nautobot.core.api.routers import OrderedDefaultRouter +from nautobot.apps.api import OrderedDefaultRouter from nautobot_golden_config.api import views router = OrderedDefaultRouter() -router.APIRootView = views.GoldenConfigRootView -router.register("compliance-feature", views.ComplianceFeatureViewSet) -router.register("compliance-rule", views.ComplianceRuleViewSet) -router.register("config-compliance", views.ConfigComplianceViewSet) -router.register("golden-config", views.GoldenConfigViewSet) -router.register("golden-config-settings", views.GoldenConfigSettingViewSet) -router.register("config-remove", views.ConfigRemoveViewSet) -router.register("config-replace", views.ConfigReplaceViewSet) -router.register("remediation-setting", views.RemediationSettingViewSet) -router.register("config-postprocessing", views.ConfigToPushViewSet) -router.register("config-plan", views.ConfigPlanViewSet) +# add the name of your api endpoint, usually hyphenated model name in plural, e.g. "my-model-classes" +router.register("compliancefeature", views.ComplianceFeatureViewSet) -urlpatterns = [ - path( - "sotagg//", - views.SOTAggDeviceDetailView.as_view(), - name="device_detail", - ), - path( - "generate-intended-config/", - views.GenerateIntendedConfigView.as_view(), - name="generate_intended_config", - ), -] -urlpatterns += router.urls +urlpatterns = router.urls diff --git a/nautobot_golden_config/api/views.py b/nautobot_golden_config/api/views.py index 11c239510..225b061ec 100644 --- a/nautobot_golden_config/api/views.py +++ b/nautobot_golden_config/api/views.py @@ -1,371 +1,17 @@ -"""View for Golden Config APIs.""" +"""API views for nautobot_golden_config.""" -import datetime -import difflib -import json -import logging -from pathlib import Path - -from django.contrib.contenttypes.models import ContentType -from django.utils.timezone import make_aware -from drf_spectacular.types import OpenApiTypes -from drf_spectacular.utils import OpenApiParameter, extend_schema -from jinja2.exceptions import TemplateError, TemplateSyntaxError -from nautobot.apps.utils import render_jinja2 -from nautobot.core.api.views import ( - BulkDestroyModelMixin, - BulkUpdateModelMixin, - ModelViewSetMixin, - NautobotAPIVersionMixin, -) -from nautobot.dcim.models import Device -from nautobot.extras.api.views import NautobotModelViewSet, NotesViewSetMixin -from nautobot.extras.datasources.git import ensure_git_repository -from nautobot.extras.models import GraphQLQuery -from nautobot_plugin_nornir.constants import NORNIR_SETTINGS -from nornir import InitNornir -from nornir_nautobot.plugins.tasks.dispatcher import dispatcher -from rest_framework import mixins, status, viewsets -from rest_framework.exceptions import APIException -from rest_framework.generics import GenericAPIView -from rest_framework.mixins import DestroyModelMixin, ListModelMixin, RetrieveModelMixin, UpdateModelMixin -from rest_framework.permissions import AllowAny, BasePermission, IsAuthenticated -from rest_framework.response import Response -from rest_framework.routers import APIRootView -from rest_framework.views import APIView -from rest_framework.viewsets import GenericViewSet +from nautobot.apps.api import NautobotModelViewSet from nautobot_golden_config import filters, models from nautobot_golden_config.api import serializers -from nautobot_golden_config.utilities.graphql import graph_ql_query -from nautobot_golden_config.utilities.helper import dispatch_params, get_device_to_settings_map, get_django_env - - -class GoldenConfigRootView(APIRootView): - """Golden Config API root view.""" - - def get_view_name(self): - """Golden Config API root view boilerplate.""" - return "Golden Config" - - -class SOTAggDeviceDetailView(APIView): - """Detail REST API view showing graphql, with a potential "transformer" of data on a specific device.""" - - permission_classes = [AllowAny] - - def get(self, request, *args, **kwargs): - """Get method serialize for a dictionary to json response.""" - device = Device.objects.get(pk=kwargs["pk"]) - settings = get_device_to_settings_map(queryset=Device.objects.filter(pk=device.pk))[device.id] - status_code, data = graph_ql_query(request, device, settings.sot_agg_query.query) - data = json.loads(json.dumps(data)) - return Response(serializers.GraphQLSerializer(data=data).initial_data, status=status_code) -class ComplianceRuleViewSet(NautobotModelViewSet): # pylint:disable=too-many-ancestors - """API viewset for interacting with ComplianceRule objects.""" - - queryset = models.ComplianceRule.objects.all() - serializer_class = serializers.ComplianceRuleSerializer - filterset_class = filters.ComplianceRuleFilterSet - - -class ComplianceFeatureViewSet(NautobotModelViewSet): # pylint:disable=too-many-ancestors - """API viewset for interacting with ComplianceFeature objects.""" +class ComplianceFeatureViewSet(NautobotModelViewSet): # pylint: disable=too-many-ancestors + """ComplianceFeature viewset.""" queryset = models.ComplianceFeature.objects.all() serializer_class = serializers.ComplianceFeatureSerializer filterset_class = filters.ComplianceFeatureFilterSet - -class ConfigComplianceViewSet(NautobotModelViewSet): # pylint:disable=too-many-ancestors - """API viewset for interacting with ConfigCompliance objects.""" - - queryset = models.ConfigCompliance.objects.all() - serializer_class = serializers.ConfigComplianceSerializer - filterset_class = filters.ConfigComplianceFilterSet - - -class GoldenConfigViewSet(NautobotModelViewSet): # pylint:disable=too-many-ancestors - """API viewset for interacting with GoldenConfig objects.""" - - queryset = models.GoldenConfig.objects.all() - serializer_class = serializers.GoldenConfigSerializer - filterset_class = filters.GoldenConfigFilterSet - - -class GoldenConfigSettingViewSet(NautobotModelViewSet): # pylint:disable=too-many-ancestors - """API viewset for interacting with GoldenConfigSetting objects.""" - - queryset = models.GoldenConfigSetting.objects.all() - serializer_class = serializers.GoldenConfigSettingSerializer - filterset_class = filters.GoldenConfigSettingFilterSet - - -class ConfigRemoveViewSet(NautobotModelViewSet): # pylint:disable=too-many-ancestors - """API viewset for interacting with ConfigRemove objects.""" - - queryset = models.ConfigRemove.objects.all() - serializer_class = serializers.ConfigRemoveSerializer - filterset_class = filters.ConfigRemoveFilterSet - - -class ConfigReplaceViewSet(NautobotModelViewSet): # pylint:disable=too-many-ancestors - """API viewset for interacting with ConfigReplace objects.""" - - queryset = models.ConfigReplace.objects.all() - serializer_class = serializers.ConfigReplaceSerializer - filterset_class = filters.ConfigReplaceFilterSet - - -class ConfigPushPermissions(BasePermission): - """Permissions class to validate access to Devices and GoldenConfig view.""" - - def has_permission(self, request, view): - """Method to validated permissions to API view.""" - return request.user.has_perm("nautobot_golden_config.view_goldenconfig") - - def has_object_permission(self, request, view, obj): - """Validate user access to the object, taking into account constraints.""" - return request.user.has_perm("dcim.view_device", obj=obj) - - -class ConfigToPushViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet): - """Detail REST API view showing configuration after postprocessing.""" - - permission_classes = [IsAuthenticated & ConfigPushPermissions] - queryset = Device.objects.all() - serializer_class = serializers.ConfigToPushSerializer - - -class RemediationSettingViewSet(NautobotModelViewSet): # pylint:disable=too-many-ancestors - """API viewset for interacting with RemediationSetting objects.""" - - queryset = models.RemediationSetting.objects.all() - serializer_class = serializers.RemediationSettingSerializer - filterset_class = filters.RemediationSettingFilterSet - - -class ConfigPlanViewSet( - NautobotAPIVersionMixin, - NotesViewSetMixin, - ModelViewSetMixin, - RetrieveModelMixin, - UpdateModelMixin, - DestroyModelMixin, - ListModelMixin, - BulkUpdateModelMixin, - BulkDestroyModelMixin, - GenericViewSet, -): # pylint:disable=too-many-ancestors - """API viewset for interacting with ConfigPlan objects. Does not support POST to create objects.""" - - queryset = models.ConfigPlan.objects.all() - serializer_class = serializers.ConfigPlanSerializer - filterset_class = filters.ConfigPlanFilterSet - - def get_serializer_context(self): - """Gather all custom fields for the model. Copied from nautobot.extras.api.views.CustomFieldModelViewSet.""" - content_type = ContentType.objects.get_for_model(self.queryset.model) - custom_fields = content_type.custom_fields.all() - - context = super().get_serializer_context() - context.update( - { - "custom_fields": custom_fields, - } - ) - return context - - -class GenerateIntendedConfigException(APIException): - """Exception for when the intended config cannot be generated.""" - - status_code = 400 - default_detail = "Unable to generate the intended config for this device." - default_code = "error" - - -def _nornir_task_inject_graphql_data(task, graphql_data, **kwargs): - """Inject the GraphQL data into the Nornir task host data and then run nornir_nautobot.plugins.tasks.dispatcher.dispatcher subtask. - - This is a small stub of the logic in nautobot_golden_config.nornir_plays.config_intended.run_template. - """ - task.host.data.update(graphql_data) - generated_config = task.run(task=dispatcher, name="GENERATE CONFIG", **kwargs) - return generated_config - - -class GenerateIntendedConfigView(NautobotAPIVersionMixin, GenericAPIView): - """API view for generating the intended config for a Device.""" - - name = "Generate Intended Config for Device" - permission_classes = [IsAuthenticated] - serializer_class = serializers.GenerateIntendedConfigSerializer - - def _get_diff(self, device, intended_config): - """Generate a unified diff between the provided config and the intended config stored on the Device's GoldenConfig.intended_config.""" - diff = None - try: - golden_config = device.goldenconfig - if golden_config.intended_last_success_date is not None: - prior_intended_config = golden_config.intended_config - diff = "".join( - difflib.unified_diff( - prior_intended_config.splitlines(keepends=True), - intended_config.splitlines(keepends=True), - fromfile="prior intended config", - tofile="rendered config", - ) - ) - except models.GoldenConfig.DoesNotExist: - pass - - return diff - - def _get_object(self, request, model, query_param): - """Get the requested model instance, restricted to requesting user.""" - pk = request.query_params.get(query_param) - if not pk: - raise GenerateIntendedConfigException(f"Parameter {query_param} is required") - try: - return model.objects.restrict(request.user, "view").get(pk=pk) - except model.DoesNotExist as exc: - raise GenerateIntendedConfigException(f"{model.__name__} with id '{pk}' not found") from exc - - def _get_jinja_template_path(self, settings, device, git_repository): - """Get the Jinja template path for the device in the provided git repository.""" - try: - rendered_path = render_jinja2(template_code=settings.jinja_path_template, context={"obj": device}) - except (TemplateSyntaxError, TemplateError) as exc: - raise GenerateIntendedConfigException("Error rendering Jinja path template") from exc - filesystem_path = Path(git_repository.filesystem_path) / rendered_path - if not filesystem_path.is_file(): - msg = f"Jinja template {filesystem_path} not found in git repository {git_repository}" - raise GenerateIntendedConfigException(msg) - return filesystem_path - - @extend_schema( - parameters=[ - OpenApiParameter( - name="device_id", - required=True, - type=OpenApiTypes.UUID, - location=OpenApiParameter.QUERY, - ), - OpenApiParameter( - name="graphql_query_id", - required=False, - type=OpenApiTypes.UUID, - location=OpenApiParameter.QUERY, - ), - ] - ) - def get(self, request, *args, **kwargs): - """Generate intended configuration for a Device.""" - device = self._get_object(request, Device, "device_id") - graphql_query = None - graphql_query_id_param = request.query_params.get("graphql_query_id") - if graphql_query_id_param: - try: - graphql_query = GraphQLQuery.objects.get(pk=request.query_params.get("graphql_query_id")) - except GraphQLQuery.DoesNotExist as exc: - raise GenerateIntendedConfigException( - f"GraphQLQuery with id '{graphql_query_id_param}' not found" - ) from exc - settings = models.GoldenConfigSetting.objects.get_for_device(device) - if not settings: - raise GenerateIntendedConfigException("No Golden Config settings found for this device") - if not settings.jinja_repository: - raise GenerateIntendedConfigException("Golden Config settings jinja_repository not set") - - if graphql_query is None: - if settings.sot_agg_query is not None: - graphql_query = settings.sot_agg_query - else: - raise GenerateIntendedConfigException("Golden Config settings sot_agg_query not set") - - if "device_id" not in graphql_query.variables: - raise GenerateIntendedConfigException("The selected GraphQL query is missing a 'device_id' variable") - - try: - git_repository = settings.jinja_repository - ensure_git_repository(git_repository) - except Exception as exc: - raise GenerateIntendedConfigException("Error trying to sync git repository") from exc - - filesystem_path = self._get_jinja_template_path(settings, device, git_repository) - - status_code, graphql_data = graph_ql_query(request, device, graphql_query.query) - if status_code == status.HTTP_200_OK: - try: - intended_config = self._render_config_nornir_serial( - device=device, - jinja_template=filesystem_path.name, - jinja_root_path=filesystem_path.parent, - graphql_data=graphql_data, - ) - except Exception as exc: - raise GenerateIntendedConfigException(f"Error rendering Jinja template: {exc}") from exc - - diff = self._get_diff(device, intended_config) - - return Response( - data={ - "intended_config": intended_config, - "intended_config_lines": intended_config.split("\n"), - "graphql_data": graphql_data, - "diff": diff, - "diff_lines": diff.split("\n") if diff else [], - }, - status=status.HTTP_200_OK, - ) - - raise GenerateIntendedConfigException("Unable to generate the intended config for this device") - - def _render_config_nornir_serial(self, device, jinja_template, jinja_root_path, graphql_data): - """Render the Jinja template for the device using Nornir serial runner. - - This is a small stub of the logic in nornir_plays.config_intended.config_intended. - """ - jinja_env = get_django_env() - with InitNornir( - runner={"plugin": "serial"}, - logging={"enabled": False}, - inventory={ - "plugin": "nautobot-inventory", - "options": { - "credentials_class": NORNIR_SETTINGS.get("credentials"), - "params": NORNIR_SETTINGS.get("inventory_params"), - "queryset": Device.objects.filter(pk=device.pk), - "defaults": {"now": make_aware(datetime.datetime.now())}, - }, - }, - ) as nornir_obj: - results = nornir_obj.run( - task=_nornir_task_inject_graphql_data, - name="REST API GENERATE CONFIG", - graphql_data=graphql_data, - obj=device, # Used by the nornir tasks for logging to the logger below - logger=logging.getLogger( - dispatcher.__module__ - ), # The nornir tasks are built for logging to a JobResult, pass a standard logger here - jinja_template=jinja_template, - jinja_root_path=jinja_root_path, - output_file_location="/dev/null", # The nornir task outputs the templated config to a file, but this API doesn't need it - jinja_filters=jinja_env.filters, - jinja_env=jinja_env, - **dispatch_params( - "generate_config", device.platform.network_driver, logging.getLogger(dispatch_params.__module__) - ), - ) - if results[device.name].failed: - if results[device.name].exception: # pylint: disable=no-else-raise - raise results[device.name].exception - else: - raise GenerateIntendedConfigException( - f"Error generating intended config for {device.name}: {results[device.name].result}" - ) - else: - return results[device.name][1][1][0].result["config"] + # Option for modifying the default HTTP methods: + # http_method_names = ["get", "post", "put", "patch", "delete", "head", "options", "trace"] diff --git a/nautobot_golden_config/filters.py b/nautobot_golden_config/filters.py index 2223200af..333644c96 100644 --- a/nautobot_golden_config/filters.py +++ b/nautobot_golden_config/filters.py @@ -1,446 +1,17 @@ -"""Filters for UI and API Views.""" +"""Filtering for nautobot_golden_config.""" -import django_filters -from nautobot.core.filters import MultiValueDateTimeFilter, SearchFilter, TreeNodeMultipleChoiceFilter -from nautobot.dcim.models import Device, DeviceType, Location, Manufacturer, Platform, Rack, RackGroup -from nautobot.extras.filters import NaturalKeyOrPKMultipleChoiceFilter, NautobotFilterSet, StatusFilter -from nautobot.extras.models import JobResult, Role, Status -from nautobot.tenancy.models import Tenant, TenantGroup +from nautobot.apps.filters import NameSearchFilterSet, NautobotFilterSet from nautobot_golden_config import models -class GoldenConfigFilterSet(NautobotFilterSet): - """Filter capabilities for GoldenConfig instances.""" - - @staticmethod - def _get_filter_lookup_dict(existing_filter): - """Extend method to account for isnull on datetime types.""" - # Choose the lookup expression map based on the filter type - lookup_map = NautobotFilterSet._get_filter_lookup_dict(existing_filter) - if isinstance(existing_filter, MultiValueDateTimeFilter): - lookup_map.update({"isnull": "isnull"}) - return lookup_map - - q = SearchFilter( - filter_predicates={ - "device__name": { - "lookup_expr": "icontains", - "preprocessor": str, - }, - }, - ) - tenant_group_id = TreeNodeMultipleChoiceFilter( - queryset=TenantGroup.objects.all(), - field_name="device__tenant__tenant_group", - to_field_name="id", - label="Tenant Group (ID)", - ) - tenant_group = TreeNodeMultipleChoiceFilter( - queryset=TenantGroup.objects.all(), - field_name="device__tenant__tenant_group", - to_field_name="name", - label="Tenant Group (name)", - ) - tenant = NaturalKeyOrPKMultipleChoiceFilter( - queryset=Tenant.objects.all(), - field_name="device__tenant", - to_field_name="name", - label="Tenant (name or ID)", - ) - location_id = TreeNodeMultipleChoiceFilter( - # Not limiting to content_type=dcim.device to allow parent locations to be included - # i.e. include all Sites in a Region, even though Region can't be assigned to a Device - queryset=Location.objects.all(), - field_name="device__location", - to_field_name="id", - label="Location (ID)", - ) - location = TreeNodeMultipleChoiceFilter( - # Not limiting to content_type=dcim.device to allow parent locations to be included - # i.e. include all sites in a Region, even though Region can't be assigned to a Device - queryset=Location.objects.all(), - field_name="device__location", - to_field_name="name", - label="Location (name)", - ) - rack_group_id = TreeNodeMultipleChoiceFilter( - queryset=RackGroup.objects.all(), - field_name="device__rack__rack_group", - to_field_name="id", - label="Rack group (ID)", - ) - rack_group = TreeNodeMultipleChoiceFilter( - queryset=RackGroup.objects.all(), - field_name="device__rack__rack_group", - to_field_name="name", - label="Rack group (name)", - ) - rack = NaturalKeyOrPKMultipleChoiceFilter( - field_name="device__rack", - queryset=Rack.objects.all(), - to_field_name="name", - label="Rack (name or ID)", - ) - role = NaturalKeyOrPKMultipleChoiceFilter( - field_name="device__role", - queryset=Role.objects.filter(content_types__model="device"), - to_field_name="name", - label="Role (name or ID)", - ) - manufacturer = NaturalKeyOrPKMultipleChoiceFilter( - field_name="device__device_type__manufacturer", - queryset=Manufacturer.objects.all(), - to_field_name="name", - label="Manufacturer (name or ID)", - ) - platform = NaturalKeyOrPKMultipleChoiceFilter( - field_name="device__platform", - queryset=Platform.objects.all(), - to_field_name="name", - label="Platform (name or ID)", - ) - device_status = StatusFilter( - field_name="device__status", - queryset=Status.objects.all(), - label="Device Status", - ) - device_type = NaturalKeyOrPKMultipleChoiceFilter( - field_name="device__device_type", - queryset=DeviceType.objects.all(), - to_field_name="model", - label="DeviceType (model or ID)", - ) - device = NaturalKeyOrPKMultipleChoiceFilter( - field_name="device", - queryset=Device.objects.all(), - to_field_name="name", - label="Device (name or ID)", - ) +class ComplianceFeatureFilterSet(NautobotFilterSet, NameSearchFilterSet): # pylint: disable=too-many-ancestors + """Filter for ComplianceFeature.""" class Meta: - """Meta class attributes for GoldenConfigFilter.""" - - model = models.GoldenConfig - distinct = True - fields = "__all__" - - -class ConfigComplianceFilterSet(GoldenConfigFilterSet): # pylint: disable=too-many-ancestors - """Filter capabilities for ConfigCompliance instances.""" - - feature_id = django_filters.ModelMultipleChoiceFilter( - field_name="rule__feature", - queryset=models.ComplianceFeature.objects.all(), - label="ComplianceFeature (ID)", - ) - feature = django_filters.ModelMultipleChoiceFilter( - field_name="rule__feature__slug", - queryset=models.ComplianceFeature.objects.all(), - to_field_name="slug", - label="ComplianceFeature (slug)", - ) - - class Meta: - """Meta class attributes for ConfigComplianceFilter.""" - - model = models.ConfigCompliance - fields = "__all__" - - -class ComplianceFeatureFilterSet(NautobotFilterSet): - """Inherits Base Class NautobotFilterSet.""" - - q = SearchFilter( - filter_predicates={ - "name": { - "lookup_expr": "icontains", - "preprocessor": str, - }, - }, - ) - - class Meta: - """Boilerplate filter Meta data for compliance feature.""" + """Meta attributes for filter.""" model = models.ComplianceFeature - fields = "__all__" - - -class ComplianceRuleFilterSet(NautobotFilterSet): - """Inherits Base Class NautobotFilterSet.""" - - q = SearchFilter( - filter_predicates={ - "feature__name": { - "lookup_expr": "icontains", - "preprocessor": str, - }, - }, - ) - platform = NaturalKeyOrPKMultipleChoiceFilter( - field_name="platform", - queryset=Platform.objects.all(), - to_field_name="name", - label="Platform (name or ID)", - ) - - class Meta: - """Boilerplate filter Meta data for compliance rule.""" - - model = models.ComplianceRule - fields = "__all__" - - -class ConfigRemoveFilterSet(NautobotFilterSet): - """Inherits Base Class NautobotFilterSet.""" - - q = SearchFilter( - filter_predicates={ - "name": { - "lookup_expr": "icontains", - "preprocessor": str, - }, - }, - ) - platform = NaturalKeyOrPKMultipleChoiceFilter( - field_name="platform", - queryset=Platform.objects.all(), - to_field_name="name", - label="Platform (name or ID)", - ) - - class Meta: - """Boilerplate filter Meta data for Config Remove.""" - - model = models.ConfigRemove - fields = "__all__" - - -class ConfigReplaceFilterSet(NautobotFilterSet): - """Inherits Base Class NautobotFilterSet.""" - - q = SearchFilter( - filter_predicates={ - "name": { - "lookup_expr": "icontains", - "preprocessor": str, - }, - }, - ) - platform = NaturalKeyOrPKMultipleChoiceFilter( - field_name="platform", - queryset=Platform.objects.all(), - to_field_name="name", - label="Platform (name or ID)", - ) - - class Meta: - """Boilerplate filter Meta data for Config Replace.""" - - model = models.ConfigReplace - fields = "__all__" - - -class GoldenConfigSettingFilterSet(NautobotFilterSet): - """Inherits Base Class NautobotFilterSet.""" - - device_id = django_filters.ModelMultipleChoiceFilter( - queryset=Device.objects.all(), - label="Device (ID)", - method="filter_device_id", - ) - - def filter_device_id(self, queryset, name, value): # pylint: disable=unused-argument - """Filter by Device ID.""" - if not value: - return queryset - golden_config_setting_ids = [] - for instance in value: - if isinstance(instance, Device): - device = instance - else: - device = Device.objects.get(id=instance) - golden_config_setting = models.GoldenConfigSetting.objects.get_for_device(device) - if golden_config_setting is not None: - golden_config_setting_ids.append(golden_config_setting.id) - return queryset.filter(id__in=golden_config_setting_ids) - - class Meta: - """Boilerplate filter Meta data for Config Remove.""" - - model = models.GoldenConfigSetting - fields = "__all__" - - -class RemediationSettingFilterSet(NautobotFilterSet): - """Inherits Base Class CustomFieldModelFilterSet.""" - - q = SearchFilter( - filter_predicates={ - "platform__name": { - "lookup_expr": "icontains", - "preprocessor": str, - }, - "remediation_type": { - "lookup_expr": "icontains", - "preprocessor": str, - }, - }, - ) - platform = django_filters.ModelMultipleChoiceFilter( - field_name="platform__name", - queryset=Platform.objects.all(), - to_field_name="name", - label="Platform Name", - ) - platform_id = django_filters.ModelMultipleChoiceFilter( - queryset=Platform.objects.all(), - label="Platform ID", - ) - - class Meta: - """Boilerplate filter Meta data for Remediation Setting.""" - - model = models.RemediationSetting - fields = "__all__" - - -class ConfigPlanFilterSet(NautobotFilterSet): - """Inherits Base Class NautobotFilterSet.""" - - q = SearchFilter( - filter_predicates={ - "device__name": { - "lookup_expr": "icontains", - "preprocessor": str, - }, - "change_control_id": { - "lookup_expr": "icontains", - "preprocessor": str, - }, - }, - ) - device_id = django_filters.ModelMultipleChoiceFilter( - queryset=Device.objects.all(), - label="Device ID", - ) - device = django_filters.ModelMultipleChoiceFilter( - field_name="device__name", - queryset=Device.objects.all(), - to_field_name="name", - label="Device Name", - ) - feature_id = django_filters.ModelMultipleChoiceFilter( - field_name="feature__id", - queryset=models.ComplianceFeature.objects.all(), - to_field_name="id", - label="Feature ID", - ) - feature = django_filters.ModelMultipleChoiceFilter( - field_name="feature__name", - queryset=models.ComplianceFeature.objects.all(), - to_field_name="name", - label="Feature Name", - ) - plan_result_id = django_filters.ModelMultipleChoiceFilter( - queryset=JobResult.objects.filter(config_plan__isnull=False).distinct(), - label="Plan JobResult ID", - to_field_name="id", - ) - tenant_group_id = TreeNodeMultipleChoiceFilter( - queryset=TenantGroup.objects.all(), - field_name="device__tenant__tenant_group", - to_field_name="id", - label="Tenant Group (ID)", - ) - tenant_group = TreeNodeMultipleChoiceFilter( - queryset=TenantGroup.objects.all(), - field_name="device__tenant__tenant_group", - to_field_name="name", - label="Tenant Group (name)", - ) - tenant = NaturalKeyOrPKMultipleChoiceFilter( - queryset=Tenant.objects.all(), - field_name="device__tenant", - to_field_name="name", - label="Tenant (name or ID)", - ) - manufacturer = NaturalKeyOrPKMultipleChoiceFilter( - field_name="device__device_type__manufacturer", - queryset=Manufacturer.objects.all(), - to_field_name="name", - label="Manufacturer (name or ID)", - ) - platform = NaturalKeyOrPKMultipleChoiceFilter( - field_name="device__platform", - queryset=Platform.objects.all(), - to_field_name="name", - label="Platform (name or ID)", - ) - location_id = TreeNodeMultipleChoiceFilter( - # Not limiting to content_type=dcim.device to allow parent locations to be included - # i.e. include all Sites in a Region, even though Region can't be assigned to a Device - queryset=Location.objects.all(), - field_name="device__location", - to_field_name="id", - label="Location (ID)", - ) - location = TreeNodeMultipleChoiceFilter( - # Not limiting to content_type=dcim.device to allow parent locations to be included - # i.e. include all sites in a Region, even though Region can't be assigned to a Device - queryset=Location.objects.all(), - field_name="device__location", - to_field_name="name", - label="Location (name)", - ) - deploy_result_id = django_filters.ModelMultipleChoiceFilter( - queryset=JobResult.objects.filter(config_plan__isnull=False).distinct(), - label="Deploy JobResult ID", - to_field_name="id", - ) - change_control_id = django_filters.CharFilter( - field_name="change_control_id", - lookup_expr="exact", - ) - rack_group_id = TreeNodeMultipleChoiceFilter( - queryset=RackGroup.objects.all(), - field_name="device__rack__rack_group", - to_field_name="id", - label="Rack group (ID)", - ) - rack_group = TreeNodeMultipleChoiceFilter( - queryset=RackGroup.objects.all(), - field_name="device__rack__rack_group", - to_field_name="name", - label="Rack group (name)", - ) - rack = NaturalKeyOrPKMultipleChoiceFilter( - field_name="device__rack", - queryset=Rack.objects.all(), - to_field_name="name", - label="Rack (name or ID)", - ) - role = NaturalKeyOrPKMultipleChoiceFilter( - field_name="device__role", - queryset=Role.objects.filter(content_types__model="device"), - to_field_name="name", - label="Role (name or ID)", - ) - status_id = django_filters.ModelMultipleChoiceFilter( - # field_name="status__id", - queryset=Status.objects.all(), - label="Status ID", - ) - status = django_filters.ModelMultipleChoiceFilter( - field_name="status__name", - queryset=Status.objects.all(), - to_field_name="name", - label="Status", - ) - - class Meta: - """Boilerplate filter Meta data for Config Plan.""" - model = models.ConfigPlan - fields = "__all__" + # add any fields from the model that you would like to filter your searches by using those + fields = ["id", "name", "description"] diff --git a/nautobot_golden_config/forms.py b/nautobot_golden_config/forms.py index 2bd455595..e5a35c990 100644 --- a/nautobot_golden_config/forms.py +++ b/nautobot_golden_config/forms.py @@ -1,616 +1,47 @@ -"""Forms for Device Configuration Backup.""" -# pylint: disable=too-many-ancestors +"""Forms for nautobot_golden_config.""" -import json - -import django.forms as django_forms -from nautobot.apps import forms -from nautobot.dcim.models import Device, DeviceType, Location, Manufacturer, Platform, Rack, RackGroup -from nautobot.extras.forms import NautobotBulkEditForm, NautobotFilterForm, NautobotModelForm -from nautobot.extras.models import DynamicGroup, GitRepository, GraphQLQuery, JobResult, Role, Status, Tag -from nautobot.tenancy.models import Tenant, TenantGroup +from django import forms +from nautobot.apps.forms import NautobotBulkEditForm, NautobotFilterForm, NautobotModelForm, TagsBulkEditFormMixin from nautobot_golden_config import models -from nautobot_golden_config.choices import ComplianceRuleConfigTypeChoice, ConfigPlanTypeChoice, RemediationTypeChoice - -# ConfigCompliance - - -class DeviceRelatedFilterForm(NautobotFilterForm): # pylint: disable=nb-no-model-found - """Base FilterForm for below FilterForms.""" - - tenant_group_id = forms.DynamicModelMultipleChoiceField( - queryset=TenantGroup.objects.all(), to_field_name="id", required=False, label="Tenant group ID" - ) - tenant_group = forms.DynamicModelMultipleChoiceField( - queryset=TenantGroup.objects.all(), - to_field_name="name", - required=False, - label="Tenant group name", - null_option="None", - ) - tenant = forms.DynamicModelMultipleChoiceField( - queryset=Tenant.objects.all(), - to_field_name="name", - required=False, - null_option="None", - query_params={"group": "$tenant_group"}, - ) - location_id = forms.DynamicModelMultipleChoiceField( - # Not limiting to query_params={"content_type": "dcim.device" to allow parent locations to be included - # i.e. include all sites in a Region, even though Region can't be assigned to a Device - queryset=Location.objects.all(), - to_field_name="id", - required=False, - label="Location ID", - ) - location = forms.DynamicModelMultipleChoiceField( - queryset=Location.objects.all(), to_field_name="name", required=False, label="Location name" - ) - rack_group_id = forms.DynamicModelMultipleChoiceField( - queryset=RackGroup.objects.all(), - to_field_name="id", - required=False, - label="Rack group ID", - query_params={"location": "$location"}, - ) - rack_group = forms.DynamicModelMultipleChoiceField( - queryset=RackGroup.objects.all(), - to_field_name="name", - required=False, - label="Rack group name", - query_params={"location": "$location"}, - ) - rack_id = forms.DynamicModelMultipleChoiceField( - queryset=Rack.objects.all(), - required=False, - label="Rack", - null_option="None", - query_params={ - "location": "$location", - "group_id": "$rack_group_id", - }, - ) - role = forms.DynamicModelMultipleChoiceField( - queryset=Role.objects.all(), - to_field_name="name", - required=False, - query_params={"content_types": "dcim.device"}, - ) - manufacturer = forms.DynamicModelMultipleChoiceField( - queryset=Manufacturer.objects.all(), to_field_name="name", required=False, label="Manufacturer" - ) - device_type = forms.DynamicModelMultipleChoiceField( - queryset=DeviceType.objects.all(), - required=False, - label="Model", - display_field="model", - query_params={"manufacturer": "$manufacturer"}, - ) - platform = forms.DynamicModelMultipleChoiceField( - queryset=Platform.objects.all(), to_field_name="name", required=False, null_option="None" - ) - device = forms.DynamicModelMultipleChoiceField( - queryset=Device.objects.all(), required=False, null_option="None", label="Device", to_field_name="name" - ) - - -class GoldenConfigFilterForm(DeviceRelatedFilterForm): - """Filter Form for GoldenConfig.""" - - model = models.GoldenConfig - field_order = [ - "q", - "tenant_group", - "tenant", - "location_id", - "location", - "rack_group_id", - "rack_group", - "rack_id", - "role", - "manufacturer", - "platform", - "device_status", - "device_type", - "device", - ] - q = django_forms.CharField(required=False, label="Search") - - -class GoldenConfigBulkEditForm(NautobotBulkEditForm): - """BulkEdit form for GoldenConfig instances.""" - - pk = django_forms.ModelMultipleChoiceField( - queryset=models.GoldenConfig.objects.all(), widget=django_forms.MultipleHiddenInput - ) - # description = django_forms.CharField(max_length=200, required=False) - - class Meta: - """Boilerplate form Meta data for GoldenConfig.""" - - nullable_fields = [] - - -class ConfigComplianceFilterForm(DeviceRelatedFilterForm): - """Filter Form for ConfigCompliance instances.""" - - model = models.ConfigCompliance - # Set field order to be explicit - field_order = [ - "q", - "tenant_group", - "tenant", - "location_id", - "location", - "rack_group_id", - "rack_group", - "rack_id", - "role", - "manufacturer", - "platform", - "device_status", - "device_type", - "device", - ] - - q = django_forms.CharField(required=False, label="Search") - - def __init__(self, *args, **kwargs): - """Required for status to work.""" - super().__init__(*args, **kwargs) - self.fields["device_status"] = forms.DynamicModelMultipleChoiceField( - required=False, - queryset=Status.objects.all(), - query_params={"content_types": Device._meta.label_lower}, - display_field="label", - label="Device Status", - to_field_name="name", - ) - self.order_fields(self.field_order) # Reorder fields again - - -# ComplianceRule - - -class ComplianceRuleForm(NautobotModelForm): - """Filter Form for ComplianceRule instances.""" - - platform = forms.DynamicModelChoiceField(queryset=Platform.objects.all()) - - class Meta: - """Boilerplate form Meta data for compliance rule.""" - - model = models.ComplianceRule - fields = "__all__" - - -class ComplianceRuleFilterForm(NautobotFilterForm): - """Form for ComplianceRule instances.""" - - model = models.ComplianceRule - - q = django_forms.CharField(required=False, label="Search") - platform = forms.DynamicModelMultipleChoiceField( - queryset=Platform.objects.all(), to_field_name="name", required=False, null_option="None" - ) - - feature = forms.DynamicModelMultipleChoiceField(queryset=models.ComplianceFeature.objects.all(), required=False) - - -class ComplianceRuleBulkEditForm(NautobotBulkEditForm): - """BulkEdit form for ComplianceRule instances.""" - - pk = django_forms.ModelMultipleChoiceField( - queryset=models.ComplianceRule.objects.all(), widget=django_forms.MultipleHiddenInput - ) - description = django_forms.CharField(max_length=200, required=False) - config_type = django_forms.ChoiceField( - required=False, - choices=forms.add_blank_choice(ComplianceRuleConfigTypeChoice), - ) - config_ordered = django_forms.NullBooleanField(required=False, widget=forms.BulkEditNullBooleanSelect()) - custom_compliance = django_forms.NullBooleanField(required=False, widget=forms.BulkEditNullBooleanSelect()) - config_remediation = django_forms.NullBooleanField(required=False, widget=forms.BulkEditNullBooleanSelect()) - - class Meta: - """Boilerplate form Meta data for ComplianceRule.""" - - nullable_fields = [] - - -# ComplianceFeature - -class ComplianceFeatureForm(NautobotModelForm): - """Filter Form for ComplianceFeature instances.""" - slug = forms.SlugField() # TODO: 2.1: Change from slugs once django-pivot is figured out +class ComplianceFeatureForm(NautobotModelForm): # pylint: disable=too-many-ancestors + """ComplianceFeature creation/edit form.""" class Meta: - """Boilerplate form Meta data for compliance feature.""" + """Meta attributes.""" model = models.ComplianceFeature - fields = "__all__" - - -class ComplianceFeatureFilterForm(NautobotFilterForm): - """Form for ComplianceFeature instances.""" - - model = models.ComplianceFeature - q = django_forms.CharField(required=False, label="Search") - name = forms.DynamicModelChoiceField(queryset=models.ComplianceFeature.objects.all(), required=False) - - -class ComplianceFeatureBulkEditForm(NautobotBulkEditForm): - """BulkEdit form for ComplianceFeature instances.""" - - pk = django_forms.ModelMultipleChoiceField( - queryset=models.ComplianceFeature.objects.all(), widget=django_forms.MultipleHiddenInput - ) - description = django_forms.CharField(max_length=200, required=False) - - class Meta: - """Boilerplate form Meta data for ComplianceFeature.""" - - nullable_fields = [] - - -# ConfigRemove - - -class ConfigRemoveForm(NautobotModelForm): - """Filter Form for Line Removal instances.""" - - platform = forms.DynamicModelChoiceField(queryset=Platform.objects.all()) - - class Meta: - """Boilerplate form Meta data for removal feature.""" - - model = models.ConfigRemove - fields = "__all__" - - -class ConfigRemoveFilterForm(NautobotFilterForm): - """Filter Form for Line Removal.""" - - model = models.ConfigRemove - platform = forms.DynamicModelMultipleChoiceField( - queryset=Platform.objects.all(), to_field_name="name", required=False, null_option="None" - ) - name = forms.DynamicModelChoiceField( - queryset=models.ConfigRemove.objects.all(), to_field_name="name", required=False - ) - - -class ConfigRemoveBulkEditForm(NautobotBulkEditForm): - """BulkEdit form for ConfigRemove instances.""" - - pk = django_forms.ModelMultipleChoiceField( - queryset=models.ConfigRemove.objects.all(), widget=django_forms.MultipleHiddenInput - ) - description = django_forms.CharField(max_length=200, required=False) - - class Meta: - """Boilerplate form Meta data for ConfigRemove.""" - - nullable_fields = [] - - -# ConfigReplace - - -class ConfigReplaceForm(NautobotModelForm): - """Filter Form for Line Removal instances.""" - - platform = forms.DynamicModelChoiceField(queryset=Platform.objects.all()) - - class Meta: - """Boilerplate form Meta data for removal feature.""" - - model = models.ConfigReplace - fields = "__all__" - - -class ConfigReplaceFilterForm(NautobotFilterForm): - """Filter Form for Line Replacement.""" - - model = models.ConfigReplace - - platform = forms.DynamicModelMultipleChoiceField( - queryset=Platform.objects.all(), to_field_name="name", required=False, null_option="None" - ) - name = forms.DynamicModelChoiceField( - queryset=models.ConfigReplace.objects.all(), to_field_name="name", required=False - ) - - -class ConfigReplaceBulkEditForm(NautobotBulkEditForm): - """BulkEdit form for ConfigReplace instances.""" - - pk = django_forms.ModelMultipleChoiceField( - queryset=models.ConfigReplace.objects.all(), widget=django_forms.MultipleHiddenInput - ) - description = django_forms.CharField(max_length=200, required=False) - - class Meta: - """Boilerplate form Meta data for ConfigReplace.""" - - nullable_fields = [] - - -# GoldenConfigSetting - - -class GoldenConfigSettingForm(NautobotModelForm): - """Filter Form for GoldenConfigSettingForm instances.""" - - slug = forms.SlugField() - dynamic_group = django_forms.ModelChoiceField(queryset=DynamicGroup.objects.all()) - - class Meta: - """Filter Form Meta Data for GoldenConfigSettingForm instances.""" - - model = models.GoldenConfigSetting - fields = "__all__" - - -class GoldenConfigSettingFilterForm(NautobotFilterForm): - """Form for GoldenConfigSetting instances.""" - - model = models.GoldenConfigSetting - - q = django_forms.CharField(required=False, label="Search") - name = django_forms.CharField(required=False) - weight = django_forms.IntegerField(required=False) - backup_repository = django_forms.ModelChoiceField( - queryset=GitRepository.objects.filter(provided_contents__contains="nautobot_golden_config.backupconfigs"), - required=False, - ) - intended_repository = django_forms.ModelChoiceField( - queryset=GitRepository.objects.filter(provided_contents__contains="nautobot_golden_config.intendedconfigs"), - required=False, - ) - jinja_repository = django_forms.ModelChoiceField( - queryset=GitRepository.objects.filter(provided_contents__contains="nautobot_golden_config.jinjatemplate"), - required=False, - ) - - -class GoldenConfigSettingBulkEditForm(NautobotBulkEditForm): - """BulkEdit form for GoldenConfigSetting instances.""" - - pk = django_forms.ModelMultipleChoiceField( - queryset=models.GoldenConfigSetting.objects.all(), widget=django_forms.MultipleHiddenInput - ) - - class Meta: - """Boilerplate form Meta data for GoldenConfigSetting.""" - - nullable_fields = [] - - -# Remediation Setting -class RemediationSettingForm(NautobotModelForm): - """Create/Update Form for Remediation Settings instances.""" - - class Meta: - """Boilerplate form Meta data for Remediation Settings.""" - - model = models.RemediationSetting - fields = "__all__" - - -class RemediationSettingFilterForm(NautobotFilterForm): - """Filter Form for Remediation Settings.""" - - model = models.RemediationSetting - q = django_forms.CharField(required=False, label="Search") - platform = forms.DynamicModelMultipleChoiceField( - queryset=Platform.objects.all(), required=False, display_field="name", to_field_name="name" - ) - remediation_type = django_forms.ChoiceField( - choices=forms.add_blank_choice(RemediationTypeChoice), - required=False, - widget=django_forms.Select(), - label="Remediation Type", - ) - - -class RemediationSettingBulkEditForm(NautobotBulkEditForm): - """BulkEdit form for RemediationSetting instances.""" - - pk = django_forms.ModelMultipleChoiceField( - queryset=models.RemediationSetting.objects.all(), widget=django_forms.MultipleHiddenInput - ) - remediation_type = django_forms.ChoiceField(choices=RemediationTypeChoice, label="Remediation Type") - - class Meta: - """Boilerplate form Meta data for RemediationSetting.""" - - nullable_fields = [] - - -# ConfigPlan - - -class ConfigPlanForm(NautobotModelForm): - """Form for ConfigPlan instances.""" - - feature = forms.DynamicModelMultipleChoiceField( - queryset=models.ComplianceFeature.objects.all(), - display_field="name", - help_text="Note: Selecting no features will generate plans for all applicable features.", - ) - commands = django_forms.CharField( - widget=django_forms.Textarea, - help_text=( - "Enter your configuration template here representing CLI configuration.
" - 'You may use Jinja2 templating. Example: {% if "foo" in bar %}foo{% endif %}
' - "You can also reference the device object with obj.
" - "For example: hostname {{ obj.name }} or ip address {{ obj.primary_ip4.host }}" - ), - ) - - tenant_group = forms.DynamicModelMultipleChoiceField(queryset=TenantGroup.objects.all(), required=False) - tenant = forms.DynamicModelMultipleChoiceField( - queryset=Tenant.objects.all(), required=False, query_params={"tenant_group": "$tenant_group"} - ) - # Requires https://github.com/nautobot/nautobot-app-golden-config/issues/430 - location = forms.DynamicModelMultipleChoiceField(queryset=Location.objects.all(), required=False) - rack_group = forms.DynamicModelMultipleChoiceField( - queryset=RackGroup.objects.all(), required=False, query_params={"location": "$location"} - ) - rack = forms.DynamicModelMultipleChoiceField( - queryset=Rack.objects.all(), required=False, query_params={"rack_group": "$rack_group", "location": "$location"} - ) - role = forms.DynamicModelMultipleChoiceField( - queryset=Role.objects.all(), required=False, query_params={"content_types": "dcim.device"} - ) - manufacturer = forms.DynamicModelMultipleChoiceField(queryset=Manufacturer.objects.all(), required=False) - platform = forms.DynamicModelMultipleChoiceField(queryset=Platform.objects.all(), required=False) - device_type = forms.DynamicModelMultipleChoiceField(queryset=DeviceType.objects.all(), required=False) - device = forms.DynamicModelMultipleChoiceField(queryset=Device.objects.all(), required=False) - tags = forms.DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), query_params={"content_types": "dcim.device"}, required=False - ) - status = forms.DynamicModelMultipleChoiceField( - queryset=Status.objects.all(), query_params={"content_types": "dcim.device"}, required=False - ) - - def __init__(self, *args, **kwargs): - """Method to get data from Python -> Django template -> JS in support of toggle form fields.""" - super().__init__(*args, **kwargs) - hide_form_data = [ - { - "event_field": "id_plan_type", - "values": [ - {"name": "manual", "show": ["id_commands"], "hide": ["id_feature"]}, - {"name": "missing", "show": ["id_feature"], "hide": ["id_commands"]}, - {"name": "intended", "show": ["id_feature"], "hide": ["id_commands"]}, - {"name": "remediation", "show": ["id_feature"], "hide": ["id_commands"]}, - {"name": "", "show": [], "hide": ["id_commands", "id_feature"]}, - ], - } + fields = [ + "name", + "description", ] - # Example of how to use this `JSON.parse('{{ form.hide_form_data|safe }}')` - self.hide_form_data = json.dumps(hide_form_data) - - class Meta: - """Boilerplate form Meta data for ConfigPlan.""" - - model = models.ConfigPlan - fields = "__all__" - - -class ConfigPlanUpdateForm(NautobotModelForm): # pylint: disable=nb-sub-class-name - """Form for ConfigPlan instances.""" - - status = forms.DynamicModelChoiceField( - queryset=Status.objects.all(), - query_params={"content_types": models.ConfigPlan._meta.label_lower}, - ) - tags = forms.DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), query_params={"content_types": "dcim.device"}, required=False - ) - - class Meta: - """Boilerplate form Meta data for ConfigPlan.""" - - model = models.ConfigPlan - fields = ( # pylint: disable=nb-use-fields-all - "change_control_id", - "change_control_url", - "status", - "tags", - ) - - -class ConfigPlanFilterForm(DeviceRelatedFilterForm): - """Filter Form for ConfigPlan.""" - - model = models.ConfigPlan - - q = django_forms.CharField(required=False, label="Search") - # device_id = forms.DynamicModelMultipleChoiceField( - # queryset=Device.objects.all(), required=False, null_option="None", label="Device" - # ) - created__lte = django_forms.DateTimeField(label="Created Before", required=False, widget=forms.DatePicker()) - created__gte = django_forms.DateTimeField(label="Created After", required=False, widget=forms.DatePicker()) - plan_type = django_forms.ChoiceField( - choices=forms.add_blank_choice(ConfigPlanTypeChoice), - required=False, - widget=django_forms.Select(), - label="Plan Type", - ) - feature = forms.DynamicModelMultipleChoiceField( - queryset=models.ComplianceFeature.objects.all(), - required=False, - null_option="None", - label="Feature", - to_field_name="name", - ) - change_control_id = django_forms.CharField(required=False, label="Change Control ID") - plan_result_id = forms.DynamicModelMultipleChoiceField( - queryset=JobResult.objects.all(), - query_params={"job_model": "Generate Config Plans"}, - label="Plan Result", - required=False, - display_field="date_created", - ) - deploy_result_id = forms.DynamicModelMultipleChoiceField( - queryset=JobResult.objects.all(), - query_params={"job_model": "Deploy Config Plans"}, - label="Deploy Result", - required=False, - display_field="date_created", - ) - status = forms.DynamicModelMultipleChoiceField( - required=False, - queryset=Status.objects.all(), - query_params={"content_types": models.ConfigPlan._meta.label_lower}, - display_field="label", - label="Status", - to_field_name="name", - ) - tags = forms.TagFilterField(model) -class ConfigPlanBulkEditForm(NautobotBulkEditForm): - """BulkEdit form for ConfigPlan instances.""" +class ComplianceFeatureBulkEditForm(TagsBulkEditFormMixin, NautobotBulkEditForm): # pylint: disable=too-many-ancestors + """ComplianceFeature bulk edit form.""" - pk = django_forms.ModelMultipleChoiceField( - queryset=models.ConfigPlan.objects.all(), widget=django_forms.MultipleHiddenInput - ) - status = forms.DynamicModelChoiceField( - queryset=Status.objects.all(), - query_params={"content_types": models.ConfigPlan._meta.label_lower}, - required=False, - ) - change_control_id = django_forms.CharField(required=False, label="Change Control ID") - change_control_url = django_forms.URLField(required=False, label="Change Control URL") + pk = forms.ModelMultipleChoiceField(queryset=models.ComplianceFeature.objects.all(), widget=forms.MultipleHiddenInput) + description = forms.CharField(required=False) class Meta: - """Boilerplate form Meta data for ConfigPlan.""" + """Meta attributes.""" nullable_fields = [ - "change_control_id", - "change_control_url", - "tags", + "description", ] -class GenerateIntendedConfigForm(django_forms.Form): - """Form for generating intended configuration.""" +class ComplianceFeatureFilterForm(NautobotFilterForm): + """Filter form to filter searches.""" - device = forms.DynamicModelChoiceField( - queryset=Device.objects.all(), - required=True, - label="Device", - ) - graphql_query = forms.DynamicModelChoiceField( - queryset=GraphQLQuery.objects.all(), - required=True, - label="GraphQL Query", - query_params={"nautobot_golden_config_graphql_query_variables": "device_id"}, + model = models.ComplianceFeature + field_order = ["q", "name"] + + q = forms.CharField( + required=False, + label="Search", + help_text="Search within Name or Slug.", ) + name = forms.CharField(required=False, label="Name") diff --git a/nautobot_golden_config/models.py b/nautobot_golden_config/models.py index bbedd8957..afc72d2c7 100644 --- a/nautobot_golden_config/models.py +++ b/nautobot_golden_config/models.py @@ -1,842 +1,38 @@ -"""Django Models for tracking the configuration compliance per feature and device.""" +"""Models for Golden Config.""" -import json -import logging -import os - -from deepdiff import DeepDiff -from django.core.exceptions import ValidationError +# Django imports from django.db import models -from django.db.models.manager import BaseManager -from django.utils.module_loading import import_string -from hier_config import Host as HierConfigHost -from nautobot.apps.models import RestrictedQuerySet -from nautobot.apps.utils import render_jinja2 -from nautobot.core.models.generics import PrimaryModel -from nautobot.core.models.utils import serialize_object, serialize_object_v2 -from nautobot.dcim.models import Device -from nautobot.extras.models import ObjectChange -from nautobot.extras.models.statuses import StatusField -from nautobot.extras.utils import extras_features -from netutils.config.compliance import feature_compliance -from xmldiff import actions, main - -from nautobot_golden_config.choices import ComplianceRuleConfigTypeChoice, ConfigPlanTypeChoice, RemediationTypeChoice -from nautobot_golden_config.utilities.constant import ENABLE_SOTAGG, PLUGIN_CFG - -LOGGER = logging.getLogger(__name__) -GRAPHQL_STR_START = "query ($device_id: ID!)" - -ERROR_MSG = ( - "There was an issue with the data that was returned by your get_custom_compliance function. " - "This is a local issue that requires the attention of your systems administrator and not something " - "that can be fixed within the Golden Config app. " -) -MISSING_MSG = ( - ERROR_MSG + "Specifically the `{}` key was not found in value the get_custom_compliance function provided." -) -VALIDATION_MSG = ( - ERROR_MSG + "Specifically the key {} was expected to be of type(s) {} and the value of {} was not that type(s)." -) - -CUSTOM_FUNCTIONS = { - "get_custom_compliance": "custom", - "get_custom_remediation": RemediationTypeChoice.TYPE_CUSTOM, -} - - -def _is_jsonable(val): - """Check is value can be converted to json.""" - try: - json.dumps(val) - return True - except (TypeError, OverflowError): - return False - - -def _null_to_empty(val): - """Convert to empty string if the value is currently null.""" - if not val: - return "" - return val - - -def _get_cli_compliance(obj): - """This function performs the actual compliance for cli configuration.""" - feature = { - "ordered": obj.rule.config_ordered, - "name": obj.rule, - } - feature.update({"section": obj.rule.match_config.splitlines()}) - value = feature_compliance( - feature, obj.actual, obj.intended, obj.device.platform.network_driver_mappings.get("netutils_parser") - ) - compliance = value["compliant"] - if compliance: - compliance_int = 1 - ordered = value["ordered_compliant"] - else: - compliance_int = 0 - ordered = value["ordered_compliant"] - missing = _null_to_empty(value["missing"]) - extra = _null_to_empty(value["extra"]) - return { - "compliance": compliance, - "compliance_int": compliance_int, - "ordered": ordered, - "missing": missing, - "extra": extra, - } - - -def _get_json_compliance(obj): - """This function performs the actual compliance for json serializable data.""" - - def _normalize_diff(diff, path_to_diff): - """Normalizes the diff to a list of keys and list indexes that have changed.""" - dictionary_items = list(diff.get(f"dictionary_item_{path_to_diff}", [])) - list_items = list(diff.get(f"iterable_item_{path_to_diff}", {}).keys()) - values_changed = list(diff.get("values_changed", {}).keys()) - type_changes = list(diff.get("type_changes", {}).keys()) - return dictionary_items + list_items + values_changed + type_changes - - diff = DeepDiff(obj.actual, obj.intended, ignore_order=obj.ordered, report_repetition=True) - if not diff: - compliance_int = 1 - compliance = True - ordered = True - missing = "" - extra = "" - else: - compliance_int = 0 - compliance = False - ordered = False - missing = _null_to_empty(_normalize_diff(diff, "added")) - extra = _null_to_empty(_normalize_diff(diff, "removed")) - - return { - "compliance": compliance, - "compliance_int": compliance_int, - "ordered": ordered, - "missing": missing, - "extra": extra, - } - -def _get_xml_compliance(obj): - """This function performs the actual compliance for xml serializable data.""" +# Nautobot imports +from nautobot.apps.models import PrimaryModel - def _normalize_diff(diff): - """Format the diff output to a list of nodes with values that have updated.""" - formatted_diff = [] - for operation in diff: - if isinstance(operation, actions.UpdateTextIn): - formatted_operation = f"{operation.node}, {operation.text}" - formatted_diff.append(formatted_operation) - return "\n".join(formatted_diff) +# from nautobot.apps.models import extras_features +# If you want to use the extras_features decorator please reference the following documentation +# https://docs.nautobot.com/projects/core/en/latest/plugins/development/#using-the-extras_features-decorator-for-graphql +# Then based on your reading you may decide to put the following decorator before the declaration of your class +# @extras_features("custom_fields", "custom_validators", "relationships", "graphql") - # Options for the diff operation. These are set to prefer updates over node insertions/deletions. - diff_options = { - "F": 0.1, - "fast_match": True, - } - missing = main.diff_texts(obj.actual, obj.intended, diff_options=diff_options) - extra = main.diff_texts(obj.intended, obj.actual, diff_options=diff_options) - compliance = not missing and not extra - compliance_int = int(compliance) - ordered = obj.ordered - missing = _null_to_empty(_normalize_diff(missing)) - extra = _null_to_empty(_normalize_diff(extra)) - - return { - "compliance": compliance, - "compliance_int": compliance_int, - "ordered": ordered, - "missing": missing, - "extra": extra, - } - - -def _verify_get_custom_compliance_data(compliance_details): - """This function verifies the data is as expected when a custom function is used.""" - for val in ["compliance", "compliance_int", "ordered", "missing", "extra"]: - try: - compliance_details[val] - except KeyError: - raise ValidationError(MISSING_MSG.format(val)) from KeyError - for val in ["compliance", "ordered"]: - if compliance_details[val] not in [True, False]: - raise ValidationError(VALIDATION_MSG.format(val, "Boolean", compliance_details[val])) - if compliance_details["compliance_int"] not in [0, 1]: - raise ValidationError(VALIDATION_MSG.format("compliance_int", "0 or 1", compliance_details["compliance_int"])) - for val in ["missing", "extra"]: - if not isinstance(compliance_details[val], str) and not _is_jsonable(compliance_details[val]): - raise ValidationError(VALIDATION_MSG.format(val, "String or Json", compliance_details[val])) - - -def _get_hierconfig_remediation(obj): - """Returns the remediating config.""" - hierconfig_os = obj.device.platform.network_driver_mappings["hier_config"] - if not hierconfig_os: - raise ValidationError(f"platform {obj.network_driver} is not supported by hierconfig.") - - try: - remediation_setting_obj = RemediationSetting.objects.get(platform=obj.rule.platform) - except Exception as err: # pylint: disable=broad-except: - raise ValidationError(f"Platform {obj.network_driver} has no Remediation Settings defined.") from err - - remediation_options = remediation_setting_obj.remediation_options - - try: - hc_kwargs = {"hostname": obj.device.name, "os": hierconfig_os} - if remediation_options: - hc_kwargs.update(hconfig_options=remediation_options) - host = HierConfigHost(**hc_kwargs) - - except Exception as err: # pylint: disable=broad-except: - raise Exception( # pylint: disable=broad-exception-raised - f"Cannot instantiate HierConfig on {obj.device.name}, check Device, Platform and Hier Options." - ) from err - - host.load_generated_config(obj.intended) - host.load_running_config(obj.actual) - host.remediation_config() - remediation_config = host.remediation_config_filtered_text(include_tags={}, exclude_tags={}) - - return remediation_config - - -# The below maps the provided compliance types -FUNC_MAPPER = { - ComplianceRuleConfigTypeChoice.TYPE_CLI: _get_cli_compliance, - ComplianceRuleConfigTypeChoice.TYPE_JSON: _get_json_compliance, - ComplianceRuleConfigTypeChoice.TYPE_XML: _get_xml_compliance, - RemediationTypeChoice.TYPE_HIERCONFIG: _get_hierconfig_remediation, -} -# The below conditionally add the custom provided compliance type -for custom_function, custom_type in CUSTOM_FUNCTIONS.items(): - if PLUGIN_CFG.get(custom_function): - try: - FUNC_MAPPER[custom_type] = import_string(PLUGIN_CFG[custom_function]) - except Exception as error: # pylint: disable=broad-except - msg = ( - "There was an issue attempting to import the custom function of" - f"{PLUGIN_CFG[custom_function]}, this is expected with a local configuration issue " - "and not related to the Golden Configuration App, please contact your system admin for further details" - ) - raise Exception(msg).with_traceback(error.__traceback__) - - -@extras_features( - "custom_fields", - "custom_validators", - "export_templates", - "graphql", - "relationships", - "webhooks", -) +# If you want to choose a specific model to overload in your class declaration, please reference the following documentation: +# how to chose a database model: https://docs.nautobot.com/projects/core/en/stable/plugins/development/#database-models class ComplianceFeature(PrimaryModel): # pylint: disable=too-many-ancestors - """ComplianceFeature details.""" + """Base model for Golden Config app.""" name = models.CharField(max_length=100, unique=True) - slug = models.SlugField(max_length=100, unique=True) description = models.CharField(max_length=200, blank=True) + # additional model fields class Meta: - """Meta information for ComplianceFeature model.""" - - ordering = ("slug",) - - def __str__(self): - """Return a sane string representation of the instance.""" - return self.slug - - -@extras_features( - "custom_fields", - "custom_validators", - "export_templates", - "graphql", - "relationships", - "webhooks", -) -class ComplianceRule(PrimaryModel): # pylint: disable=too-many-ancestors - """ComplianceRule details.""" - - feature = models.ForeignKey(to="ComplianceFeature", on_delete=models.CASCADE, related_name="feature") - - platform = models.ForeignKey( - to="dcim.Platform", - on_delete=models.CASCADE, - related_name="compliance_rules", - ) - description = models.CharField( - max_length=200, - blank=True, - ) - config_ordered = models.BooleanField( - verbose_name="Configured Ordered", - help_text="Whether or not the configuration order matters, such as in ACLs.", - default=False, - ) - - config_remediation = models.BooleanField( - default=False, - verbose_name="Config Remediation", - help_text="Whether or not the config remediation is executed for this compliance rule.", - ) - - match_config = models.TextField( - blank=True, - verbose_name="Config to Match", - help_text="The config to match that is matched based on the parent most configuration. E.g.: For CLI `router bgp` or `ntp`. For JSON this is a top level key name. For XML this is a xpath query.", - ) - config_type = models.CharField( - max_length=20, - default=ComplianceRuleConfigTypeChoice.TYPE_CLI, - choices=ComplianceRuleConfigTypeChoice, - help_text="Whether the configuration is in CLI, JSON, or XML format.", - ) - custom_compliance = models.BooleanField( - default=False, help_text="Whether this Compliance Rule is proceeded as custom." - ) - - @property - def remediation_setting(self): - """Returns remediation settings for a particular platform.""" - return RemediationSetting.objects.filter(platform=self.platform).first() - - class Meta: - """Meta information for ComplianceRule model.""" - - ordering = ("platform", "feature__name") - unique_together = ( - "feature", - "platform", - ) - - def __str__(self): - """Return a sane string representation of the instance.""" - return f"{self.platform} - {self.feature.name}" - - def clean(self): - """Verify that if cli, then match_config is set.""" - if self.config_type == ComplianceRuleConfigTypeChoice.TYPE_CLI and not self.match_config: - raise ValidationError("CLI configuration set, but no configuration set to match.") - - -@extras_features( - "custom_fields", - "custom_links", - "custom_validators", - "export_templates", - "graphql", - "relationships", - "webhooks", -) -class ConfigCompliance(PrimaryModel): # pylint: disable=too-many-ancestors - """Configuration compliance details.""" - - device = models.ForeignKey(to="dcim.Device", on_delete=models.CASCADE, help_text="The device") - rule = models.ForeignKey(to="ComplianceRule", on_delete=models.CASCADE, related_name="rule") - compliance = models.BooleanField(blank=True) - actual = models.JSONField(blank=True, help_text="Actual Configuration for feature") - intended = models.JSONField(blank=True, help_text="Intended Configuration for feature") - # these three are config snippets exposed for the ConfigDeployment. - remediation = models.JSONField(blank=True, help_text="Remediation Configuration for the device") - missing = models.JSONField(blank=True, help_text="Configuration that should be on the device.") - extra = models.JSONField(blank=True, help_text="Configuration that should not be on the device.") - ordered = models.BooleanField(default=False) - # Used for django-pivot, both compliance and compliance_int should be set. - compliance_int = models.IntegerField(blank=True) + """Meta class.""" - def to_objectchange(self, action, *, related_object=None, object_data_extra=None, object_data_exclude=None): # pylint: disable=arguments-differ - """Remove actual and intended configuration from changelog.""" - fields_to_exclude = ["actual", "intended"] - if not object_data_exclude: - object_data_exclude = fields_to_exclude - data_v2 = serialize_object_v2(self) - for field in fields_to_exclude: - data_v2.pop(field) - return ObjectChange( - changed_object=self, - object_repr=str(self), - action=action, - object_data=serialize_object(self, extra=object_data_extra, exclude=object_data_exclude), - object_data_v2=data_v2, - related_object=related_object, - ) + ordering = ["name"] - class Meta: - """Set unique together fields for model.""" + # Option for fixing capitalization (i.e. "Snmp" vs "SNMP") + # verbose_name = "Golden Config" - ordering = ["device", "rule"] - unique_together = ("device", "rule") + # Option for fixing plural name (i.e. "Chicken Tenders" vs "Chicken Tendies") + # verbose_name_plural = "Golden Configs" def __str__(self): - """String representation of a the compliance.""" - return f"{self.device} -> {self.rule} -> {self.compliance}" - - def compliance_on_save(self): - """The actual configuration compliance happens here, but the details for actual compliance job would be found in FUNC_MAPPER.""" - if self.rule.custom_compliance: - if not FUNC_MAPPER.get("custom"): - raise ValidationError( - "Custom type provided, but no `get_custom_compliance` config set, please contact system admin." - ) - compliance_details = FUNC_MAPPER["custom"](obj=self) - _verify_get_custom_compliance_data(compliance_details) - else: - compliance_details = FUNC_MAPPER[self.rule.config_type](obj=self) - - self.compliance = compliance_details["compliance"] - self.compliance_int = compliance_details["compliance_int"] - self.ordered = compliance_details["ordered"] - self.missing = compliance_details["missing"] - self.extra = compliance_details["extra"] - - def remediation_on_save(self): - """The actual remediation happens here, before saving the object.""" - if self.compliance: - self.remediation = "" - return - - if not self.rule.config_remediation: - self.remediation = "" - return - - if not self.rule.remediation_setting: - self.remediation = "" - return - - remediation_config = FUNC_MAPPER[self.rule.remediation_setting.remediation_type](obj=self) - self.remediation = remediation_config - - def save(self, *args, **kwargs): - """The actual configuration compliance happens here, but the details for actual compliance job would be found in FUNC_MAPPER.""" - self.compliance_on_save() - self.remediation_on_save() - self.full_clean() - - # This accounts for django 4.2 `Setting update_fields in Model.save() may now be required` change - # in behavior - if kwargs.get("update_fields"): - kwargs["update_fields"].update( - {"compliance", "compliance_int", "ordered", "missing", "extra", "remediation"} - ) - - super().save(*args, **kwargs) - - -@extras_features( - "custom_fields", - "custom_links", - "custom_validators", - "export_templates", - "graphql", - "relationships", - "webhooks", -) -class GoldenConfig(PrimaryModel): # pylint: disable=too-many-ancestors - """Configuration Management Model.""" - - device = models.OneToOneField( - to="dcim.Device", - on_delete=models.CASCADE, - help_text="device", - blank=False, - ) - backup_config = models.TextField(blank=True, help_text="Full backup config for device.") - backup_last_attempt_date = models.DateTimeField(null=True, blank=True) - backup_last_success_date = models.DateTimeField(null=True, blank=True) - - intended_config = models.TextField(blank=True, help_text="Intended config for the device.") - intended_last_attempt_date = models.DateTimeField(null=True, blank=True) - intended_last_success_date = models.DateTimeField(null=True, blank=True) - - compliance_config = models.TextField(blank=True, help_text="Full config diff for device.") - compliance_last_attempt_date = models.DateTimeField(null=True, blank=True) - compliance_last_success_date = models.DateTimeField(null=True, blank=True) - - def to_objectchange(self, action, *, related_object=None, object_data_extra=None, object_data_exclude=None): # pylint: disable=arguments-differ - """Remove actual and intended configuration from changelog.""" - fields_to_exclude = ["backup_config", "intended_config", "compliance_config"] - if not object_data_exclude: - object_data_exclude = fields_to_exclude - data_v2 = serialize_object_v2(self) - for field in fields_to_exclude: - data_v2.pop(field) - return ObjectChange( - changed_object=self, - object_repr=str(self), - action=action, - object_data=serialize_object(self, extra=object_data_extra, exclude=object_data_exclude), - object_data_v2=data_v2, - related_object=related_object, - ) - - @staticmethod - def get_dynamic_group_device_pks(): - """Get all Device PKs associated with GoldenConfigSetting DynamicGroups.""" - gc_dynamic_group_device_queryset = Device.objects.none() - for setting in GoldenConfigSetting.objects.all(): - # using "|" should not require calling distinct afterwards - gc_dynamic_group_device_queryset = gc_dynamic_group_device_queryset | setting.dynamic_group.members - - return set(gc_dynamic_group_device_queryset.values_list("pk", flat=True)) - - @classmethod - def get_golden_config_device_ids(cls): - """Get all Device PKs associated with GoldenConfig entries.""" - return set(cls.objects.values_list("device__pk", flat=True)) - - class Meta: - """Set unique together fields for model.""" - - ordering = ["device"] - - def __str__(self): - """String representation of a the compliance.""" - return f"{self.device}" - - -class GoldenConfigSettingManager(BaseManager.from_queryset(RestrictedQuerySet)): - """Manager for GoldenConfigSetting.""" - - def get_for_device(self, device): - """Return the highest weighted GoldenConfigSetting assigned to a device.""" - if not isinstance(device, Device): - raise ValueError("The device argument must be a Device instance.") - dynamic_group = device.dynamic_groups.exclude(golden_config_setting__isnull=True) - if dynamic_group.exists(): - return dynamic_group.order_by("-golden_config_setting__weight").first().golden_config_setting - return None - - -@extras_features( - "graphql", -) -class GoldenConfigSetting(PrimaryModel): # pylint: disable=too-many-ancestors - """GoldenConfigSetting Model definition. This provides global configs instead of via configs.py.""" - - name = models.CharField(max_length=100, unique=True) - slug = models.SlugField(max_length=100, unique=True) - weight = models.PositiveSmallIntegerField(default=1000) - description = models.CharField( - max_length=200, - blank=True, - ) - backup_repository = models.ForeignKey( - to="extras.GitRepository", - on_delete=models.PROTECT, - null=True, - blank=True, - related_name="backup_repository", - limit_choices_to={"provided_contents__contains": "nautobot_golden_config.backupconfigs"}, - ) - backup_path_template = models.CharField( - max_length=255, - blank=True, - verbose_name="Backup Path in Jinja Template Form", - help_text="The Jinja path representation of where the backup file will be found. The variable `obj` is available as the device instance object of a given device, as is the case for all Jinja templates. e.g. `{{obj.location.name|slugify}}/{{obj.name}}.cfg`", - ) - intended_repository = models.ForeignKey( - to="extras.GitRepository", - on_delete=models.PROTECT, - null=True, - blank=True, - related_name="intended_repository", - limit_choices_to={"provided_contents__contains": "nautobot_golden_config.intendedconfigs"}, - ) - intended_path_template = models.CharField( - max_length=255, - blank=True, - verbose_name="Intended Path in Jinja Template Form", - help_text="The Jinja path representation of where the generated file will be placed. e.g. `{{obj.location.name|slugify}}/{{obj.name}}.cfg`", - ) - jinja_repository = models.ForeignKey( - to="extras.GitRepository", - on_delete=models.PROTECT, - null=True, - blank=True, - related_name="jinja_template", - limit_choices_to={"provided_contents__contains": "nautobot_golden_config.jinjatemplate"}, - ) - jinja_path_template = models.CharField( - max_length=255, - blank=True, - verbose_name="Template Path in Jinja Template Form", - help_text="The Jinja path representation of where the Jinja template can be found. e.g. `{{obj.platform.network_driver}}.j2`", - ) - backup_test_connectivity = models.BooleanField( - default=True, - verbose_name="Backup Test", - help_text="Whether or not to pretest the connectivity of the device by verifying there is a resolvable IP that can connect to port 22.", - ) - sot_agg_query = models.ForeignKey( - to="extras.GraphQLQuery", - on_delete=models.PROTECT, - null=True, - blank=True, - related_name="sot_aggregation", - ) - dynamic_group = models.OneToOneField( - to="extras.DynamicGroup", - on_delete=models.PROTECT, - related_name="golden_config_setting", - ) - is_dynamic_group_associable_model = False - - objects = GoldenConfigSettingManager() - - def __str__(self): - """Return a simple string if model is called.""" - return f"Golden Config Setting - {self.name}" - - class Meta: - """Set unique fields for model. - - Provide ordering used in tables and get_device_to_settings_map. - Sorting on weight is performed from the highest weight value to the lowest weight value. - This is to ensure only one app settings could be applied per single device based on priority and name. - """ - - verbose_name = "Golden Config Setting" - ordering = ["-weight", "name"] # Refer to weight comment in class docstring. - - def clean(self): - """Validate the scope and GraphQL query.""" - super().clean() - - if ENABLE_SOTAGG and not self.sot_agg_query: - raise ValidationError("A GraphQL query must be defined when `ENABLE_SOTAGG` is True") - - if self.sot_agg_query: - LOGGER.debug("GraphQL - test query start with: `%s`", GRAPHQL_STR_START) - if not str(self.sot_agg_query.query.lstrip()).startswith(GRAPHQL_STR_START): - raise ValidationError(f"The GraphQL query must start with exactly `{GRAPHQL_STR_START}`") - - def get_queryset(self): - """Generate a Device QuerySet from the filter.""" - return self.dynamic_group.members - - def device_count(self): - """Return the number of devices in the group.""" - return self.dynamic_group.count - - def get_url_to_filtered_device_list(self): - """Get url to all devices that are matching the filter.""" - return self.dynamic_group.get_group_members_url() - - def get_jinja_template_path_for_device(self, device): - """Get the Jinja template path for a device.""" - if self.jinja_repository is not None: - rendered_path = render_jinja2(template_code=self.jinja_path_template, context={"obj": device}) - return f"{self.jinja_repository.filesystem_path}{os.path.sep}{rendered_path}" - return None - - -@extras_features( - "custom_fields", - "custom_links", - "custom_validators", - "export_templates", - "graphql", - "relationships", - "webhooks", -) -class ConfigRemove(PrimaryModel): # pylint: disable=too-many-ancestors - """ConfigRemove for Regex Line Removals from Backup Configuration Model definition.""" - - name = models.CharField(max_length=255) - platform = models.ForeignKey( - to="dcim.Platform", - on_delete=models.CASCADE, - related_name="backup_line_remove", - ) - description = models.CharField( - max_length=200, - blank=True, - ) - regex = models.CharField( - max_length=200, - verbose_name="Regex Pattern", - help_text="Regex pattern used to remove a line from the backup configuration.", - ) - - clone_fields = ["platform", "description", "regex"] - - class Meta: - """Meta information for ConfigRemove model.""" - - ordering = ("platform", "name") - unique_together = ("name", "platform") - - def __str__(self): - """Return a simple string if model is called.""" + """Stringify instance.""" return self.name - - -@extras_features( - "custom_fields", - "custom_links", - "custom_validators", - "export_templates", - "graphql", - "relationships", - "webhooks", -) -class ConfigReplace(PrimaryModel): # pylint: disable=too-many-ancestors - """ConfigReplace for Regex Line Replacements from Backup Configuration Model definition.""" - - name = models.CharField(max_length=255) - platform = models.ForeignKey( - to="dcim.Platform", - on_delete=models.CASCADE, - related_name="backup_line_replace", - ) - description = models.CharField( - max_length=200, - blank=True, - ) - regex = models.CharField( - max_length=200, - verbose_name="Regex Pattern to Substitute", - help_text="Regex pattern that will be found and replaced with 'replaced text'.", - ) - replace = models.CharField( - max_length=200, - verbose_name="Replaced Text", - help_text="Text that will be inserted in place of Regex pattern match.", - ) - - clone_fields = ["platform", "description", "regex", "replace"] - - class Meta: - """Meta information for ConfigReplace model.""" - - ordering = ("platform", "name") - unique_together = ("name", "platform") - - def __str__(self): - """Return a simple string if model is called.""" - return self.name - - -@extras_features( - "custom_fields", - "custom_links", - "custom_validators", - "export_templates", - "graphql", - "relationships", - "webhooks", -) -class RemediationSetting(PrimaryModel): # pylint: disable=too-many-ancestors - """RemediationSetting details.""" - - # Remediation points to the platform - platform = models.OneToOneField( - to="dcim.Platform", - on_delete=models.CASCADE, - related_name="remediation_settings", - ) - - remediation_type = models.CharField( - max_length=50, - default=RemediationTypeChoice.TYPE_HIERCONFIG, - choices=RemediationTypeChoice, - help_text="Whether the remediation setting is type HierConfig or custom.", - ) - - # takes options.json. - remediation_options = models.JSONField( - blank=True, - default=dict, - help_text="Remediation Configuration for the device", - ) - - csv_headers = [ - "platform", - "remediation_type", - ] - - class Meta: - """Meta information for RemediationSettings model.""" - - ordering = ("platform", "remediation_type") - - def to_csv(self): - """Indicates model fields to return as csv.""" - return ( - self.platform, - self.remediation_type, - ) - - def __str__(self): - """Return a sane string representation of the instance.""" - return str(self.platform) - - -@extras_features( - "custom_fields", - "custom_links", - "custom_validators", - "export_templates", - "graphql", - "relationships", - "webhooks", - "statuses", -) -class ConfigPlan(PrimaryModel): # pylint: disable=too-many-ancestors - """ConfigPlan for Golden Configuration Plan Model definition.""" - - plan_type = models.CharField(max_length=20, choices=ConfigPlanTypeChoice, verbose_name="Plan Type") - device = models.ForeignKey( - to="dcim.Device", - on_delete=models.CASCADE, - related_name="config_plan", - ) - config_set = models.TextField(help_text="Configuration set to be applied to device.") - feature = models.ManyToManyField( - to=ComplianceFeature, - related_name="config_plan", - blank=True, - ) - plan_result = models.ForeignKey( - to="extras.JobResult", - on_delete=models.CASCADE, - related_name="config_plan", - verbose_name="Plan Result", - ) - deploy_result = models.ForeignKey( - to="extras.JobResult", - on_delete=models.PROTECT, - related_name="config_plan_deploy_result", - verbose_name="Deploy Result", - blank=True, - null=True, - ) - change_control_id = models.CharField( - max_length=50, - blank=True, - verbose_name="Change Control ID", - help_text="Change Control ID for this configuration plan.", - ) - change_control_url = models.URLField(blank=True, verbose_name="Change Control URL") - status = StatusField(blank=True, null=True, on_delete=models.PROTECT) - - class Meta: - """Meta information for ConfigPlan model.""" - - ordering = ("-created", "device") - unique_together = ( - "plan_type", - "device", - "created", - ) - - def __str__(self): - """Return a simple string if model is called.""" - return f"{self.device.name}-{self.plan_type}-{self.created}" diff --git a/nautobot_golden_config/navigation.py b/nautobot_golden_config/navigation.py index 0b3aa46d2..e28f24c43 100644 --- a/nautobot_golden_config/navigation.py +++ b/nautobot_golden_config/navigation.py @@ -1,162 +1,24 @@ -"""Add the configuration compliance buttons to the Apps Navigation.""" +"""Menu items.""" from nautobot.apps.ui import NavMenuAddButton, NavMenuGroup, NavMenuItem, NavMenuTab -from nautobot_golden_config.utilities.constant import ENABLE_BACKUP, ENABLE_COMPLIANCE, ENABLE_PLAN - -items_operate = [ +items = ( NavMenuItem( - link="plugins:nautobot_golden_config:goldenconfig_list", - name="Config Overview", - permissions=["nautobot_golden_config.view_goldenconfig"], - ) -] - -items_setup = [] - -if ENABLE_COMPLIANCE: - items_operate.append( - NavMenuItem( - link="plugins:nautobot_golden_config:configcompliance_list", - name="Config Compliance", - permissions=["nautobot_golden_config.view_configcompliance"], - ) - ) - -if ENABLE_COMPLIANCE: - items_setup.append( - NavMenuItem( - link="plugins:nautobot_golden_config:compliancerule_list", - name="Compliance Rules", - permissions=["nautobot_golden_config.view_compliancerule"], - buttons=( - NavMenuAddButton( - link="plugins:nautobot_golden_config:compliancerule_add", - permissions=["nautobot_golden_config.add_compliancerule"], - ), - ), - ) - ) - -if ENABLE_COMPLIANCE: - items_setup.append( - NavMenuItem( - link="plugins:nautobot_golden_config:compliancefeature_list", - name="Compliance Features", - permissions=["nautobot_golden_config.view_compliancefeature"], - buttons=( - NavMenuAddButton( - link="plugins:nautobot_golden_config:compliancefeature_add", - permissions=["nautobot_golden_config.add_compliancefeature"], - ), - ), - ) - ) - - -if ENABLE_COMPLIANCE: - items_operate.append( - NavMenuItem( - link="plugins:nautobot_golden_config:configcompliance_overview", - name="Compliance Report", - permissions=["nautobot_golden_config.view_configcompliance"], - ) - ) - -if ENABLE_PLAN: - items_operate.append( - NavMenuItem( - link="plugins:nautobot_golden_config:configplan_list", - name="Config Plans", - permissions=["nautobot_golden_config.view_configplan"], - buttons=( - NavMenuAddButton( - link="plugins:nautobot_golden_config:configplan_add", - permissions=["nautobot_golden_config.add_configplan"], - ), - ), - ) - ) - -if ENABLE_BACKUP: - items_setup.append( - NavMenuItem( - link="plugins:nautobot_golden_config:configremove_list", - name="Config Removals", - permissions=["nautobot_golden_config.view_configremove"], - buttons=( - NavMenuAddButton( - link="plugins:nautobot_golden_config:configremove_add", - permissions=["nautobot_golden_config.add_configremove"], - ), - ), - ) - ) - -if ENABLE_BACKUP: - items_setup.append( - NavMenuItem( - link="plugins:nautobot_golden_config:configreplace_list", - name="Config Replacements", - permissions=["nautobot_golden_config.view_configreplace"], - buttons=( - NavMenuAddButton( - link="plugins:nautobot_golden_config:configreplace_add", - permissions=["nautobot_golden_config.add_configreplace"], - ), - ), - ) - ) - - -if ENABLE_COMPLIANCE: - items_setup.append( - NavMenuItem( - link="plugins:nautobot_golden_config:remediationsetting_list", - name="Remediation Settings", - permissions=["nautobot_golden_config.view_remediationsetting"], - buttons=( - NavMenuAddButton( - link="plugins:nautobot_golden_config:remediationsetting_add", - permissions=["nautobot_golden_config.add_remediationsetting"], - ), - ), - ) - ) - -items_setup.append( - NavMenuItem( - link="plugins:nautobot_golden_config:goldenconfigsetting_list", - name="Golden Config Settings", - permissions=["nautobot_golden_config.view_goldenconfigsetting"], + link="plugins:nautobot_golden_config:compliancefeature_list", + name="Golden Config", + permissions=["nautobot_golden_config.view_compliancefeature"], buttons=( NavMenuAddButton( - link="plugins:nautobot_golden_config:goldenconfigsetting_add", - permissions=["nautobot_golden_config.change_goldenconfigsetting"], + link="plugins:nautobot_golden_config:compliancefeature_add", + permissions=["nautobot_golden_config.add_compliancefeature"], ), ), ), ) - menu_items = ( NavMenuTab( - name="Golden Config", - weight=1000, - groups=( - NavMenuGroup(name="Manage", weight=100, items=tuple(items_operate)), - NavMenuGroup(name="Setup", weight=100, items=tuple(items_setup)), - NavMenuGroup( - name="Tools", - weight=300, - items=( - NavMenuItem( - link="plugins:nautobot_golden_config:generate_intended_config", - name="Generate Intended Config", - permissions=["dcim.view_device", "extras.view_gitrepository"], - ), - ), - ), - ), + name="Apps", + groups=(NavMenuGroup(name="Golden Config", items=tuple(items)),), ), ) diff --git a/nautobot_golden_config/tables.py b/nautobot_golden_config/tables.py index 57adbaaf2..e662cb33d 100644 --- a/nautobot_golden_config/tables.py +++ b/nautobot_golden_config/tables.py @@ -1,532 +1,38 @@ -"""Django Tables2 classes for golden_config app.""" +"""Tables for nautobot_golden_config.""" -import copy - -from django.utils.html import format_html -from django_tables2 import Column, LinkColumn, TemplateColumn -from django_tables2.utils import A -from nautobot.apps.tables import BaseTable, BooleanColumn, TagColumn, ToggleColumn -from nautobot.extras.tables import StatusTableMixin +import django_tables2 as tables +from nautobot.apps.tables import BaseTable, ButtonsColumn, ToggleColumn from nautobot_golden_config import models -from nautobot_golden_config.utilities.constant import CONFIG_FEATURES, ENABLE_BACKUP, ENABLE_COMPLIANCE, ENABLE_INTENDED - -ALL_ACTIONS = """ -{% if backup == True %} - {% if record.config_type == 'json' %} - - {% else %} - {% if record.backup_config %} - - - - {% else %} - - {% endif %} - {% endif %} -{% endif %} -{% if intended == True %} - {% if record.config_type == 'json' %} - - {% else %} - {% if record.intended_config %} - - - - {% else %} - - {% endif %} - {% endif %} -{% endif %} -{% if postprocessing == True %} - {% if record.intended_config %} - - - - {% else %} - - {% endif %} -{% endif %} -{% if compliance == True %} - {% if record.intended_config and record.backup_config %} - - - - {% else %} - - {% endif %} -{% endif %} -{% if sotagg == True %} - - - - {% if record.config_type == 'json' %} - - {% else %} - - - - - - {% endif %} -{% endif %} -""" - -CONFIG_SET_BUTTON = """ - - - - -""" - -MATCH_CONFIG = """{{ record.match_config|linebreaksbr }}""" - - -def actual_fields(): - """Convienance function to conditionally toggle columns.""" - active_fields = ["pk", "name"] - if ENABLE_BACKUP: - active_fields.append("backup_last_success_date") - if ENABLE_INTENDED: - active_fields.append("intended_last_success_date") - if ENABLE_COMPLIANCE: - active_fields.append("compliance_last_success_date") - active_fields.append("actions") - return tuple(active_fields) - - -# -# Columns -# - - -class PercentageColumn(Column): - """Column used to display percentage.""" - - def render(self, value): - """Render percentage value.""" - return f"{value} %" - - -class ComplianceColumn(Column): - """Column used to display config compliance status (True/False/None).""" - - def render(self, value): - """Render an entry in this column.""" - if value == 1: # pylint: disable=no-else-return - return format_html('') - elif value == 0: - return format_html('') - else: # value is None - return format_html('') - - -# -# Tables -# - - -# ConfigCompliance -class ConfigComplianceTable(BaseTable): - """Table for rendering a listing of Device entries and their associated ConfigCompliance record status.""" - - pk = ToggleColumn(accessor=A("device")) - device = TemplateColumn( - template_code="""{{ record.device__name }} """ - ) - - def __init__(self, *args, **kwargs): - """Override default values to dynamically add columns.""" - # Used ConfigCompliance.objects on purpose, vs queryset (set in args[0]), as there were issues with that as - # well as not as expected from user standpoint (e.g. not always the same values on columns depending on - # filtering) - features = list( - models.ConfigCompliance.objects.order_by("rule__feature__slug") - .values_list("rule__feature__slug", flat=True) - .distinct() - ) - extra_columns = [(feature, ComplianceColumn(verbose_name=feature)) for feature in features] - kwargs["extra_columns"] = extra_columns - # Nautobot's BaseTable.configurable_columns() only recognizes columns in self.base_columns, - # so override the class's base_columns to include our additional columns as configurable. - self.base_columns = copy.deepcopy(self.base_columns) - for feature, column in extra_columns: - self.base_columns[feature] = column - super().__init__(*args, **kwargs) - - class Meta(BaseTable.Meta): - """Metaclass attributes of ConfigComplianceTable.""" - - model = models.ConfigCompliance - fields = ( - "pk", - "device", - ) - # All other fields (ConfigCompliance names) are constructed dynamically at instantiation time - see views.py - - -class ConfigComplianceGlobalFeatureTable(BaseTable): # pylint: disable=nb-sub-class-name - """Table for feature compliance report.""" - - name = Column(accessor="rule__feature__slug", verbose_name="Feature") - count = Column(accessor="count", verbose_name="Total") - compliant = Column(accessor="compliant", verbose_name="Compliant") - non_compliant = Column(accessor="non_compliant", verbose_name="Non-Compliant") - comp_percent = PercentageColumn(accessor="comp_percent", verbose_name="Compliance (%)") - - class Meta(BaseTable.Meta): - """Metaclass attributes of ConfigComplianceGlobalFeatureTable.""" - - model = models.ConfigCompliance - fields = ["name", "count", "compliant", "non_compliant", "comp_percent"] - default_columns = [ - "name", - "count", - "compliant", - "non_compliant", - "comp_percent", - ] - - -class ConfigComplianceDeleteTable(BaseTable): # pylint: disable=nb-sub-class-name - """Table for device compliance report.""" - - feature = Column(accessor="rule__feature__name", verbose_name="Feature") - - class Meta(BaseTable.Meta): - """Metaclass attributes of ConfigComplianceDeleteTable.""" - - device = Column(accessor="device__name", verbose_name="Device Name") - model = models.ConfigCompliance - fields = ("device", "feature") - - -class DeleteGoldenConfigTable(BaseTable): # pylint: disable=nb-sub-class-name - """ - Table used in bulk delete confirmation. - - This is required since there model is different when deleting the record compared to when viewing the records initially via Device. - """ - - pk = ToggleColumn() - - def __init__(self, *args, **kwargs): - """Remove all fields from showing except device .""" - super().__init__(*args, **kwargs) - for feature in list(self.base_columns.keys()): # pylint: disable=no-member - if feature not in ["pk", "device"]: - self.base_columns.pop(feature) # pylint: disable=no-member - self.sequence.remove(feature) - - class Meta(BaseTable.Meta): - """Meta for class DeleteGoldenConfigTable.""" - - model = models.GoldenConfig - - -# GoldenConfig - - -class GoldenConfigTable(BaseTable): - """Table to display Config Management Status.""" - - pk = ToggleColumn() - name = LinkColumn( - "plugins:nautobot_golden_config:goldenconfig", - args=[A("pk")], - text=lambda record: record.device.name, - verbose_name="Device", - ) - - if ENABLE_BACKUP: - backup_last_success_date = Column( - verbose_name="Backup Status", empty_values=(), order_by="backup_last_success_date" - ) - if ENABLE_INTENDED: - intended_last_success_date = Column( - verbose_name="Intended Status", - empty_values=(), - order_by="intended_last_success_date", - ) - if ENABLE_COMPLIANCE: - compliance_last_success_date = Column( - verbose_name="Compliance Status", - empty_values=(), - order_by="compliance_last_success_date", - ) - - actions = TemplateColumn( - template_code=ALL_ACTIONS, verbose_name="Actions", extra_context=CONFIG_FEATURES, orderable=False - ) - - def _render_last_success_date(self, record, column, value): - """Abstract method to get last success per row record.""" - last_success_date = getattr(record, f"{value}_last_success_date", None) - last_attempt_date = getattr(record, f"{value}_last_attempt_date", None) - if not last_success_date or not last_attempt_date: - column.attrs = {"td": {"style": "color:black"}} - return "--" - if not last_success_date and not last_attempt_date: - column.attrs = {"td": {"style": "color:black"}} - return "--" - if last_success_date and last_attempt_date == last_success_date: - column.attrs = {"td": {"style": "color:green"}} - return last_success_date - column.attrs = {"td": {"style": "color:red"}} - return last_success_date - - def render_backup_last_success_date(self, record, column): - """Pull back backup last success per row record.""" - return self._render_last_success_date(record, column, "backup") - - def render_intended_last_success_date(self, record, column): - """Pull back intended last success per row record.""" - return self._render_last_success_date(record, column, "intended") - - def render_compliance_last_success_date(self, record, column): - """Pull back compliance last success per row record.""" - return self._render_last_success_date(record, column, "compliance") - - class Meta(BaseTable.Meta): - """Meta for class GoldenConfigTable.""" - - model = models.GoldenConfig - fields = actual_fields() - - -# ComplianceFeature class ComplianceFeatureTable(BaseTable): - """Table to display Compliance Features.""" - - pk = ToggleColumn() - name = LinkColumn("plugins:nautobot_golden_config:compliancefeature", args=[A("pk")]) - - class Meta(BaseTable.Meta): - """Table to display Compliance Features Meta Data.""" - - model = models.ComplianceFeature - fields = ("pk", "name", "slug", "description") - default_columns = ("pk", "name", "slug", "description") - - -# ComplianceRule - - -class ComplianceRuleTable(BaseTable): - """Table to display Compliance Rules.""" - - pk = ToggleColumn() - feature = LinkColumn("plugins:nautobot_golden_config:compliancerule", args=[A("pk")]) - match_config = TemplateColumn(template_code=MATCH_CONFIG) - config_ordered = BooleanColumn() - custom_compliance = BooleanColumn() - config_remediation = BooleanColumn() - - class Meta(BaseTable.Meta): - """Table to display Compliance Rules Meta Data.""" - - model = models.ComplianceRule - fields = ( - "pk", - "feature", - "platform", - "description", - "config_ordered", - "match_config", - "config_type", - "custom_compliance", - "config_remediation", - ) - default_columns = ( - "pk", - "feature", - "platform", - "description", - "config_ordered", - "match_config", - "config_type", - "custom_compliance", - "config_remediation", - ) - - -# ConfigRemove - - -class ConfigRemoveTable(BaseTable): - """Table to display Compliance Rules.""" - - pk = ToggleColumn() - name = LinkColumn("plugins:nautobot_golden_config:configremove", args=[A("pk")]) - - class Meta(BaseTable.Meta): - """Table to display Compliance Rules Meta Data.""" - - model = models.ConfigRemove - fields = ("pk", "name", "platform", "description", "regex") - default_columns = ("pk", "name", "platform", "description", "regex") - - -# ConfigReplace - - -class ConfigReplaceTable(BaseTable): - """Table to display Compliance Rules.""" - - pk = ToggleColumn() - name = LinkColumn("plugins:nautobot_golden_config:configreplace", args=[A("pk")]) - - class Meta(BaseTable.Meta): - """Table to display Compliance Rules Meta Data.""" - - model = models.ConfigReplace - fields = ("pk", "name", "platform", "description", "regex", "replace") - default_columns = ("pk", "name", "platform", "description", "regex", "replace") - - -class GoldenConfigSettingTable(BaseTable): # pylint: disable=R0903 """Table for list view.""" pk = ToggleColumn() - name = Column(order_by=("_name",), linkify=True) - jinja_repository = Column( - verbose_name="Jinja Repository", - empty_values=(), - ) - intended_repository = Column( - verbose_name="Intended Repository", - empty_values=(), - ) - backup_repository = Column( - verbose_name="Backup Repository", - empty_values=(), + name = tables.Column(linkify=True) + actions = ButtonsColumn( + models.ComplianceFeature, + # Option for modifying the default action buttons on each row: + # buttons=("changelog", "edit", "delete"), + # Option for modifying the pk for the action buttons: + pk_field="pk", ) - def _render_capability(self, record, column, record_attribute): # pylint: disable=unused-argument - if getattr(record, record_attribute, None): - return format_html('') - return format_html('') - - def render_backup_repository(self, record, column): - """Render backup repository boolean value.""" - return self._render_capability(record=record, column=column, record_attribute="backup_repository") - - def render_intended_repository(self, record, column): - """Render intended repository boolean value.""" - return self._render_capability(record=record, column=column, record_attribute="intended_repository") - - def render_jinja_repository(self, record, column): - """Render jinja repository boolean value.""" - return self._render_capability(record=record, column=column, record_attribute="jinja_repository") - class Meta(BaseTable.Meta): """Meta attributes.""" - model = models.GoldenConfigSetting + model = models.ComplianceFeature fields = ( "pk", "name", - "weight", "description", - "backup_repository", - "intended_repository", - "jinja_repository", ) - -class RemediationSettingTable(BaseTable): - """Table to display RemediationSetting Rules.""" - - pk = ToggleColumn() - platform = LinkColumn("plugins:nautobot_golden_config:remediationsetting", args=[A("pk")]) - - class Meta(BaseTable.Meta): - """Table to display RemediationSetting Meta Data.""" - - model = models.RemediationSetting - fields = ("pk", "platform", "remediation_type") - default_columns = ("pk", "platform", "remediation_type") - - -# ConfigPlan - - -class ConfigPlanTable(StatusTableMixin, BaseTable): - """Table to display Config Plans.""" - - pk = ToggleColumn() - device = LinkColumn("plugins:nautobot_golden_config:configplan", args=[A("pk")]) - plan_result = TemplateColumn( - template_code="""""" - ) - deploy_result = TemplateColumn( - template_code=""" - {% if record.deploy_result %} - - {% else %} - — - {% endif %} - """ - ) - config_set = TemplateColumn(template_code=CONFIG_SET_BUTTON, verbose_name="Config Set", orderable=False) - tags = TagColumn(url_name="plugins:nautobot_golden_config:configplan_list") - - class Meta(BaseTable.Meta): - """Table to display Config Plans Meta Data.""" - - model = models.ConfigPlan - fields = ( - "pk", - "device", - "created", - "plan_type", - "feature", - "change_control_id", - "change_control_url", - "plan_result", - "deploy_result", - "config_set", - "status", - "tags", - ) - default_columns = ( - "pk", - "device", - "created", - "plan_type", - "feature", - "change_control_id", - "change_control_url", - "plan_result", - "deploy_result", - "config_set", - "status", - ) + # Option for modifying the columns that show up in the list view by default: + # default_columns = ( + # "pk", + # "name", + # "description", + # ) diff --git a/nautobot_golden_config/templates/nautobot_golden_config/compliancefeature_retrieve.html b/nautobot_golden_config/templates/nautobot_golden_config/compliancefeature_retrieve.html index 72efc8110..8832fd432 100644 --- a/nautobot_golden_config/templates/nautobot_golden_config/compliancefeature_retrieve.html +++ b/nautobot_golden_config/templates/nautobot_golden_config/compliancefeature_retrieve.html @@ -1,37 +1,26 @@ -{% extends 'generic/object_detail.html' %} -{% load helpers %} -{% load buttons %} -{% block buttons %} - {% if perms.nautobot_golden_config.add_compliancefeature %} - {% clone_button object %} - {% endif %} - {% if perms.nautobot_golden_config.change_compliancefeature %} - {% edit_button object key="pk" %} - {% endif %} - {% if perms.nautobot_golden_config.delete_compliancefeature %} - {% delete_button object key="pk" %} - {% endif %} -{% endblock buttons %} +{% extends 'generic/object_retrieve.html' %} +{% load helpers %} {% block content_left_page %}
- Compliance Feature Details + ComplianceFeature
- - - - - + - +
Name{{ object.name }}
Slug{{ object.slug }} + {{ object.name }} +
Description{{ object.description|placeholder }} + {{ object.description|placeholder }} +
-{% endblock %} \ No newline at end of file +{% endblock content_left_page %} + diff --git a/nautobot_golden_config/tests/fixtures.py b/nautobot_golden_config/tests/fixtures.py new file mode 100644 index 000000000..2e8f570b7 --- /dev/null +++ b/nautobot_golden_config/tests/fixtures.py @@ -0,0 +1,10 @@ +"""Create fixtures for tests.""" + +from nautobot_golden_config.models import ComplianceFeature + + +def create_compliancefeature(): + """Fixture to create necessary number of ComplianceFeature for tests.""" + ComplianceFeature.objects.create(name="Test One") + ComplianceFeature.objects.create(name="Test Two") + ComplianceFeature.objects.create(name="Test Three") diff --git a/nautobot_golden_config/tests/test_api_views.py b/nautobot_golden_config/tests/test_api_views.py new file mode 100644 index 000000000..d6aeed74a --- /dev/null +++ b/nautobot_golden_config/tests/test_api_views.py @@ -0,0 +1,27 @@ +"""Unit tests for nautobot_golden_config.""" + +from nautobot.apps.testing import APIViewTestCases + +from nautobot_golden_config import models +from nautobot_golden_config.tests import fixtures + + +class ComplianceFeatureAPIViewTest(APIViewTestCases.APIViewTestCase): + # pylint: disable=too-many-ancestors + """Test the API viewsets for ComplianceFeature.""" + + model = models.ComplianceFeature + create_data = [ + { + "name": "Test Model 1", + "description": "test description", + }, + { + "name": "Test Model 2", + }, + ] + bulk_update_data = {"description": "Test Bulk Update"} + + @classmethod + def setUpTestData(cls): + fixtures.create_compliancefeature() diff --git a/nautobot_golden_config/tests/test_filter_compliancefeature.py b/nautobot_golden_config/tests/test_filter_compliancefeature.py new file mode 100644 index 000000000..552778574 --- /dev/null +++ b/nautobot_golden_config/tests/test_filter_compliancefeature.py @@ -0,0 +1,28 @@ +"""Test ComplianceFeature Filter.""" + +from django.test import TestCase + +from nautobot_golden_config import filters, models +from nautobot_golden_config.tests import fixtures + + +class ComplianceFeatureFilterTestCase(TestCase): + """ComplianceFeature Filter Test Case.""" + + queryset = models.ComplianceFeature.objects.all() + filterset = filters.ComplianceFeatureFilterSet + + @classmethod + def setUpTestData(cls): + """Setup test data for ComplianceFeature Model.""" + fixtures.create_compliancefeature() + + def test_q_search_name(self): + """Test using Q search with name of ComplianceFeature.""" + params = {"q": "Test One"} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + def test_q_invalid(self): + """Test using invalid Q search for ComplianceFeature.""" + params = {"q": "test-five"} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0) diff --git a/nautobot_golden_config/tests/test_form_compliancefeature.py b/nautobot_golden_config/tests/test_form_compliancefeature.py new file mode 100644 index 000000000..cdeb6daa7 --- /dev/null +++ b/nautobot_golden_config/tests/test_form_compliancefeature.py @@ -0,0 +1,33 @@ +"""Test compliancefeature forms.""" + +from django.test import TestCase + +from nautobot_golden_config import forms + + +class ComplianceFeatureTest(TestCase): + """Test ComplianceFeature forms.""" + + def test_specifying_all_fields_success(self): + form = forms.ComplianceFeatureForm( + data={ + "name": "Development", + "description": "Development Testing", + } + ) + self.assertTrue(form.is_valid()) + self.assertTrue(form.save()) + + def test_specifying_only_required_success(self): + form = forms.ComplianceFeatureForm( + data={ + "name": "Development", + } + ) + self.assertTrue(form.is_valid()) + self.assertTrue(form.save()) + + def test_validate_name_compliancefeature_is_required(self): + form = forms.ComplianceFeatureForm(data={"description": "Development Testing"}) + self.assertFalse(form.is_valid()) + self.assertIn("This field is required.", form.errors["name"]) diff --git a/nautobot_golden_config/tests/test_model_compliancefeature.py b/nautobot_golden_config/tests/test_model_compliancefeature.py new file mode 100644 index 000000000..819b52647 --- /dev/null +++ b/nautobot_golden_config/tests/test_model_compliancefeature.py @@ -0,0 +1,22 @@ +"""Test ComplianceFeature.""" + +from django.test import TestCase + +from nautobot_golden_config import models + + +class TestComplianceFeature(TestCase): + """Test ComplianceFeature.""" + + def test_create_compliancefeature_only_required(self): + """Create with only required fields, and validate null description and __str__.""" + compliancefeature = models.ComplianceFeature.objects.create(name="Development") + self.assertEqual(compliancefeature.name, "Development") + self.assertEqual(compliancefeature.description, "") + self.assertEqual(str(compliancefeature), "Development") + + def test_create_compliancefeature_all_fields_success(self): + """Create ComplianceFeature with all fields.""" + compliancefeature = models.ComplianceFeature.objects.create(name="Development", description="Development Test") + self.assertEqual(compliancefeature.name, "Development") + self.assertEqual(compliancefeature.description, "Development Test") diff --git a/nautobot_golden_config/tests/test_views.py b/nautobot_golden_config/tests/test_views.py index 937826b7f..02d5e7aaa 100644 --- a/nautobot_golden_config/tests/test_views.py +++ b/nautobot_golden_config/tests/test_views.py @@ -1,395 +1,28 @@ -"""Unit tests for nautobot_golden_config views.""" +"""Unit tests for views.""" -import datetime -from unittest import mock, skip +from nautobot.apps.testing import ViewTestCases -import nautobot -from django.contrib.auth import get_user_model -from django.contrib.contenttypes.models import ContentType -from django.test import RequestFactory, override_settings -from django.urls import reverse -from lxml import html -from nautobot.apps.models import RestrictedQuerySet -from nautobot.apps.testing import TestCase, ViewTestCases -from nautobot.dcim.models import Device -from nautobot.extras.models import Relationship, RelationshipAssociation, Status +from nautobot_golden_config import models +from nautobot_golden_config.tests import fixtures -from nautobot_golden_config import models, views -from .conftest import create_device_data, create_feature_rule_json, create_job_result +class ComplianceFeatureViewTest(ViewTestCases.PrimaryObjectViewTestCase): + # pylint: disable=too-many-ancestors + """Test the ComplianceFeature views.""" -User = get_user_model() - - -@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"]) -class ConfigComplianceOverviewHelperTestCase(TestCase): - """Test ConfigComplianceOverviewHelper.""" - - @classmethod - def setUpTestData(cls): - """Set up base objects.""" - create_device_data() - dev01 = Device.objects.get(name="Device 1") - dev02 = Device.objects.get(name="Device 2") - dev03 = Device.objects.get(name="Device 3") - dev04 = Device.objects.get(name="Device 4") - - feature_dev01 = create_feature_rule_json(dev01) - feature_dev02 = create_feature_rule_json(dev02) - feature_dev03 = create_feature_rule_json(dev03) - - updates = [ - {"device": dev01, "feature": feature_dev01}, - {"device": dev02, "feature": feature_dev02}, - {"device": dev03, "feature": feature_dev03}, - {"device": dev04, "feature": feature_dev01}, - ] - for update in updates: - models.ConfigCompliance.objects.create( - device=update["device"], - rule=update["feature"], - actual={"foo": {"bar-1": "baz"}}, - intended={"foo": {"bar-2": "baz"}}, - ) - - # TODO: 2.0 turn this back on. - # cls.ccoh = views.ConfigComplianceOverviewOverviewHelper - - def test_plot_visual_no_devices(self): - # TODO: 2.0 turn this back on. - self.assertEqual(True, True) - # aggr = {"comp_percents": 0, "compliants": 0, "non_compliants": 0, "total": 0} - # self.assertEqual(self.ccoh.plot_visual(aggr), None) - - @mock.patch.dict("nautobot_golden_config.tables.CONFIG_FEATURES", {"sotagg": True}) - def test_config_compliance_list_view_with_sotagg_enabled(self): - models.GoldenConfig.objects.create(device=Device.objects.first()) - request = self.client.get("/plugins/golden-config/golden-config/") - self.assertContains(request, '') - - @mock.patch.dict("nautobot_golden_config.tables.CONFIG_FEATURES", {"sotagg": False}) - def test_config_compliance_list_view_with_sotagg_disabled(self): - models.GoldenConfig.objects.create(device=Device.objects.first()) - request = self.client.get("/plugins/golden-config/golden-config/") - self.assertNotContains(request, '') - - @mock.patch.object(views, "graph_ql_query") - @mock.patch.object(views, "get_device_to_settings_map") - @mock.patch("nautobot_golden_config.models.GoldenConfigSetting") - def test_config_compliance_details_sotagg_error( - self, mock_gc_setting, mock_get_device_to_settings_map, mock_graphql_query - ): - device = Device.objects.first() - mock_gc_setting.sot_agg_query = None - mock_get_device_to_settings_map.return_value = {device.id: mock_gc_setting} - request = self.client.get(f"/plugins/golden-config/golden-config/{device.pk}/sotagg/") - expected = "{\n "Error": "No saved `GraphQL Query` query was configured in the `Golden Config Setting`"\n}" - self.assertContains(request, expected) - mock_graphql_query.assert_not_called() - - @mock.patch.object(views, "graph_ql_query") - @mock.patch.object(views, "get_device_to_settings_map") - @mock.patch("nautobot_golden_config.models.GoldenConfigSetting") - def test_config_compliance_details_sotagg_no_error( - self, mock_gc_setting, mock_get_device_to_settings_map, mock_graph_ql_query - ): - device = Device.objects.first() - mock_get_device_to_settings_map.return_value = {device.id: mock_gc_setting} - mock_graph_ql_query.return_value = ("discard value", "This is a mock graphql result") - request = self.client.get(f"/plugins/golden-config/golden-config/{device.pk}/sotagg/") - expected = "This is a mock graphql result" - self.assertContains(request, expected) - mock_graph_ql_query.assert_called() - - -class ConfigReplaceUIViewSetTestCase(ViewTestCases.PrimaryObjectViewTestCase): # pylint: disable=too-many-ancestors - """Test ConfigReplaceUIViewSet.""" - - model = models.ConfigReplace - - bulk_edit_data = { - "description": "new description", + model = models.ComplianceFeature + bulk_edit_data = {"description": "Bulk edit views"} + form_data = { + "name": "Test 1", + "description": "Initial model", } + csv_data = ( + "name", + "Test csv1", + "Test csv2", + "Test csv3", + ) @classmethod def setUpTestData(cls): - """Set up base objects.""" - create_device_data() - platform = Device.objects.first().platform - for num in range(3): - models.ConfigReplace.objects.create( - name=f"test configreplace {num}", - platform=platform, - description="test description", - regex="^(.*)$", - replace="xyz", - ) - cls.form_data = { - "name": "new name", - "platform": platform.pk, - "description": "new description", - "regex": "^NEW (.*)$", - "replace": "NEW replaced text", - } - - # For compatibility with Nautobot lower than v2.2.0 - cls.csv_data = ( - "name,regex,replace,platform", - f"test configreplace 4,^(.*)$,xyz,{platform.pk}", - f"test configreplace 5,^(.*)$,xyz,{platform.pk}", - f"test configreplace 6,^(.*)$,xyz,{platform.pk}", - ) - - -class GoldenConfigListViewTestCase(TestCase): - """Test GoldenConfigListView.""" - - user_permissions = ["nautobot_golden_config.view_goldenconfig", "nautobot_golden_config.change_goldenconfig"] - - @classmethod - def setUpTestData(cls): - """Set up base objects.""" - create_device_data() - cls.gc_settings = models.GoldenConfigSetting.objects.first() - cls.gc_dynamic_group = cls.gc_settings.dynamic_group - cls.gc_dynamic_group.filter = {"name": [dev.name for dev in Device.objects.all()]} - cls.gc_dynamic_group.validated_save() - models.GoldenConfig.objects.create(device=Device.objects.first()) - - def _get_golden_config_table_header(self): - response = self.client.get(f"{self._url}") - html_parsed = html.fromstring(response.content.decode()) - golden_config_table = html_parsed.find_class("table")[0] - return golden_config_table.find("thead") - - @property - def _text_table_headers(self): - if nautobot.__version__ >= "2.3.0": - return ["Device", "Backup Status", "Intended Status", "Compliance Status", "Dynamic Groups", "Actions"] - return ["Device", "Backup Status", "Intended Status", "Compliance Status", "Actions"] - - @property - def _url(self): - return reverse("plugins:nautobot_golden_config:goldenconfig_list") - - def test_page_ok(self): - response = self.client.get(f"{self._url}") - self.assertEqual(response.status_code, 200) - - def test_headers_in_table(self): - table_header = self._get_golden_config_table_header() - headers = table_header.iterdescendants("th") - checkbox_header = next(headers) - checkbox_element = checkbox_header.find("input") - self.assertEqual(checkbox_element.type, "checkbox") - text_headers = [header.text_content() for header in headers] - self.assertEqual(text_headers, self._text_table_headers) - - def test_device_relationship_not_included_in_golden_config_table(self): - # Create a RelationshipAssociation to Device Model to setup test case - device_content_type = ContentType.objects.get_for_model(Device) - platform_content_type = ContentType.objects.get(app_label="dcim", model="platform") - device = Device.objects.first() - relationship = Relationship.objects.create( - label="test platform to dev", - type="one-to-many", - source_type_id=platform_content_type.id, - destination_type_id=device_content_type.id, - ) - RelationshipAssociation.objects.create( - source_type_id=platform_content_type.id, - source_id=device.platform.id, - destination_type_id=device_content_type.id, - destination_id=device.id, - relationship_id=relationship.id, - ) - table_header = self._get_golden_config_table_header() - # xpath expression excludes the pk checkbox column (i.e. the first column) - text_headers = [header.text_content() for header in table_header.xpath("tr/th[position()>1]")] - # This will fail if the Relationships to Device objects showed up in the Golden Config table - self.assertEqual(text_headers, self._text_table_headers) - - @skip("TODO: 2.0 Figure out how do csv tests.") - def test_csv_export(self): - # verify GoldenConfig table is empty - self.assertEqual(models.GoldenConfig.objects.count(), 0) - intended_datetime = datetime.datetime.now() - first_device = self.gc_dynamic_group.members.first() - models.GoldenConfig.objects.create( - device=first_device, - intended_last_attempt_date=intended_datetime, - intended_last_success_date=intended_datetime, - ) - response = self.client.get(f"{self._url}?format=csv") - self.assertEqual(response.status_code, 200) - self.assertEqual(response.headers["Content-Type"], "text/csv") - csv_data = response.content.decode().splitlines() - csv_headers = "Device Name,backup attempt,backup successful,intended attempt,intended successful,compliance attempt,compliance successful" - self.assertEqual(csv_headers, csv_data[0]) - intended_datetime_formated = intended_datetime.strftime("%Y-%m-%dT%H:%M:%S.%f+00:00") - # Test single entry in GoldenConfig table has data - expected_first_row = f"{first_device.name},,,{intended_datetime_formated},{intended_datetime_formated},," - self.assertEqual(expected_first_row, csv_data[1]) - # Test Devices in scope but without entries in GoldenConfig have empty entries - empty_csv_rows = [ - f"{device.name},,,,,," for device in self.gc_dynamic_group.members.exclude(pk=first_device.pk) - ] - self.assertEqual(empty_csv_rows, csv_data[2:]) - - @skip("TODO: 2.0 Figure out how do csv tests.") - def test_csv_export_with_filter(self): - devices_in_site_1 = Device.objects.filter(site__name="Site 1") - golden_config_devices = self.gc_dynamic_group.members.all() - # Test that there are Devices in GC that are not related to Site 1 - self.assertNotEqual(devices_in_site_1, golden_config_devices) - response = self.client.get(f"{self._url}?site={Device.objects.first().site.slug}&format=csv") - self.assertEqual(response.status_code, 200) - csv_data = response.content.decode().splitlines() - device_names_in_export = [entry.split(",")[0] for entry in csv_data[1:]] - device_names_in_site_1 = [device.name for device in devices_in_site_1] - self.assertEqual(device_names_in_export, device_names_in_site_1) - - -# pylint: disable=too-many-ancestors,too-many-locals -class ConfigPlanTestCase( - ViewTestCases.GetObjectViewTestCase, - ViewTestCases.GetObjectChangelogViewTestCase, - ViewTestCases.ListObjectsViewTestCase, - # Disabling Create tests because ConfigPlans are created via Job - # ViewTestCases.CreateObjectViewTestCase, - ViewTestCases.DeleteObjectViewTestCase, - ViewTestCases.EditObjectViewTestCase, -): - """Test ConfigPlan views.""" - - model = models.ConfigPlan - - @classmethod - def setUpTestData(cls): - create_device_data() - device1 = Device.objects.get(name="Device 1") - device2 = Device.objects.get(name="Device 2") - device3 = Device.objects.get(name="Device 3") - - rule1 = create_feature_rule_json(device1, feature="Test Feature 1") - rule2 = create_feature_rule_json(device2, feature="Test Feature 2") - rule3 = create_feature_rule_json(device3, feature="Test Feature 3") - rule4 = create_feature_rule_json(device3, feature="Test Feature 4") - - job_result1 = create_job_result() - job_result2 = create_job_result() - job_result3 = create_job_result() - - not_approved_status = Status.objects.get(name="Not Approved") - approved_status = Status.objects.get(name="Approved") - - plan1 = models.ConfigPlan.objects.create( - device=device1, - plan_type="intended", - config_set="Test Config Set 1", - change_control_id="Test Change Control ID 1", - change_control_url="https://1.example.com/", - status=not_approved_status, - plan_result_id=job_result1.id, - ) - plan1.feature.add(rule1.feature) - plan1.validated_save() - plan2 = models.ConfigPlan.objects.create( - device=device2, - plan_type="missing", - config_set="Test Config Set 2", - change_control_id="Test Change Control ID 2", - change_control_url="https://2.example.com/", - status=not_approved_status, - plan_result_id=job_result2.id, - ) - plan2.feature.add(rule2.feature) - plan2.validated_save() - plan3 = models.ConfigPlan.objects.create( - device=device3, - plan_type="remediation", - config_set="Test Config Set 3", - change_control_id="Test Change Control ID 3", - change_control_url="https://3.example.com/", - status=not_approved_status, - plan_result_id=job_result3.id, - ) - plan3.feature.set([rule3.feature, rule4.feature]) - plan3.validated_save() - - # Used for EditObjectViewTestCase - cls.form_data = { - "change_control_id": "Test Change Control ID 4", - "change_control_url": "https://4.example.com/", - "status": approved_status.pk, - } - - @skip("TODO: 2.0 Figure out how to have pass.") - def test_list_objects_with_constrained_permission(self): - pass - - -@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"]) -class ConfigComplianceUIViewSetTestCase( - ViewTestCases.BulkDeleteObjectsViewTestCase, - # ViewTestCases.ListObjectsViewTestCase, # generic list view tests won't work for this view since the queryset is pivoted -): - """Test ConfigComplianceUIViewSet views.""" - - model = models.ConfigCompliance - - @classmethod - def setUpTestData(cls): - create_device_data() - dev01 = Device.objects.get(name="Device 1") - dev02 = Device.objects.get(name="Device 2") - dev03 = Device.objects.get(name="Device 3") - dev04 = Device.objects.get(name="Device 4") - - for iterator_i in range(4): - feature_dev01 = create_feature_rule_json(dev01, feature=f"TestFeature{iterator_i}") - feature_dev02 = create_feature_rule_json(dev02, feature=f"TestFeature{iterator_i}") - feature_dev03 = create_feature_rule_json(dev03, feature=f"TestFeature{iterator_i}") - - updates = [ - {"device": dev01, "feature": feature_dev01}, - {"device": dev02, "feature": feature_dev02}, - {"device": dev03, "feature": feature_dev03}, - {"device": dev04, "feature": feature_dev01}, - ] - for iterator_j, update in enumerate(updates): - compliance_int = iterator_j % 2 - models.ConfigCompliance.objects.create( - device=update["device"], - rule=update["feature"], - actual={"foo": {"bar-1": "baz"}}, - intended={"foo": {f"bar-{compliance_int}": "baz"}}, - compliance=bool(compliance_int), - compliance_int=compliance_int, - ) - - def test_alter_queryset(self): - """Test alter_queryset method returns the expected pivoted queryset.""" - - unused_features = ( - models.ComplianceFeature.objects.create(slug="unused-feature-1", name="Unused Feature 1"), - models.ComplianceFeature.objects.create(slug="unused-feature-2", name="Unused Feature 2"), - ) - request = RequestFactory(SERVER_NAME="nautobot.example.com").get( - reverse("plugins:nautobot_golden_config:configcompliance_list") - ) - request.user = self.user - queryset = views.ConfigComplianceUIViewSet(request=request).alter_queryset(request) - features = ( - models.ComplianceFeature.objects.filter(feature__rule__isnull=False) - .values_list("slug", flat=True) - .distinct() - ) - self.assertNotIn(unused_features[0].slug, features) - self.assertNotIn(unused_features[1].slug, features) - self.assertGreater(len(features), 0) - self.assertIsInstance(queryset, RestrictedQuerySet) - for device in queryset: - self.assertSequenceEqual(list(device.keys()), ["device", "device__name", *features]) - for feature in features: - self.assertIn(device[feature], [0, 1]) + fixtures.create_compliancefeature() diff --git a/nautobot_golden_config/urls.py b/nautobot_golden_config/urls.py index c66dd37ea..22eccbd5c 100644 --- a/nautobot_golden_config/urls.py +++ b/nautobot_golden_config/urls.py @@ -1,28 +1,21 @@ -"""Django urlpatterns declaration for config compliance app.""" +"""Django urlpatterns declaration for nautobot_golden_config app.""" from django.templatetags.static import static from django.urls import path from django.views.generic import RedirectView -from nautobot.core.views.routers import NautobotUIViewSetRouter +from nautobot.apps.urls import NautobotUIViewSetRouter + from nautobot_golden_config import views -app_name = "nautobot_golden_config" router = NautobotUIViewSetRouter() -router.register("compliance-feature", views.ComplianceFeatureUIViewSet) -router.register("compliance-rule", views.ComplianceRuleUIViewSet) -router.register("golden-config-setting", views.GoldenConfigSettingUIViewSet) -router.register("config-remove", views.ConfigRemoveUIViewSet) -router.register("config-replace", views.ConfigReplaceUIViewSet) -router.register("remediation-setting", views.RemediationSettingUIViewSet) -router.register("config-plan", views.ConfigPlanUIViewSet) -router.register("config-compliance", views.ConfigComplianceUIViewSet) -router.register("golden-config", views.GoldenConfigUIViewSet) + +router.register("compliancefeature", views.ComplianceFeatureUIViewSet) + urlpatterns = [ - path("config-compliance/overview/", views.ConfigComplianceOverview.as_view(), name="configcompliance_overview"), - path("config-plan/bulk_deploy/", views.ConfigPlanBulkDeploy.as_view(), name="configplan_bulk-deploy"), - path("generate-intended-config/", views.GenerateIntendedConfigView.as_view(), name="generate_intended_config"), path("docs/", RedirectView.as_view(url=static("nautobot_golden_config/docs/index.html")), name="docs"), -] + router.urls +] + +urlpatterns += router.urls diff --git a/nautobot_golden_config/views.py b/nautobot_golden_config/views.py index e094ad663..26a9cd52f 100644 --- a/nautobot_golden_config/views.py +++ b/nautobot_golden_config/views.py @@ -1,606 +1,19 @@ -"""Django views for Nautobot Golden Configuration.""" # pylint: disable=too-many-lines +"""Views for nautobot_golden_config.""" -import json -import logging -from datetime import datetime - -import yaml -from django.contrib import messages -from django.contrib.auth.mixins import PermissionRequiredMixin -from django.core.exceptions import ObjectDoesNotExist -from django.db.models import Count, ExpressionWrapper, F, FloatField, Max, Q -from django.shortcuts import redirect, render -from django.urls import reverse -from django.utils.html import format_html -from django.utils.timezone import make_aware -from django.views.generic import TemplateView, View -from django_pivot.pivot import pivot -from nautobot.apps import views -from nautobot.core.views import generic -from nautobot.core.views.mixins import PERMISSIONS_ACTION_MAP, ObjectPermissionRequiredMixin -from nautobot.dcim.models import Device -from nautobot.extras.models import Job, JobResult -from rest_framework.decorators import action -from rest_framework.response import Response +from nautobot.apps.views import NautobotUIViewSet from nautobot_golden_config import filters, forms, models, tables from nautobot_golden_config.api import serializers -from nautobot_golden_config.utilities import constant -from nautobot_golden_config.utilities.config_postprocessing import get_config_postprocessing -from nautobot_golden_config.utilities.graphql import graph_ql_query -from nautobot_golden_config.utilities.helper import add_message, get_device_to_settings_map -from nautobot_golden_config.utilities.mat_plot import get_global_aggr, plot_barchart_visual, plot_visual - -# TODO: Future #4512 -PERMISSIONS_ACTION_MAP.update( - { - "backup": "view", - "compliance": "view", - "intended": "view", - "sotagg": "view", - "postprocessing": "view", - "devicetab": "view", - } -) -LOGGER = logging.getLogger(__name__) - -# -# GoldenConfig -# - - -class GoldenConfigUIViewSet( # pylint: disable=abstract-method - views.ObjectDetailViewMixin, - views.ObjectDestroyViewMixin, - views.ObjectBulkDestroyViewMixin, - views.ObjectListViewMixin, # TODO: Changing the order of the mixins breaks things... why? -): - """Views for the GoldenConfig model.""" - - bulk_update_form_class = forms.GoldenConfigBulkEditForm - table_class = tables.GoldenConfigTable - filterset_class = filters.GoldenConfigFilterSet - filterset_form_class = forms.GoldenConfigFilterForm - queryset = models.GoldenConfig.objects.all() - serializer_class = serializers.GoldenConfigSerializer - action_buttons = ("export",) - - def __init__(self, *args, **kwargs): - """Used to set default variables on GoldenConfigUIViewSet.""" - super().__init__(*args, **kwargs) - self.device = None - self.output = "" - self.structured_format = None - self.title_name = None - self.is_modal = None - self.config_details = None - self.action_template_name = None - - def filter_queryset(self, queryset): - """Add a warning message when GoldenConfig Table is out of sync.""" - queryset = super().filter_queryset(queryset) - # Only adding a message when no filters are applied - if self.filter_params: - return queryset - - sync_job = Job.objects.get( - module_name="nautobot_golden_config.jobs", job_class_name="SyncGoldenConfigWithDynamicGroups" - ) - sync_job_url = f"{sync_job.name}" - out_of_sync_message = format_html( - "The expected devices and actual devices here are not in sync. " - f"Running the job {sync_job_url} will put it back in sync." - ) - - gc_dynamic_group_device_pks = models.GoldenConfig.get_dynamic_group_device_pks() - gc_device_pks = models.GoldenConfig.get_golden_config_device_ids() - if gc_dynamic_group_device_pks != gc_device_pks: - messages.warning(self.request, message=out_of_sync_message) - - return queryset - - def get_extra_context(self, request, instance=None, **kwargs): - """Get extra context data.""" - context = super().get_extra_context(request, instance) - context["compliance"] = constant.ENABLE_COMPLIANCE - context["backup"] = constant.ENABLE_BACKUP - context["intended"] = constant.ENABLE_INTENDED - jobs = [] - jobs.append(["BackupJob", constant.ENABLE_BACKUP]) - jobs.append(["IntendedJob", constant.ENABLE_INTENDED]) - jobs.append(["ComplianceJob", constant.ENABLE_COMPLIANCE]) - add_message(jobs, request) - return context - - def _pre_helper(self, pk, request): - self.device = Device.objects.get(pk=pk) - self.config_details = models.GoldenConfig.objects.filter(device=self.device).first() - self.action_template_name = "nautobot_golden_config/goldenconfig_details.html" - self.structured_format = "json" - self.is_modal = False - if request.GET.get("modal") == "true": - self.action_template_name = "nautobot_golden_config/goldenconfig_detailsmodal.html" - self.is_modal = True - - def _post_render(self, request): - context = { - "output": self.output, - "device": self.device, - "device_name": self.device.name, - "format": self.structured_format, - "title_name": self.title_name, - "is_modal": self.is_modal, - } - return render(request, self.action_template_name, context) - - @action(detail=True, methods=["get"]) - def backup(self, request, pk, *args, **kwargs): - """Additional action to handle backup_config.""" - self._pre_helper(pk, request) - self.output = self.config_details.backup_config - self.structured_format = "cli" - self.title_name = "Backup Configuration Details" - return self._post_render(request) - - @action(detail=True, methods=["get"]) - def intended(self, request, pk, *args, **kwargs): - """Additional action to handle intended_config.""" - self._pre_helper(pk, request) - self.output = self.config_details.intended_config - self.structured_format = "cli" - self.title_name = "Intended Configuration Details" - return self._post_render(request) - - @action(detail=True, methods=["get"]) - def postprocessing(self, request, pk, *args, **kwargs): - """Additional action to handle postprocessing.""" - self._pre_helper(pk, request) - self.output = get_config_postprocessing(self.config_details, request) - self.structured_format = "cli" - self.title_name = "Post Processing" - return self._post_render(request) - - @action(detail=True, methods=["get"]) - def sotagg(self, request, pk, *args, **kwargs): - """Additional action to handle sotagg.""" - self._pre_helper(pk, request) - self.structured_format = "json" - if request.GET.get("format") in ["json", "yaml"]: - self.structured_format = request.GET.get("format") - - settings = get_device_to_settings_map(queryset=Device.objects.filter(pk=self.device.pk)) - if self.device.id in settings: - sot_agg_query_setting = settings[self.device.id].sot_agg_query - if sot_agg_query_setting is not None: - _, self.output = graph_ql_query(request, self.device, sot_agg_query_setting.query) - else: - self.output = {"Error": "No saved `GraphQL Query` query was configured in the `Golden Config Setting`"} - else: - raise ObjectDoesNotExist(f"{self.device.name} does not map to a Golden Config Setting.") - - if self.structured_format == "yaml": - self.output = yaml.dump(json.loads(json.dumps(self.output)), default_flow_style=False) - else: - self.output = json.dumps(self.output, indent=4) - self.title_name = "Aggregate Data" - return self._post_render(request) - - @action(detail=True, methods=["get"]) - def compliance(self, request, pk, *args, **kwargs): - """Additional action to handle compliance.""" - self._pre_helper(pk, request) - - self.output = self.config_details.compliance_config - if self.config_details.backup_last_success_date: - backup_date = str(self.config_details.backup_last_success_date.strftime("%b %d %Y")) - else: - backup_date = make_aware(datetime.now()).strftime("%b %d %Y") - if self.config_details.intended_last_success_date: - intended_date = str(self.config_details.intended_last_success_date.strftime("%b %d %Y")) - else: - intended_date = make_aware(datetime.now()).strftime("%b %d %Y") - - diff_type = "File" - self.structured_format = "diff" - - if self.output == "": - # This is used if all config snippets are in compliance and no diff exist. - self.output = f"--- Backup {diff_type} - " + backup_date + f"\n+++ Intended {diff_type} - " + intended_date - else: - first_occurence = self.output.index("@@") - second_occurence = self.output.index("@@", first_occurence) - # This is logic to match diff2html's expected input. - self.output = ( - f"--- Backup {diff_type} - " - + backup_date - + f"\n+++ Intended {diff_type} - " - + intended_date - + "\n" - + self.output[first_occurence:second_occurence] - + "@@" - + self.output[second_occurence + 2 :] # noqa: E203 - ) - self.title_name = "Compliance Details" - return self._post_render(request) - - -# -# ConfigCompliance -# - - -class ConfigComplianceUIViewSet( # pylint: disable=abstract-method - views.ObjectDetailViewMixin, - views.ObjectDestroyViewMixin, - views.ObjectBulkDestroyViewMixin, - views.ObjectListViewMixin, -): - """Views for the ConfigCompliance model.""" - - filterset_class = filters.ConfigComplianceFilterSet - filterset_form_class = forms.ConfigComplianceFilterForm - queryset = models.ConfigCompliance.objects.all().order_by("device__name") - serializer_class = serializers.ConfigComplianceSerializer - table_class = tables.ConfigComplianceTable - table_delete_class = tables.ConfigComplianceDeleteTable - - custom_action_permission_map = None - action_buttons = ("export",) - - def __init__(self, *args, **kwargs): - """Used to set default variables on ConfigComplianceUIViewSet.""" - super().__init__(*args, **kwargs) - self.pk_list = None - self.report_context = None - self.store_table = None # Used to store the table for bulk delete. No longer required in Nautobot 2.3.11 - - def get_extra_context(self, request, instance=None, **kwargs): - """A ConfigCompliance helper function to warn if the Job is not enabled to run.""" - context = super().get_extra_context(request, instance) - if self.action == "overview": - context = {**context, **self.report_context} - # TODO Remove when dropping support for Nautobot < 2.3.11 - if self.action == "bulk_destroy": - context["table"] = self.store_table - - context["compliance"] = constant.ENABLE_COMPLIANCE - context["backup"] = constant.ENABLE_BACKUP - context["intended"] = constant.ENABLE_INTENDED - add_message([["ComplianceJob", constant.ENABLE_COMPLIANCE]], request) - return context - - def alter_queryset(self, request): - """Build actual runtime queryset as the build time queryset of table `pivoted`.""" - return pivot( - self.queryset, - ["device", "device__name"], - "rule__feature__slug", - "compliance_int", - aggregation=Max, - ) - - def perform_bulk_destroy(self, request, **kwargs): - """Overwrite perform_bulk_destroy to handle special use case in which the UI shows devices but want to delete ConfigCompliance objects.""" - model = self.queryset.model - # Are we deleting *all* objects in the queryset or just a selected subset? - if request.POST.get("_all"): - filter_params = self.get_filter_params(request) - if not filter_params: - compliance_objects = model.objects.only("pk").all().values_list("pk", flat=True) - elif self.filterset_class is None: - raise NotImplementedError("filterset_class must be defined to use _all") - else: - compliance_objects = self.filterset_class(filter_params, model.objects.only("pk")).qs - # When selecting *all* the resulting request args are ConfigCompliance object PKs - self.pk_list = [item[0] for item in self.queryset.filter(pk__in=compliance_objects).values_list("id")] - elif "_confirm" not in request.POST: - # When it is not being confirmed, the pk's are the device objects. - device_objects = request.POST.getlist("pk") - self.pk_list = [item[0] for item in self.queryset.filter(device__pk__in=device_objects).values_list("id")] - else: - self.pk_list = request.POST.getlist("pk") - - form_class = self.get_form_class(**kwargs) - data = {} - if "_confirm" in request.POST: - form = form_class(request.POST) - if form.is_valid(): - return self.form_valid(form) - return self.form_invalid(form) - - table = self.table_delete_class(self.queryset.filter(pk__in=self.pk_list), orderable=False) - - if not table.rows: - messages.warning( - request, - f"No {self.queryset.model._meta.verbose_name_plural} were selected for deletion.", - ) - return redirect(self.get_return_url(request)) - - # TODO Remove when dropping support for Nautobot < 2.3.11 - self.store_table = table - - if not request.POST.get("_all"): - data.update({"table": table, "total_objs_to_delete": len(table.rows)}) - else: - data.update({"table": None, "delete_all": True, "total_objs_to_delete": len(table.rows)}) - return Response(data) - - @action(detail=True, methods=["get"]) - def devicetab(self, request, pk, *args, **kwargs): - """Additional action to handle backup_config.""" - device = Device.objects.get(pk=pk) - context = {} - compliance_details = models.ConfigCompliance.objects.filter(device=device) - context["compliance_details"] = compliance_details - if request.GET.get("compliance") == "compliant": - context["compliance_details"] = compliance_details.filter(compliance=True) - elif request.GET.get("compliance") == "non-compliant": - context["compliance_details"] = compliance_details.filter(compliance=False) - - context["active_tab"] = request.GET.get("tab") - context["device"] = device - context["object"] = device - context["verbose_name"] = "Device" - return render(request, "nautobot_golden_config/configcompliance_devicetab.html", context) - - -class ConfigComplianceOverview(generic.ObjectListView): - """View for executive report on configuration compliance.""" - - action_buttons = ("export",) - filterset = filters.ConfigComplianceFilterSet - filterset_form = forms.ConfigComplianceFilterForm - table = tables.ConfigComplianceGlobalFeatureTable - template_name = "nautobot_golden_config/configcompliance_overview.html" - # kind = "Features" - - queryset = ( - models.ConfigCompliance.objects.values("rule__feature__slug") - .annotate( - count=Count("rule__feature__slug"), - compliant=Count("rule__feature__slug", filter=Q(compliance=True)), - non_compliant=Count("rule__feature__slug", filter=~Q(compliance=True)), - comp_percent=ExpressionWrapper(100 * F("compliant") / F("count"), output_field=FloatField()), - ) - .order_by("-comp_percent") - ) - extra_content = {} - - # Once https://github.com/nautobot/nautobot/issues/4529 is addressed, can turn this on. - # Permalink reference: https://github.com/nautobot/nautobot-app-golden-config/blob/017d5e1526fa9f642b9e02bfc7161f27d4948bef/nautobot_golden_config/views.py#L383 - # @action(detail=False, methods=["get"]) - # def overview(self, request, *args, **kwargs): - def setup(self, request, *args, **kwargs): - """Using request object to perform filtering based on query params.""" - super().setup(request, *args, **kwargs) - filter_params = self.get_filter_params(request) - main_qs = models.ConfigCompliance.objects - device_aggr, feature_aggr = get_global_aggr(main_qs, self.filterset, filter_params) - feature_qs = self.filterset(request.GET, self.queryset).qs - self.extra_content = { - "bar_chart": plot_barchart_visual(feature_qs), - "device_aggr": device_aggr, - "device_visual": plot_visual(device_aggr), - "feature_aggr": feature_aggr, - "feature_visual": plot_visual(feature_aggr), - "compliance": constant.ENABLE_COMPLIANCE, - } - def extra_context(self): - """Extra content method on.""" - # add global aggregations to extra context. - return self.extra_content - -class ComplianceFeatureUIViewSet(views.NautobotUIViewSet): - """Views for the ComplianceFeature model.""" +class ComplianceFeatureUIViewSet(NautobotUIViewSet): + """ViewSet for ComplianceFeature views.""" bulk_update_form_class = forms.ComplianceFeatureBulkEditForm filterset_class = filters.ComplianceFeatureFilterSet filterset_form_class = forms.ComplianceFeatureFilterForm form_class = forms.ComplianceFeatureForm + lookup_field = "pk" queryset = models.ComplianceFeature.objects.all() serializer_class = serializers.ComplianceFeatureSerializer table_class = tables.ComplianceFeatureTable - lookup_field = "pk" - - def get_extra_context(self, request, instance=None): - """A ComplianceFeature helper function to warn if the Job is not enabled to run.""" - add_message([["ComplianceJob", constant.ENABLE_COMPLIANCE]], request) - return {} - - -class ComplianceRuleUIViewSet(views.NautobotUIViewSet): - """Views for the ComplianceRule model.""" - - bulk_update_form_class = forms.ComplianceRuleBulkEditForm - filterset_class = filters.ComplianceRuleFilterSet - filterset_form_class = forms.ComplianceRuleFilterForm - form_class = forms.ComplianceRuleForm - queryset = models.ComplianceRule.objects.all() - serializer_class = serializers.ComplianceRuleSerializer - table_class = tables.ComplianceRuleTable - lookup_field = "pk" - - def get_extra_context(self, request, instance=None): - """A ComplianceRule helper function to warn if the Job is not enabled to run.""" - add_message([["ComplianceJob", constant.ENABLE_COMPLIANCE]], request) - return {} - - -class GoldenConfigSettingUIViewSet(views.NautobotUIViewSet): - """Views for the GoldenConfigSetting model.""" - - bulk_update_form_class = forms.GoldenConfigSettingBulkEditForm - filterset_class = filters.GoldenConfigSettingFilterSet - filterset_form_class = forms.GoldenConfigSettingFilterForm - form_class = forms.GoldenConfigSettingForm - queryset = models.GoldenConfigSetting.objects.all() - serializer_class = serializers.GoldenConfigSettingSerializer - table_class = tables.GoldenConfigSettingTable - lookup_field = "pk" - - def get_extra_context(self, request, instance=None): - """A GoldenConfig helper function to warn if the Job is not enabled to run.""" - jobs = [] - jobs.append(["BackupJob", constant.ENABLE_BACKUP]) - jobs.append(["IntendedJob", constant.ENABLE_INTENDED]) - jobs.append(["DeployConfigPlans", constant.ENABLE_DEPLOY]) - jobs.append(["ComplianceJob", constant.ENABLE_COMPLIANCE]) - jobs.append( - [ - "AllGoldenConfig", - [ - constant.ENABLE_BACKUP, - constant.ENABLE_COMPLIANCE, - constant.ENABLE_DEPLOY, - constant.ENABLE_INTENDED, - constant.ENABLE_SOTAGG, - ], - ] - ) - jobs.append( - [ - "AllDevicesGoldenConfig", - [ - constant.ENABLE_BACKUP, - constant.ENABLE_COMPLIANCE, - constant.ENABLE_DEPLOY, - constant.ENABLE_INTENDED, - constant.ENABLE_SOTAGG, - ], - ] - ) - add_message(jobs, request) - return {} - - -class ConfigRemoveUIViewSet(views.NautobotUIViewSet): - """Views for the ConfigRemove model.""" - - bulk_update_form_class = forms.ConfigRemoveBulkEditForm - filterset_class = filters.ConfigRemoveFilterSet - filterset_form_class = forms.ConfigRemoveFilterForm - form_class = forms.ConfigRemoveForm - queryset = models.ConfigRemove.objects.all() - serializer_class = serializers.ConfigRemoveSerializer - table_class = tables.ConfigRemoveTable - lookup_field = "pk" - - def get_extra_context(self, request, instance=None): - """A ConfigRemove helper function to warn if the Job is not enabled to run.""" - add_message([["BackupJob", constant.ENABLE_BACKUP]], request) - return {} - - -class ConfigReplaceUIViewSet(views.NautobotUIViewSet): - """Views for the ConfigReplace model.""" - - bulk_update_form_class = forms.ConfigReplaceBulkEditForm - filterset_class = filters.ConfigReplaceFilterSet - filterset_form_class = forms.ConfigReplaceFilterForm - form_class = forms.ConfigReplaceForm - queryset = models.ConfigReplace.objects.all() - serializer_class = serializers.ConfigReplaceSerializer - table_class = tables.ConfigReplaceTable - lookup_field = "pk" - - def get_extra_context(self, request, instance=None): - """A ConfigReplace helper function to warn if the Job is not enabled to run.""" - add_message([["BackupJob", constant.ENABLE_BACKUP]], request) - return {} - - -class RemediationSettingUIViewSet(views.NautobotUIViewSet): - """Views for the RemediationSetting model.""" - - # bulk_create_form_class = forms.RemediationSettingCSVForm - bulk_update_form_class = forms.RemediationSettingBulkEditForm - filterset_class = filters.RemediationSettingFilterSet - filterset_form_class = forms.RemediationSettingFilterForm - form_class = forms.RemediationSettingForm - queryset = models.RemediationSetting.objects.all() - serializer_class = serializers.RemediationSettingSerializer - table_class = tables.RemediationSettingTable - lookup_field = "pk" - - def get_extra_context(self, request, instance=None): - """A RemediationSetting helper function to warn if the Job is not enabled to run.""" - add_message([["ComplianceJob", constant.ENABLE_COMPLIANCE]], request) - return {} - - -class ConfigPlanUIViewSet(views.NautobotUIViewSet): - """Views for the ConfigPlan model.""" - - bulk_update_form_class = forms.ConfigPlanBulkEditForm - filterset_class = filters.ConfigPlanFilterSet - filterset_form_class = forms.ConfigPlanFilterForm - form_class = forms.ConfigPlanForm - queryset = models.ConfigPlan.objects.all() - serializer_class = serializers.ConfigPlanSerializer - table_class = tables.ConfigPlanTable - lookup_field = "pk" - action_buttons = ("add",) - update_form_class = forms.ConfigPlanUpdateForm - - def alter_queryset(self, request): - """Build actual runtime queryset to automatically remove `Completed` by default.""" - if "Completed" not in request.GET.getlist("status"): - return self.queryset.exclude(status__name="Completed") - return self.queryset - - def get_extra_context(self, request, instance=None): - """A ConfigPlan helper function to warn if the Job is not enabled to run.""" - jobs = [] - jobs.append(["GenerateConfigPlans", constant.ENABLE_PLAN]) - jobs.append(["DeployConfigPlans", constant.ENABLE_DEPLOY]) - jobs.append(["DeployConfigPlanJobButtonReceiver", constant.ENABLE_DEPLOY]) - add_message(jobs, request) - return {} - - -class ConfigPlanBulkDeploy(ObjectPermissionRequiredMixin, View): - """View to run the Config Plan Deploy Job.""" - - queryset = models.ConfigPlan.objects.all() - - def get_required_permission(self): - """Permissions required for the view.""" - return "extras.run_job" - - # Once https://github.com/nautobot/nautobot/issues/4529 is addressed, can turn this on. - # Permalink reference: https://github.com/nautobot/nautobot-app-golden-config/blob/017d5e1526fa9f642b9e02bfc7161f27d4948bef/nautobot_golden_config/views.py#L609-L612 - # @action(detail=False, methods=["post"]) - # def bulk_deploy(self, request): - def post(self, request): - """Enqueue the job and redirect to the job results page.""" - config_plan_pks = request.POST.getlist("pk") - if not config_plan_pks: - messages.warning(request, "No Config Plans selected for deployment.") - return redirect("plugins:nautobot_golden_config:configplan_list") - - job_data = {"config_plan": config_plan_pks} - job = Job.objects.get(name="Generate Config Plans") - - job_result = JobResult.enqueue_job( - job, - request.user, - data=job_data, - **job.job_class.serialize_data(request), - ) - return redirect(job_result.get_absolute_url()) - - -class GenerateIntendedConfigView(PermissionRequiredMixin, TemplateView): - """View to generate the intended configuration.""" - - template_name = "nautobot_golden_config/generate_intended_config.html" - permission_required = ["dcim.view_device", "extras.view_gitrepository"] - - def get_context_data(self, **kwargs): - """Get the context data for the view.""" - context = super().get_context_data(**kwargs) - context["form"] = forms.GenerateIntendedConfigForm() - return context diff --git a/poetry.lock b/poetry.lock index 1d6ee6311..d7321b2f2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "amqp" @@ -25,28 +25,6 @@ files = [ {file = "aniso8601-7.0.0.tar.gz", hash = "sha256:513d2b6637b7853806ae79ffaca6f3e8754bdd547048f5ccc1420aec4b714f1e"}, ] -[[package]] -name = "anyio" -version = "4.5.2" -description = "High level compatibility layer for multiple asynchronous event loop implementations" -optional = false -python-versions = ">=3.8" -files = [ - {file = "anyio-4.5.2-py3-none-any.whl", hash = "sha256:c011ee36bc1e8ba40e5a81cb9df91925c218fe9b778554e0b56a21e1b5d4716f"}, - {file = "anyio-4.5.2.tar.gz", hash = "sha256:23009af4ed04ce05991845451e11ef02fc7c5ed29179ac9a420e5ad0ac7ddc5b"}, -] - -[package.dependencies] -exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} -idna = ">=2.8" -sniffio = ">=1.1" -typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} - -[package.extras] -doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"] -trio = ["trio (>=0.26.1)"] - [[package]] name = "appnope" version = "0.1.4" @@ -96,21 +74,18 @@ wrapt = [ [[package]] name = "asttokens" -version = "2.4.1" +version = "3.0.0" description = "Annotate AST trees with source code positions" optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"}, - {file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"}, + {file = "asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2"}, + {file = "asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7"}, ] -[package.dependencies] -six = ">=1.12.0" - [package.extras] -astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"] -test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] +astroid = ["astroid (>=2,<4)"] +test = ["astroid (>=2,<4)", "pytest", "pytest-cov", "pytest-xdist"] [[package]] name = "astunparse" @@ -140,19 +115,19 @@ files = [ [[package]] name = "attrs" -version = "24.2.0" +version = "24.3.0" description = "Classes Without Boilerplate" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, - {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, + {file = "attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308"}, + {file = "attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff"}, ] [package.extras] benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] @@ -231,44 +206,6 @@ tzdata = {version = "*", optional = true, markers = "extra == \"tzdata\""} [package.extras] tzdata = ["tzdata"] -[[package]] -name = "bcrypt" -version = "4.2.1" -description = "Modern password hashing for your software and your servers" -optional = false -python-versions = ">=3.7" -files = [ - {file = "bcrypt-4.2.1-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:1340411a0894b7d3ef562fb233e4b6ed58add185228650942bdc885362f32c17"}, - {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1ee315739bc8387aa36ff127afc99120ee452924e0df517a8f3e4c0187a0f5f"}, - {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dbd0747208912b1e4ce730c6725cb56c07ac734b3629b60d4398f082ea718ad"}, - {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:aaa2e285be097050dba798d537b6efd9b698aa88eef52ec98d23dcd6d7cf6fea"}, - {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:76d3e352b32f4eeb34703370e370997065d28a561e4a18afe4fef07249cb4396"}, - {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:b7703ede632dc945ed1172d6f24e9f30f27b1b1a067f32f68bf169c5f08d0425"}, - {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:89df2aea2c43be1e1fa066df5f86c8ce822ab70a30e4c210968669565c0f4685"}, - {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:04e56e3fe8308a88b77e0afd20bec516f74aecf391cdd6e374f15cbed32783d6"}, - {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:cfdf3d7530c790432046c40cda41dfee8c83e29482e6a604f8930b9930e94139"}, - {file = "bcrypt-4.2.1-cp37-abi3-win32.whl", hash = "sha256:adadd36274510a01f33e6dc08f5824b97c9580583bd4487c564fc4617b328005"}, - {file = "bcrypt-4.2.1-cp37-abi3-win_amd64.whl", hash = "sha256:8c458cd103e6c5d1d85cf600e546a639f234964d0228909d8f8dbeebff82d526"}, - {file = "bcrypt-4.2.1-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:8ad2f4528cbf0febe80e5a3a57d7a74e6635e41af1ea5675282a33d769fba413"}, - {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:909faa1027900f2252a9ca5dfebd25fc0ef1417943824783d1c8418dd7d6df4a"}, - {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cde78d385d5e93ece5479a0a87f73cd6fa26b171c786a884f955e165032b262c"}, - {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:533e7f3bcf2f07caee7ad98124fab7499cb3333ba2274f7a36cf1daee7409d99"}, - {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:687cf30e6681eeda39548a93ce9bfbb300e48b4d445a43db4298d2474d2a1e54"}, - {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:041fa0155c9004eb98a232d54da05c0b41d4b8e66b6fc3cb71b4b3f6144ba837"}, - {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f85b1ffa09240c89aa2e1ae9f3b1c687104f7b2b9d2098da4e923f1b7082d331"}, - {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c6f5fa3775966cca251848d4d5393ab016b3afed251163c1436fefdec3b02c84"}, - {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:807261df60a8b1ccd13e6599c779014a362ae4e795f5c59747f60208daddd96d"}, - {file = "bcrypt-4.2.1-cp39-abi3-win32.whl", hash = "sha256:b588af02b89d9fad33e5f98f7838bf590d6d692df7153647724a7f20c186f6bf"}, - {file = "bcrypt-4.2.1-cp39-abi3-win_amd64.whl", hash = "sha256:e84e0e6f8e40a242b11bce56c313edc2be121cec3e0ec2d76fce01f6af33c07c"}, - {file = "bcrypt-4.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:76132c176a6d9953cdc83c296aeaed65e1a708485fd55abf163e0d9f8f16ce0e"}, - {file = "bcrypt-4.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e158009a54c4c8bc91d5e0da80920d048f918c61a581f0a63e4e93bb556d362f"}, - {file = "bcrypt-4.2.1.tar.gz", hash = "sha256:6765386e3ab87f569b276988742039baab087b2cdb01e809d74e74503c2faafe"}, -] - -[package.extras] -tests = ["pytest (>=3.2.1,!=3.3.0)"] -typecheck = ["mypy"] - [[package]] name = "billiard" version = "4.2.1" @@ -338,13 +275,13 @@ zstd = ["zstandard (==0.22.0)"] [[package]] name = "certifi" -version = "2024.8.30" +version = "2024.12.14" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, - {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, + {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, + {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, ] [[package]] @@ -428,127 +365,114 @@ pycparser = "*" [[package]] name = "charset-normalizer" -version = "3.4.0" +version = "3.4.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false -python-versions = ">=3.7.0" +python-versions = ">=3.7" files = [ - {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, - {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, - {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, + {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, + {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, ] [[package]] name = "click" -version = "8.1.7" +version = "8.1.8" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, ] [package.dependencies] @@ -631,80 +555,6 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -[[package]] -name = "contourpy" -version = "1.1.1" -description = "Python library for calculating contours of 2D quadrilateral grids" -optional = false -python-versions = ">=3.8" -files = [ - {file = "contourpy-1.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:46e24f5412c948d81736509377e255f6040e94216bf1a9b5ea1eaa9d29f6ec1b"}, - {file = "contourpy-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e48694d6a9c5a26ee85b10130c77a011a4fedf50a7279fa0bdaf44bafb4299d"}, - {file = "contourpy-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a66045af6cf00e19d02191ab578a50cb93b2028c3eefed999793698e9ea768ae"}, - {file = "contourpy-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4ebf42695f75ee1a952f98ce9775c873e4971732a87334b099dde90b6af6a916"}, - {file = "contourpy-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6aec19457617ef468ff091669cca01fa7ea557b12b59a7908b9474bb9674cf0"}, - {file = "contourpy-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:462c59914dc6d81e0b11f37e560b8a7c2dbab6aca4f38be31519d442d6cde1a1"}, - {file = "contourpy-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6d0a8efc258659edc5299f9ef32d8d81de8b53b45d67bf4bfa3067f31366764d"}, - {file = "contourpy-1.1.1-cp310-cp310-win32.whl", hash = "sha256:d6ab42f223e58b7dac1bb0af32194a7b9311065583cc75ff59dcf301afd8a431"}, - {file = "contourpy-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:549174b0713d49871c6dee90a4b499d3f12f5e5f69641cd23c50a4542e2ca1eb"}, - {file = "contourpy-1.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:407d864db716a067cc696d61fa1ef6637fedf03606e8417fe2aeed20a061e6b2"}, - {file = "contourpy-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe80c017973e6a4c367e037cb31601044dd55e6bfacd57370674867d15a899b"}, - {file = "contourpy-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e30aaf2b8a2bac57eb7e1650df1b3a4130e8d0c66fc2f861039d507a11760e1b"}, - {file = "contourpy-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3de23ca4f381c3770dee6d10ead6fff524d540c0f662e763ad1530bde5112532"}, - {file = "contourpy-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:566f0e41df06dfef2431defcfaa155f0acfa1ca4acbf8fd80895b1e7e2ada40e"}, - {file = "contourpy-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b04c2f0adaf255bf756cf08ebef1be132d3c7a06fe6f9877d55640c5e60c72c5"}, - {file = "contourpy-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d0c188ae66b772d9d61d43c6030500344c13e3f73a00d1dc241da896f379bb62"}, - {file = "contourpy-1.1.1-cp311-cp311-win32.whl", hash = "sha256:0683e1ae20dc038075d92e0e0148f09ffcefab120e57f6b4c9c0f477ec171f33"}, - {file = "contourpy-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:8636cd2fc5da0fb102a2504fa2c4bea3cbc149533b345d72cdf0e7a924decc45"}, - {file = "contourpy-1.1.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:560f1d68a33e89c62da5da4077ba98137a5e4d3a271b29f2f195d0fba2adcb6a"}, - {file = "contourpy-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:24216552104ae8f3b34120ef84825400b16eb6133af2e27a190fdc13529f023e"}, - {file = "contourpy-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56de98a2fb23025882a18b60c7f0ea2d2d70bbbcfcf878f9067234b1c4818442"}, - {file = "contourpy-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:07d6f11dfaf80a84c97f1a5ba50d129d9303c5b4206f776e94037332e298dda8"}, - {file = "contourpy-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1eaac5257a8f8a047248d60e8f9315c6cff58f7803971170d952555ef6344a7"}, - {file = "contourpy-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19557fa407e70f20bfaba7d55b4d97b14f9480856c4fb65812e8a05fe1c6f9bf"}, - {file = "contourpy-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:081f3c0880712e40effc5f4c3b08feca6d064cb8cfbb372ca548105b86fd6c3d"}, - {file = "contourpy-1.1.1-cp312-cp312-win32.whl", hash = "sha256:059c3d2a94b930f4dafe8105bcdc1b21de99b30b51b5bce74c753686de858cb6"}, - {file = "contourpy-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f44d78b61740e4e8c71db1cf1fd56d9050a4747681c59ec1094750a658ceb970"}, - {file = "contourpy-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:70e5a10f8093d228bb2b552beeb318b8928b8a94763ef03b858ef3612b29395d"}, - {file = "contourpy-1.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8394e652925a18ef0091115e3cc191fef350ab6dc3cc417f06da66bf98071ae9"}, - {file = "contourpy-1.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5bd5680f844c3ff0008523a71949a3ff5e4953eb7701b28760805bc9bcff217"}, - {file = "contourpy-1.1.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66544f853bfa85c0d07a68f6c648b2ec81dafd30f272565c37ab47a33b220684"}, - {file = "contourpy-1.1.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0c02b75acfea5cab07585d25069207e478d12309557f90a61b5a3b4f77f46ce"}, - {file = "contourpy-1.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41339b24471c58dc1499e56783fedc1afa4bb018bcd035cfb0ee2ad2a7501ef8"}, - {file = "contourpy-1.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f29fb0b3f1217dfe9362ec55440d0743fe868497359f2cf93293f4b2701b8251"}, - {file = "contourpy-1.1.1-cp38-cp38-win32.whl", hash = "sha256:f9dc7f933975367251c1b34da882c4f0e0b2e24bb35dc906d2f598a40b72bfc7"}, - {file = "contourpy-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:498e53573e8b94b1caeb9e62d7c2d053c263ebb6aa259c81050766beb50ff8d9"}, - {file = "contourpy-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ba42e3810999a0ddd0439e6e5dbf6d034055cdc72b7c5c839f37a7c274cb4eba"}, - {file = "contourpy-1.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c06e4c6e234fcc65435223c7b2a90f286b7f1b2733058bdf1345d218cc59e34"}, - {file = "contourpy-1.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca6fab080484e419528e98624fb5c4282148b847e3602dc8dbe0cb0669469887"}, - {file = "contourpy-1.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93df44ab351119d14cd1e6b52a5063d3336f0754b72736cc63db59307dabb718"}, - {file = "contourpy-1.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eafbef886566dc1047d7b3d4b14db0d5b7deb99638d8e1be4e23a7c7ac59ff0f"}, - {file = "contourpy-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efe0fab26d598e1ec07d72cf03eaeeba8e42b4ecf6b9ccb5a356fde60ff08b85"}, - {file = "contourpy-1.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f08e469821a5e4751c97fcd34bcb586bc243c39c2e39321822060ba902eac49e"}, - {file = "contourpy-1.1.1-cp39-cp39-win32.whl", hash = "sha256:bfc8a5e9238232a45ebc5cb3bfee71f1167064c8d382cadd6076f0d51cff1da0"}, - {file = "contourpy-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:c84fdf3da00c2827d634de4fcf17e3e067490c4aea82833625c4c8e6cdea0887"}, - {file = "contourpy-1.1.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:229a25f68046c5cf8067d6d6351c8b99e40da11b04d8416bf8d2b1d75922521e"}, - {file = "contourpy-1.1.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a10dab5ea1bd4401c9483450b5b0ba5416be799bbd50fc7a6cc5e2a15e03e8a3"}, - {file = "contourpy-1.1.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4f9147051cb8fdb29a51dc2482d792b3b23e50f8f57e3720ca2e3d438b7adf23"}, - {file = "contourpy-1.1.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a75cc163a5f4531a256f2c523bd80db509a49fc23721b36dd1ef2f60ff41c3cb"}, - {file = "contourpy-1.1.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b53d5769aa1f2d4ea407c65f2d1d08002952fac1d9e9d307aa2e1023554a163"}, - {file = "contourpy-1.1.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:11b836b7dbfb74e049c302bbf74b4b8f6cb9d0b6ca1bf86cfa8ba144aedadd9c"}, - {file = "contourpy-1.1.1.tar.gz", hash = "sha256:96ba37c2e24b7212a77da85004c38e7c4d155d3e72a45eeaf22c1f03f607e8ab"}, -] - -[package.dependencies] -numpy = [ - {version = ">=1.16,<2.0", markers = "python_version <= \"3.11\""}, - {version = ">=1.26.0rc1,<2.0", markers = "python_version >= \"3.12\""}, -] - -[package.extras] -bokeh = ["bokeh", "selenium"] -docs = ["furo", "sphinx (>=7.2)", "sphinx-copybutton"] -mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.4.1)", "types-Pillow"] -test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] -test-no-images = ["pytest", "pytest-cov", "wurlitzer"] - [[package]] name = "coverage" version = "7.6.1" @@ -852,21 +702,6 @@ ssh = ["bcrypt (>=3.1.5)"] test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] test-randomorder = ["pytest-randomly"] -[[package]] -name = "cycler" -version = "0.12.1" -description = "Composable style cycles" -optional = false -python-versions = ">=3.8" -files = [ - {file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"}, - {file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"}, -] - -[package.extras] -docs = ["ipython", "matplotlib", "numpydoc", "sphinx"] -tests = ["pytest", "pytest-cov", "pytest-xdist"] - [[package]] name = "decorator" version = "5.1.1" @@ -878,24 +713,6 @@ files = [ {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, ] -[[package]] -name = "deepdiff" -version = "7.0.1" -description = "Deep Difference and Search of any Python object/data. Recreate objects by adding adding deltas to each other." -optional = false -python-versions = ">=3.8" -files = [ - {file = "deepdiff-7.0.1-py3-none-any.whl", hash = "sha256:447760081918216aa4fd4ca78a4b6a848b81307b2ea94c810255334b759e1dc3"}, - {file = "deepdiff-7.0.1.tar.gz", hash = "sha256:260c16f052d4badbf60351b4f77e8390bee03a0b516246f6839bc813fb429ddf"}, -] - -[package.dependencies] -ordered-set = ">=4.1.0,<4.2.0" - -[package.extras] -cli = ["click (==8.1.7)", "pyyaml (==6.0.1)"] -optimize = ["orjson"] - [[package]] name = "defusedxml" version = "0.7.1" @@ -924,13 +741,13 @@ profile = ["gprof2dot (>=2022.7.29)"] [[package]] name = "django" -version = "4.2.16" +version = "4.2.17" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.8" files = [ - {file = "Django-4.2.16-py3-none-any.whl", hash = "sha256:1ddc333a16fc139fd253035a1606bb24261951bbc3a6ca256717fa06cc41a898"}, - {file = "Django-4.2.16.tar.gz", hash = "sha256:6f1616c2786c408ce86ab7e10f792b8f15742f7b7b7460243929cb371e7f1dad"}, + {file = "Django-4.2.17-py3-none-any.whl", hash = "sha256:3a93350214ba25f178d4045c0786c61573e7dbfa3c509b3551374f1e11ba8de0"}, + {file = "Django-4.2.17.tar.gz", hash = "sha256:6b56d834cc94c8b21a8f4e775064896be3b4a4ca387f2612d4406a5927cd2fdc"}, ] [package.dependencies] @@ -1036,17 +853,17 @@ Django = "*" [[package]] name = "django-debug-toolbar" -version = "4.3.0" +version = "4.4.6" description = "A configurable set of panels that display various debug information about the current request/response." optional = false python-versions = ">=3.8" files = [ - {file = "django_debug_toolbar-4.3.0-py3-none-any.whl", hash = "sha256:e09b7dcb8417b743234dfc57c95a7c1d1d87a88844abd13b4c5387f807b31bf6"}, - {file = "django_debug_toolbar-4.3.0.tar.gz", hash = "sha256:0b0dddee5ea29b9cb678593bc0d7a6d76b21d7799cb68e091a2148341a80f3c4"}, + {file = "django_debug_toolbar-4.4.6-py3-none-any.whl", hash = "sha256:3beb671c9ec44ffb817fad2780667f172bd1c067dbcabad6268ce39a81335f45"}, + {file = "django_debug_toolbar-4.4.6.tar.gz", hash = "sha256:36e421cb908c2f0675e07f9f41e3d1d8618dc386392ec82d23bcfcd5d29c7044"}, ] [package.dependencies] -django = ">=3.2.4" +django = ">=4.2.9" sqlparse = ">=0.2" [[package]] @@ -1141,20 +958,6 @@ Django = ">=3.2" [package.extras] tests = ["tox"] -[[package]] -name = "django-pivot" -version = "1.9.0" -description = "Create pivot tables and histograms from ORM querysets" -optional = false -python-versions = "*" -files = [ - {file = "django-pivot-1.9.0.tar.gz", hash = "sha256:5e985d32d9ff2a6b89419dd0292c0fa2822d494ee479b5fd16cdb542abf66a88"}, - {file = "django_pivot-1.9.0-py3-none-any.whl", hash = "sha256:1c60e18e7d5f7e42856faee0961748082ddd05b01ae7c8a4baed64d2bbacd051"}, -] - -[package.dependencies] -django = ">=2.2.0" - [[package]] name = "django-prometheus" version = "2.3.1" @@ -1243,6 +1046,23 @@ Django = ">=3.2" [package.extras] tablib = ["tablib"] +[[package]] +name = "django-tables2" +version = "2.7.5" +description = "Table/data-grid framework for Django" +optional = false +python-versions = ">=3.9" +files = [ + {file = "django_tables2-2.7.5-py3-none-any.whl", hash = "sha256:d9338937797207ffb6f481be2125c5ec3a0bb1858d409c672cc25fc5d654cb22"}, + {file = "django_tables2-2.7.5.tar.gz", hash = "sha256:fb5dcaa09379cf3947598ec7e1bd5f26ed63aafdee3b23963446763bbeac37bf"}, +] + +[package.dependencies] +django = ">=4.2" + +[package.extras] +tablib = ["tablib"] + [[package]] name = "django-taggit" version = "5.0.1" @@ -1363,13 +1183,13 @@ sidecar = ["drf-spectacular-sidecar"] [[package]] name = "drf-spectacular-sidecar" -version = "2024.11.1" +version = "2024.12.1" description = "Serve self-contained distribution builds of Swagger UI and Redoc with Django" optional = false python-versions = ">=3.6" files = [ - {file = "drf_spectacular_sidecar-2024.11.1-py3-none-any.whl", hash = "sha256:e2efd49c5bd1a607fd5d120d9da58d78e587852db8220b8880282a849296ff83"}, - {file = "drf_spectacular_sidecar-2024.11.1.tar.gz", hash = "sha256:fcfccc72cbdbe41e93f8416fa0c712d14126b8d1629e65c09c07c8edea24aad0"}, + {file = "drf_spectacular_sidecar-2024.12.1-py3-none-any.whl", hash = "sha256:e30821d150d29294f3be2018aab31b55cd724158e9e690b51a215264751aa8c7"}, + {file = "drf_spectacular_sidecar-2024.12.1.tar.gz", hash = "sha256:6be31df38bcf95681224b6550faa9344ee6dd5360dcf2b44afcc3f7460385613"}, ] [package.dependencies] @@ -1392,20 +1212,6 @@ typing-extensions = ">=4.7.0" [package.extras] dev = ["coverage", "pytest (>=7.4.4)"] -[[package]] -name = "exceptiongroup" -version = "1.2.2" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -files = [ - {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, - {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, -] - -[package.extras] -test = ["pytest (>=6)"] - [[package]] name = "executing" version = "2.1.0" @@ -1420,90 +1226,6 @@ files = [ [package.extras] tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] -[[package]] -name = "fonttools" -version = "4.55.0" -description = "Tools to manipulate font files" -optional = false -python-versions = ">=3.8" -files = [ - {file = "fonttools-4.55.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:51c029d4c0608a21a3d3d169dfc3fb776fde38f00b35ca11fdab63ba10a16f61"}, - {file = "fonttools-4.55.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bca35b4e411362feab28e576ea10f11268b1aeed883b9f22ed05675b1e06ac69"}, - {file = "fonttools-4.55.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ce4ba6981e10f7e0ccff6348e9775ce25ffadbee70c9fd1a3737e3e9f5fa74f"}, - {file = "fonttools-4.55.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31d00f9852a6051dac23294a4cf2df80ced85d1d173a61ba90a3d8f5abc63c60"}, - {file = "fonttools-4.55.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e198e494ca6e11f254bac37a680473a311a88cd40e58f9cc4dc4911dfb686ec6"}, - {file = "fonttools-4.55.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7208856f61770895e79732e1dcbe49d77bd5783adf73ae35f87fcc267df9db81"}, - {file = "fonttools-4.55.0-cp310-cp310-win32.whl", hash = "sha256:e7e6a352ff9e46e8ef8a3b1fe2c4478f8a553e1b5a479f2e899f9dc5f2055880"}, - {file = "fonttools-4.55.0-cp310-cp310-win_amd64.whl", hash = "sha256:636caaeefe586d7c84b5ee0734c1a5ab2dae619dc21c5cf336f304ddb8f6001b"}, - {file = "fonttools-4.55.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fa34aa175c91477485c44ddfbb51827d470011e558dfd5c7309eb31bef19ec51"}, - {file = "fonttools-4.55.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:37dbb3fdc2ef7302d3199fb12468481cbebaee849e4b04bc55b77c24e3c49189"}, - {file = "fonttools-4.55.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5263d8e7ef3c0ae87fbce7f3ec2f546dc898d44a337e95695af2cd5ea21a967"}, - {file = "fonttools-4.55.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f307f6b5bf9e86891213b293e538d292cd1677e06d9faaa4bf9c086ad5f132f6"}, - {file = "fonttools-4.55.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f0a4b52238e7b54f998d6a56b46a2c56b59c74d4f8a6747fb9d4042190f37cd3"}, - {file = "fonttools-4.55.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3e569711464f777a5d4ef522e781dc33f8095ab5efd7548958b36079a9f2f88c"}, - {file = "fonttools-4.55.0-cp311-cp311-win32.whl", hash = "sha256:2b3ab90ec0f7b76c983950ac601b58949f47aca14c3f21eed858b38d7ec42b05"}, - {file = "fonttools-4.55.0-cp311-cp311-win_amd64.whl", hash = "sha256:aa046f6a63bb2ad521004b2769095d4c9480c02c1efa7d7796b37826508980b6"}, - {file = "fonttools-4.55.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:838d2d8870f84fc785528a692e724f2379d5abd3fc9dad4d32f91cf99b41e4a7"}, - {file = "fonttools-4.55.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f46b863d74bab7bb0d395f3b68d3f52a03444964e67ce5c43ce43a75efce9246"}, - {file = "fonttools-4.55.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33b52a9cfe4e658e21b1f669f7309b4067910321757fec53802ca8f6eae96a5a"}, - {file = "fonttools-4.55.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:732a9a63d6ea4a81b1b25a1f2e5e143761b40c2e1b79bb2b68e4893f45139a40"}, - {file = "fonttools-4.55.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7dd91ac3fcb4c491bb4763b820bcab6c41c784111c24172616f02f4bc227c17d"}, - {file = "fonttools-4.55.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1f0e115281a32ff532118aa851ef497a1b7cda617f4621c1cdf81ace3e36fb0c"}, - {file = "fonttools-4.55.0-cp312-cp312-win32.whl", hash = "sha256:6c99b5205844f48a05cb58d4a8110a44d3038c67ed1d79eb733c4953c628b0f6"}, - {file = "fonttools-4.55.0-cp312-cp312-win_amd64.whl", hash = "sha256:f8c8c76037d05652510ae45be1cd8fb5dd2fd9afec92a25374ac82255993d57c"}, - {file = "fonttools-4.55.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8118dc571921dc9e4b288d9cb423ceaf886d195a2e5329cc427df82bba872cd9"}, - {file = "fonttools-4.55.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01124f2ca6c29fad4132d930da69158d3f49b2350e4a779e1efbe0e82bd63f6c"}, - {file = "fonttools-4.55.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81ffd58d2691f11f7c8438796e9f21c374828805d33e83ff4b76e4635633674c"}, - {file = "fonttools-4.55.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5435e5f1eb893c35c2bc2b9cd3c9596b0fcb0a59e7a14121562986dd4c47b8dd"}, - {file = "fonttools-4.55.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d12081729280c39d001edd0f4f06d696014c26e6e9a0a55488fabc37c28945e4"}, - {file = "fonttools-4.55.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a7ad1f1b98ab6cb927ab924a38a8649f1ffd7525c75fe5b594f5dab17af70e18"}, - {file = "fonttools-4.55.0-cp313-cp313-win32.whl", hash = "sha256:abe62987c37630dca69a104266277216de1023cf570c1643bb3a19a9509e7a1b"}, - {file = "fonttools-4.55.0-cp313-cp313-win_amd64.whl", hash = "sha256:2863555ba90b573e4201feaf87a7e71ca3b97c05aa4d63548a4b69ea16c9e998"}, - {file = "fonttools-4.55.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:00f7cf55ad58a57ba421b6a40945b85ac7cc73094fb4949c41171d3619a3a47e"}, - {file = "fonttools-4.55.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f27526042efd6f67bfb0cc2f1610fa20364396f8b1fc5edb9f45bb815fb090b2"}, - {file = "fonttools-4.55.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e67974326af6a8879dc2a4ec63ab2910a1c1a9680ccd63e4a690950fceddbe"}, - {file = "fonttools-4.55.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61dc0a13451143c5e987dec5254d9d428f3c2789a549a7cf4f815b63b310c1cc"}, - {file = "fonttools-4.55.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:b2e526b325a903868c62155a6a7e24df53f6ce4c5c3160214d8fe1be2c41b478"}, - {file = "fonttools-4.55.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:b7ef9068a1297714e6fefe5932c33b058aa1d45a2b8be32a4c6dee602ae22b5c"}, - {file = "fonttools-4.55.0-cp38-cp38-win32.whl", hash = "sha256:55718e8071be35dff098976bc249fc243b58efa263768c611be17fe55975d40a"}, - {file = "fonttools-4.55.0-cp38-cp38-win_amd64.whl", hash = "sha256:553bd4f8cc327f310c20158e345e8174c8eed49937fb047a8bda51daf2c353c8"}, - {file = "fonttools-4.55.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f901cef813f7c318b77d1c5c14cf7403bae5cb977cede023e22ba4316f0a8f6"}, - {file = "fonttools-4.55.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8c9679fc0dd7e8a5351d321d8d29a498255e69387590a86b596a45659a39eb0d"}, - {file = "fonttools-4.55.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd2820a8b632f3307ebb0bf57948511c2208e34a4939cf978333bc0a3f11f838"}, - {file = "fonttools-4.55.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23bbbb49bec613a32ed1b43df0f2b172313cee690c2509f1af8fdedcf0a17438"}, - {file = "fonttools-4.55.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a656652e1f5d55b9728937a7e7d509b73d23109cddd4e89ee4f49bde03b736c6"}, - {file = "fonttools-4.55.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f50a1f455902208486fbca47ce33054208a4e437b38da49d6721ce2fef732fcf"}, - {file = "fonttools-4.55.0-cp39-cp39-win32.whl", hash = "sha256:161d1ac54c73d82a3cded44202d0218ab007fde8cf194a23d3dd83f7177a2f03"}, - {file = "fonttools-4.55.0-cp39-cp39-win_amd64.whl", hash = "sha256:ca7fd6987c68414fece41c96836e945e1f320cda56fc96ffdc16e54a44ec57a2"}, - {file = "fonttools-4.55.0-py3-none-any.whl", hash = "sha256:12db5888cd4dd3fcc9f0ee60c6edd3c7e1fd44b7dd0f31381ea03df68f8a153f"}, - {file = "fonttools-4.55.0.tar.gz", hash = "sha256:7636acc6ab733572d5e7eec922b254ead611f1cdad17be3f0be7418e8bfaca71"}, -] - -[package.extras] -all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "pycairo", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.1.0)", "xattr", "zopfli (>=0.1.4)"] -graphite = ["lz4 (>=1.7.4.2)"] -interpolatable = ["munkres", "pycairo", "scipy"] -lxml = ["lxml (>=4.0)"] -pathops = ["skia-pathops (>=0.5.0)"] -plot = ["matplotlib"] -repacker = ["uharfbuzz (>=0.23.0)"] -symfont = ["sympy"] -type1 = ["xattr"] -ufo = ["fs (>=2.2.0,<3)"] -unicode = ["unicodedata2 (>=15.1.0)"] -woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] - -[[package]] -name = "future" -version = "1.0.0" -description = "Clean single-source support for Python 3 and 2" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216"}, - {file = "future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05"}, -] - [[package]] name = "ghp-import" version = "2.1.0" @@ -1523,13 +1245,13 @@ dev = ["flake8", "markdown", "twine", "wheel"] [[package]] name = "gitdb" -version = "4.0.11" +version = "4.0.12" description = "Git Object Database" optional = false python-versions = ">=3.7" files = [ - {file = "gitdb-4.0.11-py3-none-any.whl", hash = "sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4"}, - {file = "gitdb-4.0.11.tar.gz", hash = "sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b"}, + {file = "gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf"}, + {file = "gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571"}, ] [package.dependencies] @@ -1537,20 +1259,20 @@ smmap = ">=3.0.1,<6" [[package]] name = "gitpython" -version = "3.1.43" +version = "3.1.44" description = "GitPython is a Python library used to interact with Git repositories" optional = false python-versions = ">=3.7" files = [ - {file = "GitPython-3.1.43-py3-none-any.whl", hash = "sha256:eec7ec56b92aad751f9912a73404bc02ba212a23adb2c7098ee668417051a1ff"}, - {file = "GitPython-3.1.43.tar.gz", hash = "sha256:35f314a9f878467f5453cc1fee295c3e18e52f1b99f10f6cf5b1682e968a9e7c"}, + {file = "GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110"}, + {file = "gitpython-3.1.44.tar.gz", hash = "sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269"}, ] [package.dependencies] gitdb = ">=4.0.1,<5" [package.extras] -doc = ["sphinx (==4.3.2)", "sphinx-autodoc-typehints", "sphinx-rtd-theme", "sphinxcontrib-applehelp (>=1.0.2,<=1.0.4)", "sphinxcontrib-devhelp (==1.0.2)", "sphinxcontrib-htmlhelp (>=2.0.0,<=2.0.1)", "sphinxcontrib-qthelp (==1.0.3)", "sphinxcontrib-serializinghtml (==1.1.5)"] +doc = ["sphinx (>=7.1.2,<7.2)", "sphinx-autodoc-typehints", "sphinx_rtd_theme"] test = ["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", "typing-extensions"] [[package]] @@ -1671,76 +1393,6 @@ files = [ astunparse = {version = ">=1.6", markers = "python_version < \"3.9\""} colorama = ">=0.4" -[[package]] -name = "h11" -version = "0.14.0" -description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -optional = false -python-versions = ">=3.7" -files = [ - {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, - {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, -] - -[[package]] -name = "hier-config" -version = "2.2.3" -description = "A network configuration comparison tool, used to build remediation configurations." -optional = false -python-versions = ">=3.8,<4.0" -files = [ - {file = "hier_config-2.2.3-py3-none-any.whl", hash = "sha256:9adb860278afcf3813a49b75886649c9a21f7cc0c89f9d720f47ce8edcf021ca"}, - {file = "hier_config-2.2.3.tar.gz", hash = "sha256:6b0fb526c229b0f930f15a67be742d36230bf75a3041bf1d9d9487bbf9b01277"}, -] - -[package.dependencies] -PyYAML = ">=5.4" - -[[package]] -name = "httpcore" -version = "1.0.7" -description = "A minimal low-level HTTP client." -optional = false -python-versions = ">=3.8" -files = [ - {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, - {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, -] - -[package.dependencies] -certifi = "*" -h11 = ">=0.13,<0.15" - -[package.extras] -asyncio = ["anyio (>=4.0,<5.0)"] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] -trio = ["trio (>=0.22.0,<1.0)"] - -[[package]] -name = "httpx" -version = "0.27.0" -description = "The next generation HTTP client." -optional = false -python-versions = ">=3.8" -files = [ - {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, - {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, -] - -[package.dependencies] -anyio = "*" -certifi = "*" -httpcore = "==1.*" -idna = "*" -sniffio = "*" - -[package.extras] -brotli = ["brotli", "brotlicffi"] -cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] - [[package]] name = "idna" version = "3.10" @@ -1757,22 +1409,26 @@ all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2 [[package]] name = "importlib-metadata" -version = "4.13.0" +version = "8.5.0" description = "Read metadata from Python packages" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "importlib_metadata-4.13.0-py3-none-any.whl", hash = "sha256:8a8a81bcf996e74fee46f0d16bd3eaa382a7eb20fd82445c3ad11f4090334116"}, - {file = "importlib_metadata-4.13.0.tar.gz", hash = "sha256:dd0173e8f150d6815e098fd354f6414b0f079af4644ddfe90c71e2fc6174346d"}, + {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, + {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, ] [package.dependencies] -zipp = ">=0.5" +zipp = ">=3.20" [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] perf = ["ipython"] -testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] +test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["pytest-mypy"] [[package]] name = "importlib-resources" @@ -1910,13 +1566,13 @@ testing = ["Django", "attrs", "colorama", "docopt", "pytest (<9.0.0)"] [[package]] name = "jinja2" -version = "3.1.4" +version = "3.1.5" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" files = [ - {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, - {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, + {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, + {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, ] [package.dependencies] @@ -1963,152 +1619,6 @@ files = [ importlib-resources = {version = ">=1.4.0", markers = "python_version < \"3.9\""} referencing = ">=0.31.0" -[[package]] -name = "junos-eznc" -version = "2.7.1" -description = "Junos 'EZ' automation for non-programmers" -optional = false -python-versions = ">=3.8" -files = [ - {file = "junos-eznc-2.7.1.tar.gz", hash = "sha256:371f0298bf03e0cb4c017c43f6f4122263584eda0d690d0112e93f13daae41ac"}, - {file = "junos_eznc-2.7.1-py3-none-any.whl", hash = "sha256:8a7918faa8f0570341cac64c1210c1cd3e3542162d1e7449c3364f8d805716b2"}, -] - -[package.dependencies] -jinja2 = ">=2.7.1" -lxml = ">=3.2.4" -ncclient = ">=0.6.15" -pyparsing = "*" -pyserial = "*" -PyYAML = ">=5.1" -scp = ">=0.7.0" -six = "*" -transitions = "*" -yamlordereddictloader = "*" - -[[package]] -name = "kiwisolver" -version = "1.4.7" -description = "A fast implementation of the Cassowary constraint solver" -optional = false -python-versions = ">=3.8" -files = [ - {file = "kiwisolver-1.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8a9c83f75223d5e48b0bc9cb1bf2776cf01563e00ade8775ffe13b0b6e1af3a6"}, - {file = "kiwisolver-1.4.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:58370b1ffbd35407444d57057b57da5d6549d2d854fa30249771775c63b5fe17"}, - {file = "kiwisolver-1.4.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aa0abdf853e09aff551db11fce173e2177d00786c688203f52c87ad7fcd91ef9"}, - {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8d53103597a252fb3ab8b5845af04c7a26d5e7ea8122303dd7a021176a87e8b9"}, - {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:88f17c5ffa8e9462fb79f62746428dd57b46eb931698e42e990ad63103f35e6c"}, - {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a9ca9c710d598fd75ee5de59d5bda2684d9db36a9f50b6125eaea3969c2599"}, - {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f4d742cb7af1c28303a51b7a27aaee540e71bb8e24f68c736f6f2ffc82f2bf05"}, - {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e28c7fea2196bf4c2f8d46a0415c77a1c480cc0724722f23d7410ffe9842c407"}, - {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e968b84db54f9d42046cf154e02911e39c0435c9801681e3fc9ce8a3c4130278"}, - {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0c18ec74c0472de033e1bebb2911c3c310eef5649133dd0bedf2a169a1b269e5"}, - {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8f0ea6da6d393d8b2e187e6a5e3fb81f5862010a40c3945e2c6d12ae45cfb2ad"}, - {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:f106407dda69ae456dd1227966bf445b157ccc80ba0dff3802bb63f30b74e895"}, - {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:84ec80df401cfee1457063732d90022f93951944b5b58975d34ab56bb150dfb3"}, - {file = "kiwisolver-1.4.7-cp310-cp310-win32.whl", hash = "sha256:71bb308552200fb2c195e35ef05de12f0c878c07fc91c270eb3d6e41698c3bcc"}, - {file = "kiwisolver-1.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:44756f9fd339de0fb6ee4f8c1696cfd19b2422e0d70b4cefc1cc7f1f64045a8c"}, - {file = "kiwisolver-1.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:78a42513018c41c2ffd262eb676442315cbfe3c44eed82385c2ed043bc63210a"}, - {file = "kiwisolver-1.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d2b0e12a42fb4e72d509fc994713d099cbb15ebf1103545e8a45f14da2dfca54"}, - {file = "kiwisolver-1.4.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2a8781ac3edc42ea4b90bc23e7d37b665d89423818e26eb6df90698aa2287c95"}, - {file = "kiwisolver-1.4.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46707a10836894b559e04b0fd143e343945c97fd170d69a2d26d640b4e297935"}, - {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef97b8df011141c9b0f6caf23b29379f87dd13183c978a30a3c546d2c47314cb"}, - {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab58c12a2cd0fc769089e6d38466c46d7f76aced0a1f54c77652446733d2d02"}, - {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:803b8e1459341c1bb56d1c5c010406d5edec8a0713a0945851290a7930679b51"}, - {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9a9e8a507420fe35992ee9ecb302dab68550dedc0da9e2880dd88071c5fb052"}, - {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18077b53dc3bb490e330669a99920c5e6a496889ae8c63b58fbc57c3d7f33a18"}, - {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6af936f79086a89b3680a280c47ea90b4df7047b5bdf3aa5c524bbedddb9e545"}, - {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3abc5b19d24af4b77d1598a585b8a719beb8569a71568b66f4ebe1fb0449460b"}, - {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:933d4de052939d90afbe6e9d5273ae05fb836cc86c15b686edd4b3560cc0ee36"}, - {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:65e720d2ab2b53f1f72fb5da5fb477455905ce2c88aaa671ff0a447c2c80e8e3"}, - {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3bf1ed55088f214ba6427484c59553123fdd9b218a42bbc8c6496d6754b1e523"}, - {file = "kiwisolver-1.4.7-cp311-cp311-win32.whl", hash = "sha256:4c00336b9dd5ad96d0a558fd18a8b6f711b7449acce4c157e7343ba92dd0cf3d"}, - {file = "kiwisolver-1.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:929e294c1ac1e9f615c62a4e4313ca1823ba37326c164ec720a803287c4c499b"}, - {file = "kiwisolver-1.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:e33e8fbd440c917106b237ef1a2f1449dfbb9b6f6e1ce17c94cd6a1e0d438376"}, - {file = "kiwisolver-1.4.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:5360cc32706dab3931f738d3079652d20982511f7c0ac5711483e6eab08efff2"}, - {file = "kiwisolver-1.4.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942216596dc64ddb25adb215c3c783215b23626f8d84e8eff8d6d45c3f29f75a"}, - {file = "kiwisolver-1.4.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:48b571ecd8bae15702e4f22d3ff6a0f13e54d3d00cd25216d5e7f658242065ee"}, - {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad42ba922c67c5f219097b28fae965e10045ddf145d2928bfac2eb2e17673640"}, - {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:612a10bdae23404a72941a0fc8fa2660c6ea1217c4ce0dbcab8a8f6543ea9e7f"}, - {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e838bba3a3bac0fe06d849d29772eb1afb9745a59710762e4ba3f4cb8424483"}, - {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22f499f6157236c19f4bbbd472fa55b063db77a16cd74d49afe28992dff8c258"}, - {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693902d433cf585133699972b6d7c42a8b9f8f826ebcaf0132ff55200afc599e"}, - {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4e77f2126c3e0b0d055f44513ed349038ac180371ed9b52fe96a32aa071a5107"}, - {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:657a05857bda581c3656bfc3b20e353c232e9193eb167766ad2dc58b56504948"}, - {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4bfa75a048c056a411f9705856abfc872558e33c055d80af6a380e3658766038"}, - {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:34ea1de54beef1c104422d210c47c7d2a4999bdecf42c7b5718fbe59a4cac383"}, - {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:90da3b5f694b85231cf93586dad5e90e2d71b9428f9aad96952c99055582f520"}, - {file = "kiwisolver-1.4.7-cp312-cp312-win32.whl", hash = "sha256:18e0cca3e008e17fe9b164b55735a325140a5a35faad8de92dd80265cd5eb80b"}, - {file = "kiwisolver-1.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:58cb20602b18f86f83a5c87d3ee1c766a79c0d452f8def86d925e6c60fbf7bfb"}, - {file = "kiwisolver-1.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:f5a8b53bdc0b3961f8b6125e198617c40aeed638b387913bf1ce78afb1b0be2a"}, - {file = "kiwisolver-1.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2e6039dcbe79a8e0f044f1c39db1986a1b8071051efba3ee4d74f5b365f5226e"}, - {file = "kiwisolver-1.4.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a1ecf0ac1c518487d9d23b1cd7139a6a65bc460cd101ab01f1be82ecf09794b6"}, - {file = "kiwisolver-1.4.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ab9ccab2b5bd5702ab0803676a580fffa2aa178c2badc5557a84cc943fcf750"}, - {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f816dd2277f8d63d79f9c8473a79fe54047bc0467754962840782c575522224d"}, - {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf8bcc23ceb5a1b624572a1623b9f79d2c3b337c8c455405ef231933a10da379"}, - {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dea0bf229319828467d7fca8c7c189780aa9ff679c94539eed7532ebe33ed37c"}, - {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c06a4c7cf15ec739ce0e5971b26c93638730090add60e183530d70848ebdd34"}, - {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:913983ad2deb14e66d83c28b632fd35ba2b825031f2fa4ca29675e665dfecbe1"}, - {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5337ec7809bcd0f424c6b705ecf97941c46279cf5ed92311782c7c9c2026f07f"}, - {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4c26ed10c4f6fa6ddb329a5120ba3b6db349ca192ae211e882970bfc9d91420b"}, - {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c619b101e6de2222c1fcb0531e1b17bbffbe54294bfba43ea0d411d428618c27"}, - {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:073a36c8273647592ea332e816e75ef8da5c303236ec0167196793eb1e34657a"}, - {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3ce6b2b0231bda412463e152fc18335ba32faf4e8c23a754ad50ffa70e4091ee"}, - {file = "kiwisolver-1.4.7-cp313-cp313-win32.whl", hash = "sha256:f4c9aee212bc89d4e13f58be11a56cc8036cabad119259d12ace14b34476fd07"}, - {file = "kiwisolver-1.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:8a3ec5aa8e38fc4c8af308917ce12c536f1c88452ce554027e55b22cbbfbff76"}, - {file = "kiwisolver-1.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:76c8094ac20ec259471ac53e774623eb62e6e1f56cd8690c67ce6ce4fcb05650"}, - {file = "kiwisolver-1.4.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5d5abf8f8ec1f4e22882273c423e16cae834c36856cac348cfbfa68e01c40f3a"}, - {file = "kiwisolver-1.4.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:aeb3531b196ef6f11776c21674dba836aeea9d5bd1cf630f869e3d90b16cfade"}, - {file = "kiwisolver-1.4.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b7d755065e4e866a8086c9bdada157133ff466476a2ad7861828e17b6026e22c"}, - {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08471d4d86cbaec61f86b217dd938a83d85e03785f51121e791a6e6689a3be95"}, - {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7bbfcb7165ce3d54a3dfbe731e470f65739c4c1f85bb1018ee912bae139e263b"}, - {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d34eb8494bea691a1a450141ebb5385e4b69d38bb8403b5146ad279f4b30fa3"}, - {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9242795d174daa40105c1d86aba618e8eab7bf96ba8c3ee614da8302a9f95503"}, - {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a0f64a48bb81af7450e641e3fe0b0394d7381e342805479178b3d335d60ca7cf"}, - {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8e045731a5416357638d1700927529e2b8ab304811671f665b225f8bf8d8f933"}, - {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:4322872d5772cae7369f8351da1edf255a604ea7087fe295411397d0cfd9655e"}, - {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:e1631290ee9271dffe3062d2634c3ecac02c83890ada077d225e081aca8aab89"}, - {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:edcfc407e4eb17e037bca59be0e85a2031a2ac87e4fed26d3e9df88b4165f92d"}, - {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4d05d81ecb47d11e7f8932bd8b61b720bf0b41199358f3f5e36d38e28f0532c5"}, - {file = "kiwisolver-1.4.7-cp38-cp38-win32.whl", hash = "sha256:b38ac83d5f04b15e515fd86f312479d950d05ce2368d5413d46c088dda7de90a"}, - {file = "kiwisolver-1.4.7-cp38-cp38-win_amd64.whl", hash = "sha256:d83db7cde68459fc803052a55ace60bea2bae361fc3b7a6d5da07e11954e4b09"}, - {file = "kiwisolver-1.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f9362ecfca44c863569d3d3c033dbe8ba452ff8eed6f6b5806382741a1334bd"}, - {file = "kiwisolver-1.4.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e8df2eb9b2bac43ef8b082e06f750350fbbaf2887534a5be97f6cf07b19d9583"}, - {file = "kiwisolver-1.4.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f32d6edbc638cde7652bd690c3e728b25332acbadd7cad670cc4a02558d9c417"}, - {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e2e6c39bd7b9372b0be21456caab138e8e69cc0fc1190a9dfa92bd45a1e6e904"}, - {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dda56c24d869b1193fcc763f1284b9126550eaf84b88bbc7256e15028f19188a"}, - {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79849239c39b5e1fd906556c474d9b0439ea6792b637511f3fe3a41158d89ca8"}, - {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e3bc157fed2a4c02ec468de4ecd12a6e22818d4f09cde2c31ee3226ffbefab2"}, - {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3da53da805b71e41053dc670f9a820d1157aae77b6b944e08024d17bcd51ef88"}, - {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8705f17dfeb43139a692298cb6637ee2e59c0194538153e83e9ee0c75c2eddde"}, - {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:82a5c2f4b87c26bb1a0ef3d16b5c4753434633b83d365cc0ddf2770c93829e3c"}, - {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce8be0466f4c0d585cdb6c1e2ed07232221df101a4c6f28821d2aa754ca2d9e2"}, - {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:409afdfe1e2e90e6ee7fc896f3df9a7fec8e793e58bfa0d052c8a82f99c37abb"}, - {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5b9c3f4ee0b9a439d2415012bd1b1cc2df59e4d6a9939f4d669241d30b414327"}, - {file = "kiwisolver-1.4.7-cp39-cp39-win32.whl", hash = "sha256:a79ae34384df2b615eefca647a2873842ac3b596418032bef9a7283675962644"}, - {file = "kiwisolver-1.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:cf0438b42121a66a3a667de17e779330fc0f20b0d97d59d2f2121e182b0505e4"}, - {file = "kiwisolver-1.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:764202cc7e70f767dab49e8df52c7455e8de0df5d858fa801a11aa0d882ccf3f"}, - {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:94252291e3fe68001b1dd747b4c0b3be12582839b95ad4d1b641924d68fd4643"}, - {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b7dfa3b546da08a9f622bb6becdb14b3e24aaa30adba66749d38f3cc7ea9706"}, - {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd3de6481f4ed8b734da5df134cd5a6a64fe32124fe83dde1e5b5f29fe30b1e6"}, - {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a91b5f9f1205845d488c928e8570dcb62b893372f63b8b6e98b863ebd2368ff2"}, - {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40fa14dbd66b8b8f470d5fc79c089a66185619d31645f9b0773b88b19f7223c4"}, - {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:eb542fe7933aa09d8d8f9d9097ef37532a7df6497819d16efe4359890a2f417a"}, - {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bfa1acfa0c54932d5607e19a2c24646fb4c1ae2694437789129cf099789a3b00"}, - {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:eee3ea935c3d227d49b4eb85660ff631556841f6e567f0f7bda972df6c2c9935"}, - {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f3160309af4396e0ed04db259c3ccbfdc3621b5559b5453075e5de555e1f3a1b"}, - {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a17f6a29cf8935e587cc8a4dbfc8368c55edc645283db0ce9801016f83526c2d"}, - {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10849fb2c1ecbfae45a693c070e0320a91b35dd4bcf58172c023b994283a124d"}, - {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:ac542bf38a8a4be2dc6b15248d36315ccc65f0743f7b1a76688ffb6b5129a5c2"}, - {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8b01aac285f91ca889c800042c35ad3b239e704b150cfd3382adfc9dcc780e39"}, - {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:48be928f59a1f5c8207154f935334d374e79f2b5d212826307d072595ad76a2e"}, - {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f37cfe618a117e50d8c240555331160d73d0411422b59b5ee217843d7b693608"}, - {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:599b5c873c63a1f6ed7eead644a8a380cfbdf5db91dcb6f85707aaab213b1674"}, - {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:801fa7802e5cfabe3ab0c81a34c323a319b097dfb5004be950482d882f3d7225"}, - {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0c6c43471bc764fad4bc99c5c2d6d16a676b1abf844ca7c8702bdae92df01ee0"}, - {file = "kiwisolver-1.4.7.tar.gz", hash = "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60"}, -] - [[package]] name = "kombu" version = "5.4.2" @@ -2190,160 +1700,6 @@ files = [ {file = "lazy_object_proxy-1.10.0-pp310.pp311.pp312.pp38.pp39-none-any.whl", hash = "sha256:80fa48bd89c8f2f456fc0765c11c23bf5af827febacd2f523ca5bc1893fcc09d"}, ] -[[package]] -name = "lxml" -version = "5.3.0" -description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." -optional = false -python-versions = ">=3.6" -files = [ - {file = "lxml-5.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:dd36439be765e2dde7660212b5275641edbc813e7b24668831a5c8ac91180656"}, - {file = "lxml-5.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ae5fe5c4b525aa82b8076c1a59d642c17b6e8739ecf852522c6321852178119d"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:501d0d7e26b4d261fca8132854d845e4988097611ba2531408ec91cf3fd9d20a"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb66442c2546446944437df74379e9cf9e9db353e61301d1a0e26482f43f0dd8"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e41506fec7a7f9405b14aa2d5c8abbb4dbbd09d88f9496958b6d00cb4d45330"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f7d4a670107d75dfe5ad080bed6c341d18c4442f9378c9f58e5851e86eb79965"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41ce1f1e2c7755abfc7e759dc34d7d05fd221723ff822947132dc934d122fe22"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:44264ecae91b30e5633013fb66f6ddd05c006d3e0e884f75ce0b4755b3e3847b"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:3c174dc350d3ec52deb77f2faf05c439331d6ed5e702fc247ccb4e6b62d884b7"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:2dfab5fa6a28a0b60a20638dc48e6343c02ea9933e3279ccb132f555a62323d8"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b1c8c20847b9f34e98080da785bb2336ea982e7f913eed5809e5a3c872900f32"}, - {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2c86bf781b12ba417f64f3422cfc302523ac9cd1d8ae8c0f92a1c66e56ef2e86"}, - {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c162b216070f280fa7da844531169be0baf9ccb17263cf5a8bf876fcd3117fa5"}, - {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:36aef61a1678cb778097b4a6eeae96a69875d51d1e8f4d4b491ab3cfb54b5a03"}, - {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f65e5120863c2b266dbcc927b306c5b78e502c71edf3295dfcb9501ec96e5fc7"}, - {file = "lxml-5.3.0-cp310-cp310-win32.whl", hash = "sha256:ef0c1fe22171dd7c7c27147f2e9c3e86f8bdf473fed75f16b0c2e84a5030ce80"}, - {file = "lxml-5.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:052d99051e77a4f3e8482c65014cf6372e61b0a6f4fe9edb98503bb5364cfee3"}, - {file = "lxml-5.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74bcb423462233bc5d6066e4e98b0264e7c1bed7541fff2f4e34fe6b21563c8b"}, - {file = "lxml-5.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a3d819eb6f9b8677f57f9664265d0a10dd6551d227afb4af2b9cd7bdc2ccbf18"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b8f5db71b28b8c404956ddf79575ea77aa8b1538e8b2ef9ec877945b3f46442"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c3406b63232fc7e9b8783ab0b765d7c59e7c59ff96759d8ef9632fca27c7ee4"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ecdd78ab768f844c7a1d4a03595038c166b609f6395e25af9b0f3f26ae1230f"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:168f2dfcfdedf611eb285efac1516c8454c8c99caf271dccda8943576b67552e"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa617107a410245b8660028a7483b68e7914304a6d4882b5ff3d2d3eb5948d8c"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:69959bd3167b993e6e710b99051265654133a98f20cec1d9b493b931942e9c16"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:bd96517ef76c8654446fc3db9242d019a1bb5fe8b751ba414765d59f99210b79"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ab6dd83b970dc97c2d10bc71aa925b84788c7c05de30241b9e96f9b6d9ea3080"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:eec1bb8cdbba2925bedc887bc0609a80e599c75b12d87ae42ac23fd199445654"}, - {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6a7095eeec6f89111d03dabfe5883a1fd54da319c94e0fb104ee8f23616b572d"}, - {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6f651ebd0b21ec65dfca93aa629610a0dbc13dbc13554f19b0113da2e61a4763"}, - {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f422a209d2455c56849442ae42f25dbaaba1c6c3f501d58761c619c7836642ec"}, - {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:62f7fdb0d1ed2065451f086519865b4c90aa19aed51081979ecd05a21eb4d1be"}, - {file = "lxml-5.3.0-cp311-cp311-win32.whl", hash = "sha256:c6379f35350b655fd817cd0d6cbeef7f265f3ae5fedb1caae2eb442bbeae9ab9"}, - {file = "lxml-5.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c52100e2c2dbb0649b90467935c4b0de5528833c76a35ea1a2691ec9f1ee7a1"}, - {file = "lxml-5.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e99f5507401436fdcc85036a2e7dc2e28d962550afe1cbfc07c40e454256a859"}, - {file = "lxml-5.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:384aacddf2e5813a36495233b64cb96b1949da72bef933918ba5c84e06af8f0e"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:874a216bf6afaf97c263b56371434e47e2c652d215788396f60477540298218f"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65ab5685d56914b9a2a34d67dd5488b83213d680b0c5d10b47f81da5a16b0b0e"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aac0bbd3e8dd2d9c45ceb82249e8bdd3ac99131a32b4d35c8af3cc9db1657179"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b369d3db3c22ed14c75ccd5af429086f166a19627e84a8fdade3f8f31426e52a"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24037349665434f375645fa9d1f5304800cec574d0310f618490c871fd902b3"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:62d172f358f33a26d6b41b28c170c63886742f5b6772a42b59b4f0fa10526cb1"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:c1f794c02903c2824fccce5b20c339a1a14b114e83b306ff11b597c5f71a1c8d"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:5d6a6972b93c426ace71e0be9a6f4b2cfae9b1baed2eed2006076a746692288c"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:3879cc6ce938ff4eb4900d901ed63555c778731a96365e53fadb36437a131a99"}, - {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:74068c601baff6ff021c70f0935b0c7bc528baa8ea210c202e03757c68c5a4ff"}, - {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ecd4ad8453ac17bc7ba3868371bffb46f628161ad0eefbd0a855d2c8c32dd81a"}, - {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7e2f58095acc211eb9d8b5771bf04df9ff37d6b87618d1cbf85f92399c98dae8"}, - {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e63601ad5cd8f860aa99d109889b5ac34de571c7ee902d6812d5d9ddcc77fa7d"}, - {file = "lxml-5.3.0-cp312-cp312-win32.whl", hash = "sha256:17e8d968d04a37c50ad9c456a286b525d78c4a1c15dd53aa46c1d8e06bf6fa30"}, - {file = "lxml-5.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:c1a69e58a6bb2de65902051d57fde951febad631a20a64572677a1052690482f"}, - {file = "lxml-5.3.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c72e9563347c7395910de6a3100a4840a75a6f60e05af5e58566868d5eb2d6a"}, - {file = "lxml-5.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e92ce66cd919d18d14b3856906a61d3f6b6a8500e0794142338da644260595cd"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d04f064bebdfef9240478f7a779e8c5dc32b8b7b0b2fc6a62e39b928d428e51"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c2fb570d7823c2bbaf8b419ba6e5662137f8166e364a8b2b91051a1fb40ab8b"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c120f43553ec759f8de1fee2f4794452b0946773299d44c36bfe18e83caf002"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:562e7494778a69086f0312ec9689f6b6ac1c6b65670ed7d0267e49f57ffa08c4"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:423b121f7e6fa514ba0c7918e56955a1d4470ed35faa03e3d9f0e3baa4c7e492"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c00f323cc00576df6165cc9d21a4c21285fa6b9989c5c39830c3903dc4303ef3"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:1fdc9fae8dd4c763e8a31e7630afef517eab9f5d5d31a278df087f307bf601f4"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:658f2aa69d31e09699705949b5fc4719cbecbd4a97f9656a232e7d6c7be1a367"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1473427aff3d66a3fa2199004c3e601e6c4500ab86696edffdbc84954c72d832"}, - {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a87de7dd873bf9a792bf1e58b1c3887b9264036629a5bf2d2e6579fe8e73edff"}, - {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0d7b36afa46c97875303a94e8f3ad932bf78bace9e18e603f2085b652422edcd"}, - {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cf120cce539453ae086eacc0130a324e7026113510efa83ab42ef3fcfccac7fb"}, - {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:df5c7333167b9674aa8ae1d4008fa4bc17a313cc490b2cca27838bbdcc6bb15b"}, - {file = "lxml-5.3.0-cp313-cp313-win32.whl", hash = "sha256:c802e1c2ed9f0c06a65bc4ed0189d000ada8049312cfeab6ca635e39c9608957"}, - {file = "lxml-5.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:406246b96d552e0503e17a1006fd27edac678b3fcc9f1be71a2f94b4ff61528d"}, - {file = "lxml-5.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8f0de2d390af441fe8b2c12626d103540b5d850d585b18fcada58d972b74a74e"}, - {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1afe0a8c353746e610bd9031a630a95bcfb1a720684c3f2b36c4710a0a96528f"}, - {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56b9861a71575f5795bde89256e7467ece3d339c9b43141dbdd54544566b3b94"}, - {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:9fb81d2824dff4f2e297a276297e9031f46d2682cafc484f49de182aa5e5df99"}, - {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2c226a06ecb8cdef28845ae976da407917542c5e6e75dcac7cc33eb04aaeb237"}, - {file = "lxml-5.3.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:7d3d1ca42870cdb6d0d29939630dbe48fa511c203724820fc0fd507b2fb46577"}, - {file = "lxml-5.3.0-cp36-cp36m-win32.whl", hash = "sha256:094cb601ba9f55296774c2d57ad68730daa0b13dc260e1f941b4d13678239e70"}, - {file = "lxml-5.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:eafa2c8658f4e560b098fe9fc54539f86528651f61849b22111a9b107d18910c"}, - {file = "lxml-5.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cb83f8a875b3d9b458cada4f880fa498646874ba4011dc974e071a0a84a1b033"}, - {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25f1b69d41656b05885aa185f5fdf822cb01a586d1b32739633679699f220391"}, - {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23e0553b8055600b3bf4a00b255ec5c92e1e4aebf8c2c09334f8368e8bd174d6"}, - {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ada35dd21dc6c039259596b358caab6b13f4db4d4a7f8665764d616daf9cc1d"}, - {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:81b4e48da4c69313192d8c8d4311e5d818b8be1afe68ee20f6385d0e96fc9512"}, - {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:2bc9fd5ca4729af796f9f59cd8ff160fe06a474da40aca03fcc79655ddee1a8b"}, - {file = "lxml-5.3.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:07da23d7ee08577760f0a71d67a861019103e4812c87e2fab26b039054594cc5"}, - {file = "lxml-5.3.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:ea2e2f6f801696ad7de8aec061044d6c8c0dd4037608c7cab38a9a4d316bfb11"}, - {file = "lxml-5.3.0-cp37-cp37m-win32.whl", hash = "sha256:5c54afdcbb0182d06836cc3d1be921e540be3ebdf8b8a51ee3ef987537455f84"}, - {file = "lxml-5.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:f2901429da1e645ce548bf9171784c0f74f0718c3f6150ce166be39e4dd66c3e"}, - {file = "lxml-5.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c56a1d43b2f9ee4786e4658c7903f05da35b923fb53c11025712562d5cc02753"}, - {file = "lxml-5.3.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ee8c39582d2652dcd516d1b879451500f8db3fe3607ce45d7c5957ab2596040"}, - {file = "lxml-5.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdf3a3059611f7585a78ee10399a15566356116a4288380921a4b598d807a22"}, - {file = "lxml-5.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:146173654d79eb1fc97498b4280c1d3e1e5d58c398fa530905c9ea50ea849b22"}, - {file = "lxml-5.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:0a7056921edbdd7560746f4221dca89bb7a3fe457d3d74267995253f46343f15"}, - {file = "lxml-5.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:9e4b47ac0f5e749cfc618efdf4726269441014ae1d5583e047b452a32e221920"}, - {file = "lxml-5.3.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f914c03e6a31deb632e2daa881fe198461f4d06e57ac3d0e05bbcab8eae01945"}, - {file = "lxml-5.3.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:213261f168c5e1d9b7535a67e68b1f59f92398dd17a56d934550837143f79c42"}, - {file = "lxml-5.3.0-cp38-cp38-win32.whl", hash = "sha256:218c1b2e17a710e363855594230f44060e2025b05c80d1f0661258142b2add2e"}, - {file = "lxml-5.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:315f9542011b2c4e1d280e4a20ddcca1761993dda3afc7a73b01235f8641e903"}, - {file = "lxml-5.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1ffc23010330c2ab67fac02781df60998ca8fe759e8efde6f8b756a20599c5de"}, - {file = "lxml-5.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2b3778cb38212f52fac9fe913017deea2fdf4eb1a4f8e4cfc6b009a13a6d3fcc"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b0c7a688944891086ba192e21c5229dea54382f4836a209ff8d0a660fac06be"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:747a3d3e98e24597981ca0be0fd922aebd471fa99d0043a3842d00cdcad7ad6a"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86a6b24b19eaebc448dc56b87c4865527855145d851f9fc3891673ff97950540"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b11a5d918a6216e521c715b02749240fb07ae5a1fefd4b7bf12f833bc8b4fe70"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68b87753c784d6acb8a25b05cb526c3406913c9d988d51f80adecc2b0775d6aa"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:109fa6fede314cc50eed29e6e56c540075e63d922455346f11e4d7a036d2b8cf"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:02ced472497b8362c8e902ade23e3300479f4f43e45f4105c85ef43b8db85229"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:6b038cc86b285e4f9fea2ba5ee76e89f21ed1ea898e287dc277a25884f3a7dfe"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:7437237c6a66b7ca341e868cda48be24b8701862757426852c9b3186de1da8a2"}, - {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7f41026c1d64043a36fda21d64c5026762d53a77043e73e94b71f0521939cc71"}, - {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:482c2f67761868f0108b1743098640fbb2a28a8e15bf3f47ada9fa59d9fe08c3"}, - {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:1483fd3358963cc5c1c9b122c80606a3a79ee0875bcac0204149fa09d6ff2727"}, - {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2dec2d1130a9cda5b904696cec33b2cfb451304ba9081eeda7f90f724097300a"}, - {file = "lxml-5.3.0-cp39-cp39-win32.whl", hash = "sha256:a0eabd0a81625049c5df745209dc7fcef6e2aea7793e5f003ba363610aa0a3ff"}, - {file = "lxml-5.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:89e043f1d9d341c52bf2af6d02e6adde62e0a46e6755d5eb60dc6e4f0b8aeca2"}, - {file = "lxml-5.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7b1cd427cb0d5f7393c31b7496419da594fe600e6fdc4b105a54f82405e6626c"}, - {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51806cfe0279e06ed8500ce19479d757db42a30fd509940b1701be9c86a5ff9a"}, - {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee70d08fd60c9565ba8190f41a46a54096afa0eeb8f76bd66f2c25d3b1b83005"}, - {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8dc2c0395bea8254d8daebc76dcf8eb3a95ec2a46fa6fae5eaccee366bfe02ce"}, - {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6ba0d3dcac281aad8a0e5b14c7ed6f9fa89c8612b47939fc94f80b16e2e9bc83"}, - {file = "lxml-5.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:6e91cf736959057f7aac7adfc83481e03615a8e8dd5758aa1d95ea69e8931dba"}, - {file = "lxml-5.3.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:94d6c3782907b5e40e21cadf94b13b0842ac421192f26b84c45f13f3c9d5dc27"}, - {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c300306673aa0f3ed5ed9372b21867690a17dba38c68c44b287437c362ce486b"}, - {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d9b952e07aed35fe2e1a7ad26e929595412db48535921c5013edc8aa4a35ce"}, - {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:01220dca0d066d1349bd6a1726856a78f7929f3878f7e2ee83c296c69495309e"}, - {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2d9b8d9177afaef80c53c0a9e30fa252ff3036fb1c6494d427c066a4ce6a282f"}, - {file = "lxml-5.3.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:20094fc3f21ea0a8669dc4c61ed7fa8263bd37d97d93b90f28fc613371e7a875"}, - {file = "lxml-5.3.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ace2c2326a319a0bb8a8b0e5b570c764962e95818de9f259ce814ee666603f19"}, - {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92e67a0be1639c251d21e35fe74df6bcc40cba445c2cda7c4a967656733249e2"}, - {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd5350b55f9fecddc51385463a4f67a5da829bc741e38cf689f38ec9023f54ab"}, - {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c1fefd7e3d00921c44dc9ca80a775af49698bbfd92ea84498e56acffd4c5469"}, - {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:71a8dd38fbd2f2319136d4ae855a7078c69c9a38ae06e0c17c73fd70fc6caad8"}, - {file = "lxml-5.3.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:97acf1e1fd66ab53dacd2c35b319d7e548380c2e9e8c54525c6e76d21b1ae3b1"}, - {file = "lxml-5.3.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:68934b242c51eb02907c5b81d138cb977b2129a0a75a8f8b60b01cb8586c7b21"}, - {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b710bc2b8292966b23a6a0121f7a6c51d45d2347edcc75f016ac123b8054d3f2"}, - {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18feb4b93302091b1541221196a2155aa296c363fd233814fa11e181adebc52f"}, - {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3eb44520c4724c2e1a57c0af33a379eee41792595023f367ba3952a2d96c2aab"}, - {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:609251a0ca4770e5a8768ff902aa02bf636339c5a93f9349b48eb1f606f7f3e9"}, - {file = "lxml-5.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:516f491c834eb320d6c843156440fe7fc0d50b33e44387fcec5b02f0bc118a4c"}, - {file = "lxml-5.3.0.tar.gz", hash = "sha256:4e109ca30d1edec1ac60cdbe341905dc3b8f55b16855e03a54aaf59e51ec8c6f"}, -] - -[package.extras] -cssselect = ["cssselect (>=0.7)"] -html-clean = ["lxml-html-clean"] -html5 = ["html5lib"] -htmlsoup = ["BeautifulSoup4"] -source = ["Cython (>=3.0.11)"] - [[package]] name = "markdown" version = "3.6" @@ -2445,74 +1801,6 @@ files = [ {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, ] -[[package]] -name = "matplotlib" -version = "3.7.5" -description = "Python plotting package" -optional = false -python-versions = ">=3.8" -files = [ - {file = "matplotlib-3.7.5-cp310-cp310-macosx_10_12_universal2.whl", hash = "sha256:4a87b69cb1cb20943010f63feb0b2901c17a3b435f75349fd9865713bfa63925"}, - {file = "matplotlib-3.7.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:d3ce45010fefb028359accebb852ca0c21bd77ec0f281952831d235228f15810"}, - {file = "matplotlib-3.7.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fbea1e762b28400393d71be1a02144aa16692a3c4c676ba0178ce83fc2928fdd"}, - {file = "matplotlib-3.7.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec0e1adc0ad70ba8227e957551e25a9d2995e319c29f94a97575bb90fa1d4469"}, - {file = "matplotlib-3.7.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6738c89a635ced486c8a20e20111d33f6398a9cbebce1ced59c211e12cd61455"}, - {file = "matplotlib-3.7.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1210b7919b4ed94b5573870f316bca26de3e3b07ffdb563e79327dc0e6bba515"}, - {file = "matplotlib-3.7.5-cp310-cp310-win32.whl", hash = "sha256:068ebcc59c072781d9dcdb82f0d3f1458271c2de7ca9c78f5bd672141091e9e1"}, - {file = "matplotlib-3.7.5-cp310-cp310-win_amd64.whl", hash = "sha256:f098ffbaab9df1e3ef04e5a5586a1e6b1791380698e84938d8640961c79b1fc0"}, - {file = "matplotlib-3.7.5-cp311-cp311-macosx_10_12_universal2.whl", hash = "sha256:f65342c147572673f02a4abec2d5a23ad9c3898167df9b47c149f32ce61ca078"}, - {file = "matplotlib-3.7.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4ddf7fc0e0dc553891a117aa083039088d8a07686d4c93fb8a810adca68810af"}, - {file = "matplotlib-3.7.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0ccb830fc29442360d91be48527809f23a5dcaee8da5f4d9b2d5b867c1b087b8"}, - {file = "matplotlib-3.7.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efc6bb28178e844d1f408dd4d6341ee8a2e906fc9e0fa3dae497da4e0cab775d"}, - {file = "matplotlib-3.7.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b15c4c2d374f249f324f46e883340d494c01768dd5287f8bc00b65b625ab56c"}, - {file = "matplotlib-3.7.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d028555421912307845e59e3de328260b26d055c5dac9b182cc9783854e98fb"}, - {file = "matplotlib-3.7.5-cp311-cp311-win32.whl", hash = "sha256:fe184b4625b4052fa88ef350b815559dd90cc6cc8e97b62f966e1ca84074aafa"}, - {file = "matplotlib-3.7.5-cp311-cp311-win_amd64.whl", hash = "sha256:084f1f0f2f1010868c6f1f50b4e1c6f2fb201c58475494f1e5b66fed66093647"}, - {file = "matplotlib-3.7.5-cp312-cp312-macosx_10_12_universal2.whl", hash = "sha256:34bceb9d8ddb142055ff27cd7135f539f2f01be2ce0bafbace4117abe58f8fe4"}, - {file = "matplotlib-3.7.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:c5a2134162273eb8cdfd320ae907bf84d171de948e62180fa372a3ca7cf0f433"}, - {file = "matplotlib-3.7.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:039ad54683a814002ff37bf7981aa1faa40b91f4ff84149beb53d1eb64617980"}, - {file = "matplotlib-3.7.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d742ccd1b09e863b4ca58291728db645b51dab343eebb08d5d4b31b308296ce"}, - {file = "matplotlib-3.7.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:743b1c488ca6a2bc7f56079d282e44d236bf375968bfd1b7ba701fd4d0fa32d6"}, - {file = "matplotlib-3.7.5-cp312-cp312-win_amd64.whl", hash = "sha256:fbf730fca3e1f23713bc1fae0a57db386e39dc81ea57dc305c67f628c1d7a342"}, - {file = "matplotlib-3.7.5-cp38-cp38-macosx_10_12_universal2.whl", hash = "sha256:cfff9b838531698ee40e40ea1a8a9dc2c01edb400b27d38de6ba44c1f9a8e3d2"}, - {file = "matplotlib-3.7.5-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:1dbcca4508bca7847fe2d64a05b237a3dcaec1f959aedb756d5b1c67b770c5ee"}, - {file = "matplotlib-3.7.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4cdf4ef46c2a1609a50411b66940b31778db1e4b73d4ecc2eaa40bd588979b13"}, - {file = "matplotlib-3.7.5-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:167200ccfefd1674b60e957186dfd9baf58b324562ad1a28e5d0a6b3bea77905"}, - {file = "matplotlib-3.7.5-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:53e64522934df6e1818b25fd48cf3b645b11740d78e6ef765fbb5fa5ce080d02"}, - {file = "matplotlib-3.7.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3e3bc79b2d7d615067bd010caff9243ead1fc95cf735c16e4b2583173f717eb"}, - {file = "matplotlib-3.7.5-cp38-cp38-win32.whl", hash = "sha256:6b641b48c6819726ed47c55835cdd330e53747d4efff574109fd79b2d8a13748"}, - {file = "matplotlib-3.7.5-cp38-cp38-win_amd64.whl", hash = "sha256:f0b60993ed3488b4532ec6b697059897891927cbfc2b8d458a891b60ec03d9d7"}, - {file = "matplotlib-3.7.5-cp39-cp39-macosx_10_12_universal2.whl", hash = "sha256:090964d0afaff9c90e4d8de7836757e72ecfb252fb02884016d809239f715651"}, - {file = "matplotlib-3.7.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:9fc6fcfbc55cd719bc0bfa60bde248eb68cf43876d4c22864603bdd23962ba25"}, - {file = "matplotlib-3.7.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e7cc3078b019bb863752b8b60e8b269423000f1603cb2299608231996bd9d54"}, - {file = "matplotlib-3.7.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e4e9a868e8163abaaa8259842d85f949a919e1ead17644fb77a60427c90473c"}, - {file = "matplotlib-3.7.5-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fa7ebc995a7d747dacf0a717d0eb3aa0f0c6a0e9ea88b0194d3a3cd241a1500f"}, - {file = "matplotlib-3.7.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3785bfd83b05fc0e0c2ae4c4a90034fe693ef96c679634756c50fe6efcc09856"}, - {file = "matplotlib-3.7.5-cp39-cp39-win32.whl", hash = "sha256:29b058738c104d0ca8806395f1c9089dfe4d4f0f78ea765c6c704469f3fffc81"}, - {file = "matplotlib-3.7.5-cp39-cp39-win_amd64.whl", hash = "sha256:fd4028d570fa4b31b7b165d4a685942ae9cdc669f33741e388c01857d9723eab"}, - {file = "matplotlib-3.7.5-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2a9a3f4d6a7f88a62a6a18c7e6a84aedcaf4faf0708b4ca46d87b19f1b526f88"}, - {file = "matplotlib-3.7.5-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9b3fd853d4a7f008a938df909b96db0b454225f935d3917520305b90680579c"}, - {file = "matplotlib-3.7.5-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0ad550da9f160737d7890217c5eeed4337d07e83ca1b2ca6535078f354e7675"}, - {file = "matplotlib-3.7.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:20da7924a08306a861b3f2d1da0d1aa9a6678e480cf8eacffe18b565af2813e7"}, - {file = "matplotlib-3.7.5-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b45c9798ea6bb920cb77eb7306409756a7fab9db9b463e462618e0559aecb30e"}, - {file = "matplotlib-3.7.5-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a99866267da1e561c7776fe12bf4442174b79aac1a47bd7e627c7e4d077ebd83"}, - {file = "matplotlib-3.7.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b6aa62adb6c268fc87d80f963aca39c64615c31830b02697743c95590ce3fbb"}, - {file = "matplotlib-3.7.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e530ab6a0afd082d2e9c17eb1eb064a63c5b09bb607b2b74fa41adbe3e162286"}, - {file = "matplotlib-3.7.5.tar.gz", hash = "sha256:1e5c971558ebc811aa07f54c7b7c677d78aa518ef4c390e14673a09e0860184a"}, -] - -[package.dependencies] -contourpy = ">=1.0.1" -cycler = ">=0.10" -fonttools = ">=4.22.0" -importlib-resources = {version = ">=3.2.0", markers = "python_version < \"3.10\""} -kiwisolver = ">=1.0.1" -numpy = ">=1.20,<2" -packaging = ">=20.0" -pillow = ">=6.2.0" -pyparsing = ">=2.3.1" -python-dateutil = ">=2.7" - [[package]] name = "matplotlib-inline" version = "0.1.7" @@ -2696,62 +1984,20 @@ files = [ griffe = ">=0.49" mkdocstrings = ">=0.25" -[[package]] -name = "mypy-extensions" -version = "1.0.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.5" -files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, -] - -[[package]] -name = "napalm" -version = "5.0.0" -description = "Network Automation and Programmability Abstraction Layer with Multivendor support" -optional = false -python-versions = "*" -files = [ - {file = "napalm-5.0.0-py2.py3-none-any.whl", hash = "sha256:458837932e527ca06a4bab7e600b0ca6e6bc3bb4b33fad9c9ef2befc7df6d2f5"}, - {file = "napalm-5.0.0.tar.gz", hash = "sha256:350ac3d74f2f10030dbae44d3395551d7e03ee25c65fa5eb8263a4e6f51f2c94"}, -] - -[package.dependencies] -cffi = ">=1.11.3" -jinja2 = "*" -junos-eznc = ">=2.7.0" -lxml = ">=4.3.0" -ncclient = "*" -netaddr = "*" -netmiko = ">=4.1.0" -netutils = ">=1.0.0" -paramiko = ">=2.6.0" -pyeapi = ">=1.0.2" -pyYAML = "*" -requests = ">=2.7.0" -scp = "*" -setuptools = ">=38.4.0" -textfsm = "*" -ttp = "*" -ttp-templates = "*" -typing-extensions = ">=4.3.0" - [[package]] name = "nautobot" -version = "2.3.12" +version = "2.3.16" description = "Source of truth and network automation platform." optional = false python-versions = "<3.13,>=3.8" files = [ - {file = "nautobot-2.3.12-py3-none-any.whl", hash = "sha256:f6317d0af6c592b0e7d95bf2dbfbd1826976f8ceb284fe1457dd18509a2b5996"}, - {file = "nautobot-2.3.12.tar.gz", hash = "sha256:c81c2ed2691f7132194d560d7c0dbe0931b0bc6afdbb2a1edfc7db797ad66237"}, + {file = "nautobot-2.3.16-py3-none-any.whl", hash = "sha256:60a1043c97ca0c6575c01ee7b92d28da761843d449d6ad1f038ba2dafeefcaf3"}, + {file = "nautobot-2.3.16.tar.gz", hash = "sha256:92aed5dfbf457f52f47b96191103dd327981b0173bc8f813dc03a6c929cda45b"}, ] [package.dependencies] celery = ">=5.3.6,<5.4.0" -Django = ">=4.2.16,<4.3.0" +Django = ">=4.2.17,<4.3.0" django-ajax-tables = ">=1.1.1,<1.2.0" django-celery-beat = ">=2.6.0,<2.7.0" django-celery-results = ">=2.5.1,<2.6.0" @@ -2766,7 +2012,10 @@ django-prometheus = ">=2.3.1,<2.4.0" django-redis = ">=5.4.0,<5.5.0" django-silk = ">=5.1.0,<5.2.0" django-structlog = {version = ">=8.1.0,<9.0.0", extras = ["celery"]} -django-tables2 = ">=2.7.0,<2.8.0" +django-tables2 = [ + {version = "2.7.0", markers = "python_version < \"3.9\""}, + {version = ">=2.7.4,<2.8.0", markers = "python_version >= \"3.9\""}, +] django-taggit = ">=5.0.0,<5.1.0" django-timezone-field = ">=7.0,<7.1" django-tree-queries = ">=0.19.0,<0.20.0" @@ -2778,14 +2027,14 @@ emoji = ">=2.12.1,<2.13.0" GitPython = ">=3.1.43,<3.2.0" graphene-django = ">=2.16.0,<2.17.0" graphene-django-optimizer = ">=0.8.0,<0.9.0" -Jinja2 = ">=3.1.4,<3.2.0" +Jinja2 = ">=3.1.5,<3.2.0" jsonschema = ">=4.7.0,<5.0.0" kombu = ">=5.4.2,<5.5.0" Markdown = ">=3.6,<3.7" MarkupSafe = ">=2.1.5,<2.2.0" netaddr = ">=1.3.0,<1.4.0" netutils = ">=1.6.0,<2.0.0" -nh3 = ">=0.2.15,<0.3.0" +nh3 = ">=0.2.20,<0.3.0" packaging = ">=23.1" Pillow = ">=10.3.0,<10.4.0" prometheus-client = ">=0.20.0,<0.21.0" @@ -2804,52 +2053,6 @@ napalm = ["napalm (>=4.1.0,<6.0.0)"] remote-storage = ["django-storages (==1.14.3)"] sso = ["social-auth-core[saml] (>=4.5.3,<4.6.0)"] -[[package]] -name = "nautobot-capacity-metrics" -version = "3.1.1" -description = "App to improve the instrumentation of Nautobot and expose additional metrics (Application Metrics, RQ Worker)." -optional = false -python-versions = "<3.13,>=3.8" -files = [ - {file = "nautobot_capacity_metrics-3.1.1-py3-none-any.whl", hash = "sha256:cba7108fc32473dd57e67e49e4c9de353837d0db63212e3dc9bed78ea6df57e6"}, - {file = "nautobot_capacity_metrics-3.1.1.tar.gz", hash = "sha256:3f54cbaca846fd89bd215829305e28877b596a4de081e785d22afd91f2ae90c2"}, -] - -[package.dependencies] -nautobot = ">=2.0.0,<3.0.0" - -[[package]] -name = "nautobot-plugin-nornir" -version = "2.1.0" -description = "Nautobot App that provides a shim layer to simplify using Nornir within other Nautobot Apps and Nautobot Jobs" -optional = false -python-versions = "<3.13,>=3.8" -files = [ - {file = "nautobot_plugin_nornir-2.1.0-py3-none-any.whl", hash = "sha256:aa50882b5fc729fb95e2d03383596a582f1b09419c8ec9c6db5f12cbb6f6ffa0"}, - {file = "nautobot_plugin_nornir-2.1.0.tar.gz", hash = "sha256:ea7ead4e52d27f349846d55bcdc00d6953f1bd03813e70a094f035a66bc863e7"}, -] - -[package.dependencies] -nautobot = ">=2.0.0,<3.0.0" -netutils = ">=1.6.0" -nornir-nautobot = ">=3.0.0,<4.0.0" - -[[package]] -name = "ncclient" -version = "0.6.16" -description = "Python library for NETCONF clients" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -files = [ - {file = "ncclient-0.6.16.tar.gz", hash = "sha256:a16a351d8c234e3bbf3495577b63c96ae4adfcdf67f2d84194313473ea65b805"}, -] - -[package.dependencies] -lxml = ">=3.3.0" -paramiko = ">=1.15.0" -setuptools = ">0.6" -six = "*" - [[package]] name = "netaddr" version = "1.3.0" @@ -2864,36 +2067,15 @@ files = [ [package.extras] nicer-shell = ["ipython"] -[[package]] -name = "netmiko" -version = "4.4.0" -description = "Multi-vendor library to simplify legacy CLI connections to network devices" -optional = false -python-versions = "<4.0,>=3.8" -files = [ - {file = "netmiko-4.4.0-py3-none-any.whl", hash = "sha256:2ff4683f013fac0f80715286c7d3250e89166aefc4421cb75d3ff483f2ebbbc0"}, - {file = "netmiko-4.4.0.tar.gz", hash = "sha256:25ff1237976aa3ff2cacf04949314638c899220a1675bd029e31b07ce20ce3b6"}, -] - -[package.dependencies] -cffi = ">=1.17.0rc1" -ntc-templates = ">=3.1.0" -paramiko = ">=2.9.5" -pyserial = ">=3.3" -pyyaml = ">=5.3" -scp = ">=0.13.6" -setuptools = ">=65.0.0" -textfsm = ">=1.1.3" - [[package]] name = "netutils" -version = "1.10.0" +version = "1.11.0" description = "Common helper functions useful in network automation." optional = false python-versions = "<4.0,>=3.8" files = [ - {file = "netutils-1.10.0-py3-none-any.whl", hash = "sha256:19b8cc3d2cf567a986f916c90f298d241af03a71c62ec6d38d6dc3395347670b"}, - {file = "netutils-1.10.0.tar.gz", hash = "sha256:f457fb85cb622e89aa0403fb2128c50986f7ce38d93a5873981727d088619793"}, + {file = "netutils-1.11.0-py3-none-any.whl", hash = "sha256:863674eb7dce2b85972d52079b4884fb30e498ccf1dd581abc28b4d69bfdf0cd"}, + {file = "netutils-1.11.0.tar.gz", hash = "sha256:1631152256db1623675d9087d4327b2f4633d294f758518742a974e868a50ae8"}, ] [package.extras] @@ -2901,223 +2083,35 @@ optionals = ["jsonschema (>=4.17.3,<5.0.0)", "napalm (>=4.0.0,<5.0.0)"] [[package]] name = "nh3" -version = "0.2.18" -description = "Python bindings to the ammonia HTML sanitization library." -optional = false -python-versions = "*" -files = [ - {file = "nh3-0.2.18-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:14c5a72e9fe82aea5fe3072116ad4661af5cf8e8ff8fc5ad3450f123e4925e86"}, - {file = "nh3-0.2.18-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:7b7c2a3c9eb1a827d42539aa64091640bd275b81e097cd1d8d82ef91ffa2e811"}, - {file = "nh3-0.2.18-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42c64511469005058cd17cc1537578eac40ae9f7200bedcfd1fc1a05f4f8c200"}, - {file = "nh3-0.2.18-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0411beb0589eacb6734f28d5497ca2ed379eafab8ad8c84b31bb5c34072b7164"}, - {file = "nh3-0.2.18-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5f36b271dae35c465ef5e9090e1fdaba4a60a56f0bb0ba03e0932a66f28b9189"}, - {file = "nh3-0.2.18-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34c03fa78e328c691f982b7c03d4423bdfd7da69cd707fe572f544cf74ac23ad"}, - {file = "nh3-0.2.18-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19aaba96e0f795bd0a6c56291495ff59364f4300d4a39b29a0abc9cb3774a84b"}, - {file = "nh3-0.2.18-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de3ceed6e661954871d6cd78b410213bdcb136f79aafe22aa7182e028b8c7307"}, - {file = "nh3-0.2.18-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6955369e4d9f48f41e3f238a9e60f9410645db7e07435e62c6a9ea6135a4907f"}, - {file = "nh3-0.2.18-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f0eca9ca8628dbb4e916ae2491d72957fdd35f7a5d326b7032a345f111ac07fe"}, - {file = "nh3-0.2.18-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:3a157ab149e591bb638a55c8c6bcb8cdb559c8b12c13a8affaba6cedfe51713a"}, - {file = "nh3-0.2.18-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:c8b3a1cebcba9b3669ed1a84cc65bf005728d2f0bc1ed2a6594a992e817f3a50"}, - {file = "nh3-0.2.18-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36c95d4b70530b320b365659bb5034341316e6a9b30f0b25fa9c9eff4c27a204"}, - {file = "nh3-0.2.18-cp37-abi3-win32.whl", hash = "sha256:a7f1b5b2c15866f2db413a3649a8fe4fd7b428ae58be2c0f6bca5eefd53ca2be"}, - {file = "nh3-0.2.18-cp37-abi3-win_amd64.whl", hash = "sha256:8ce0f819d2f1933953fca255db2471ad58184a60508f03e6285e5114b6254844"}, - {file = "nh3-0.2.18.tar.gz", hash = "sha256:94a166927e53972a9698af9542ace4e38b9de50c34352b962f4d9a7d4c927af4"}, -] - -[[package]] -name = "nornir" -version = "3.4.1" -description = "Pluggable multi-threaded framework with inventory management to help operate collections of devices" -optional = false -python-versions = ">=3.8,<4.0" -files = [ - {file = "nornir-3.4.1-py3-none-any.whl", hash = "sha256:db079cb95e3baf855530f4f40cb6ee93f93e1bf3cb74ac08180546adb1b987b8"}, - {file = "nornir-3.4.1.tar.gz", hash = "sha256:82a90a3478a3890bef8ad51b256fa966e6e4ca326cbe20a230918ef907cf68c3"}, -] - -[package.dependencies] -importlib-metadata = {version = ">=4,<5", markers = "python_version < \"3.10\""} -mypy_extensions = ">=1.0.0,<2.0.0" -"ruamel.yaml" = ">=0.17" - -[[package]] -name = "nornir-jinja2" -version = "0.2.0" -description = "Jinja2 plugins for nornir" -optional = false -python-versions = ">=3.6,<4.0" -files = [ - {file = "nornir_jinja2-0.2.0-py3-none-any.whl", hash = "sha256:0c446bec7a8492923d4eb9ca00fb327603b41bc35d5f0112843c048737b506b1"}, - {file = "nornir_jinja2-0.2.0.tar.gz", hash = "sha256:9ee5e725fe5543dcba4ec8b976804e9e88ecd356ea3b62bad97578cea0de1f75"}, -] - -[package.dependencies] -jinja2 = ">=2.11.2,<4" -nornir = ">=3,<4" - -[[package]] -name = "nornir-napalm" -version = "0.5.0" -description = "NAPALM's plugins for nornir" -optional = false -python-versions = "<4.0,>=3.8" -files = [ - {file = "nornir_napalm-0.5.0-py3-none-any.whl", hash = "sha256:1a418bf0f5e38ac65894d474f81b50787dafe0aa1965c4fbd1b86d34d4374418"}, - {file = "nornir_napalm-0.5.0.tar.gz", hash = "sha256:4c95979eebe2475e7b8516411ad8e3205d2ff30e410d1dbdce785a55033d1130"}, -] - -[package.dependencies] -napalm = ">=5,<6" -nornir = ">=3,<4" - -[[package]] -name = "nornir-nautobot" -version = "3.2.0" -description = "Nornir Nautobot" -optional = false -python-versions = "<4.0,>=3.8" -files = [ - {file = "nornir_nautobot-3.2.0-py3-none-any.whl", hash = "sha256:ed0ac258eebd2e3072f1d7a0c1f964965e7c9bf8c744290bb5ea04d5800b0ef4"}, - {file = "nornir_nautobot-3.2.0.tar.gz", hash = "sha256:087ad3f6b37112e2a4ff4be64a3b5bfbddfae22057c182e57fae7084850d3d63"}, -] - -[package.dependencies] -httpx = ">=0.23.0,<=0.27.0" -netutils = ">=1.6.0,<2.0.0" -nornir = ">=3.0.0,<4.0.0" -nornir-jinja2 = ">=0.2.0,<0.3.0" -nornir-napalm = ">=0.4.0,<1.0.0" -nornir-netmiko = ">=1,<2" -nornir-utils = ">=0,<1" -pynautobot = ">=2.0.2" -requests = ">=2.25.1,<3.0.0" - -[package.extras] -mikrotik-driver = ["routeros-api (>=0.17.0,<0.18.0)"] - -[[package]] -name = "nornir-netmiko" -version = "1.0.1" -description = "Netmiko's plugins for Nornir" -optional = false -python-versions = ">=3.8,<4.0" -files = [ - {file = "nornir_netmiko-1.0.1-py3-none-any.whl", hash = "sha256:eaee2944ad386b40c0719e8ac393ac63d531f44fb9a07d660bae7de430f12834"}, - {file = "nornir_netmiko-1.0.1.tar.gz", hash = "sha256:498546df001e0e499f10c5646d1356e361ccbb165b1335b89cfe8f19765e24d7"}, -] - -[package.dependencies] -netmiko = ">=4.0.0,<5.0.0" - -[[package]] -name = "nornir-utils" -version = "0.2.0" -description = "Collection of plugins and functions for nornir that don't require external dependencies" -optional = false -python-versions = ">=3.6.2,<4.0.0" -files = [ - {file = "nornir_utils-0.2.0-py3-none-any.whl", hash = "sha256:b4c430793a74f03affd5ff2d90abc8c67a28c7ff325f48e3a01a9a44ec71b844"}, - {file = "nornir_utils-0.2.0.tar.gz", hash = "sha256:4de6aaa35e5c1a98e1c84db84a008b0b1e974dc65d88484f2dcea3e30c95fbc2"}, -] - -[package.dependencies] -colorama = ">=0.4.3,<0.5.0" -nornir = ">=3,<4" - -[[package]] -name = "ntc-templates" -version = "7.4.0" -description = "TextFSM Templates for Network Devices, and Python wrapper for TextFSM's CliTable." -optional = false -python-versions = "<4.0,>=3.8" -files = [ - {file = "ntc_templates-7.4.0-py3-none-any.whl", hash = "sha256:e113991ab266590a76b021d947a6e870cc46624996492402e0b6f8d691289cbe"}, - {file = "ntc_templates-7.4.0.tar.gz", hash = "sha256:898948948983237a0cc739b55d19890148badc0f6521f85245bca9e7d953f4e2"}, -] - -[package.dependencies] -textfsm = ">=1.1.0,<2.0.0" - -[[package]] -name = "numpy" -version = "1.24.4" -description = "Fundamental package for array computing in Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "numpy-1.24.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0bfb52d2169d58c1cdb8cc1f16989101639b34c7d3ce60ed70b19c63eba0b64"}, - {file = "numpy-1.24.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed094d4f0c177b1b8e7aa9cba7d6ceed51c0e569a5318ac0ca9a090680a6a1b1"}, - {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79fc682a374c4a8ed08b331bef9c5f582585d1048fa6d80bc6c35bc384eee9b4"}, - {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ffe43c74893dbf38c2b0a1f5428760a1a9c98285553c89e12d70a96a7f3a4d6"}, - {file = "numpy-1.24.4-cp310-cp310-win32.whl", hash = "sha256:4c21decb6ea94057331e111a5bed9a79d335658c27ce2adb580fb4d54f2ad9bc"}, - {file = "numpy-1.24.4-cp310-cp310-win_amd64.whl", hash = "sha256:b4bea75e47d9586d31e892a7401f76e909712a0fd510f58f5337bea9572c571e"}, - {file = "numpy-1.24.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f136bab9c2cfd8da131132c2cf6cc27331dd6fae65f95f69dcd4ae3c3639c810"}, - {file = "numpy-1.24.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2926dac25b313635e4d6cf4dc4e51c8c0ebfed60b801c799ffc4c32bf3d1254"}, - {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:222e40d0e2548690405b0b3c7b21d1169117391c2e82c378467ef9ab4c8f0da7"}, - {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7215847ce88a85ce39baf9e89070cb860c98fdddacbaa6c0da3ffb31b3350bd5"}, - {file = "numpy-1.24.4-cp311-cp311-win32.whl", hash = "sha256:4979217d7de511a8d57f4b4b5b2b965f707768440c17cb70fbf254c4b225238d"}, - {file = "numpy-1.24.4-cp311-cp311-win_amd64.whl", hash = "sha256:b7b1fc9864d7d39e28f41d089bfd6353cb5f27ecd9905348c24187a768c79694"}, - {file = "numpy-1.24.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1452241c290f3e2a312c137a9999cdbf63f78864d63c79039bda65ee86943f61"}, - {file = "numpy-1.24.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:04640dab83f7c6c85abf9cd729c5b65f1ebd0ccf9de90b270cd61935eef0197f"}, - {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5425b114831d1e77e4b5d812b69d11d962e104095a5b9c3b641a218abcc050e"}, - {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd80e219fd4c71fc3699fc1dadac5dcf4fd882bfc6f7ec53d30fa197b8ee22dc"}, - {file = "numpy-1.24.4-cp38-cp38-win32.whl", hash = "sha256:4602244f345453db537be5314d3983dbf5834a9701b7723ec28923e2889e0bb2"}, - {file = "numpy-1.24.4-cp38-cp38-win_amd64.whl", hash = "sha256:692f2e0f55794943c5bfff12b3f56f99af76f902fc47487bdfe97856de51a706"}, - {file = "numpy-1.24.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2541312fbf09977f3b3ad449c4e5f4bb55d0dbf79226d7724211acc905049400"}, - {file = "numpy-1.24.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9667575fb6d13c95f1b36aca12c5ee3356bf001b714fc354eb5465ce1609e62f"}, - {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a86ed21e4f87050382c7bc96571755193c4c1392490744ac73d660e8f564a9"}, - {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d11efb4dbecbdf22508d55e48d9c8384db795e1b7b51ea735289ff96613ff74d"}, - {file = "numpy-1.24.4-cp39-cp39-win32.whl", hash = "sha256:6620c0acd41dbcb368610bb2f4d83145674040025e5536954782467100aa8835"}, - {file = "numpy-1.24.4-cp39-cp39-win_amd64.whl", hash = "sha256:befe2bf740fd8373cf56149a5c23a0f601e82869598d41f8e188a0e9869926f8"}, - {file = "numpy-1.24.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:31f13e25b4e304632a4619d0e0777662c2ffea99fcae2029556b17d8ff958aef"}, - {file = "numpy-1.24.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95f7ac6540e95bc440ad77f56e520da5bf877f87dca58bd095288dce8940532a"}, - {file = "numpy-1.24.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e98f220aa76ca2a977fe435f5b04d7b3470c0a2e6312907b37ba6068f26787f2"}, - {file = "numpy-1.24.4.tar.gz", hash = "sha256:80f5e3a4e498641401868df4208b74581206afbee7cf7b8329daae82676d9463"}, -] - -[[package]] -name = "numpy" -version = "1.26.4" -description = "Fundamental package for array computing in Python" -optional = false -python-versions = ">=3.9" -files = [ - {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, - {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, - {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, - {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, - {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, - {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, - {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, - {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, - {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, - {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, - {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, - {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, - {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, - {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, - {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, - {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, - {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, - {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, - {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, - {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, - {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, - {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, - {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, - {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, - {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, - {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, - {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, - {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, - {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, - {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, - {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, - {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, - {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, - {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, - {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, - {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, +version = "0.2.20" +description = "Python binding to Ammonia HTML sanitizer Rust crate" +optional = false +python-versions = ">=3.8" +files = [ + {file = "nh3-0.2.20-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e1061a4ab6681f6bdf72b110eea0c4e1379d57c9de937db3be4202f7ad6043db"}, + {file = "nh3-0.2.20-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb4254b1dac4a1ee49919a5b3f1caf9803ea8dada1816d9e8289e63d3cd0dd9a"}, + {file = "nh3-0.2.20-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0ae9cbd713524cdb81e64663d0d6aae26f678db9f2cd9db0bf162606f1f9f20c"}, + {file = "nh3-0.2.20-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e1f7370b4e14cc03f5ae141ef30a1caf81fa5787711f80be9081418dd9eb79d2"}, + {file = "nh3-0.2.20-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:ac4d27dc836a476efffc6eb661994426b8b805c951b29c9cf2ff36bc9ad58bc5"}, + {file = "nh3-0.2.20-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4fd2e9248725ebcedac3997a8d3da0d90a12a28c9179c6ba51f1658938ac30d0"}, + {file = "nh3-0.2.20-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f7d564871833ddbe54df3aa59053b1110729d3a800cb7628ae8f42adb3d75208"}, + {file = "nh3-0.2.20-cp313-cp313t-win32.whl", hash = "sha256:d2a176fd4306b6f0f178a3f67fac91bd97a3a8d8fafb771c9b9ef675ba5c8886"}, + {file = "nh3-0.2.20-cp313-cp313t-win_amd64.whl", hash = "sha256:6ed834c68452a600f517dd3e1534dbfaff1f67f98899fecf139a055a25d99150"}, + {file = "nh3-0.2.20-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:76e2f603b30c02ff6456b233a83fc377dedab6a50947b04e960a6b905637b776"}, + {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:181063c581defe683bd4bb78188ac9936d208aebbc74c7f7c16b6a32ae2ebb38"}, + {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:231addb7643c952cd6d71f1c8702d703f8fe34afcb20becb3efb319a501a12d7"}, + {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1b9a8340a0aab991c68a5ca938d35ef4a8a3f4bf1b455da8855a40bee1fa0ace"}, + {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10317cd96fe4bbd4eb6b95f3920b71c902157ad44fed103fdcde43e3b8ee8be6"}, + {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8698db4c04b140800d1a1cd3067fda399e36e1e2b8fc1fe04292a907350a3e9b"}, + {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3eb04b9c3deb13c3a375ea39fd4a3c00d1f92e8fb2349f25f1e3e4506751774b"}, + {file = "nh3-0.2.20-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:92f3f1c4f47a2c6f3ca7317b1d5ced05bd29556a75d3a4e2715652ae9d15c05d"}, + {file = "nh3-0.2.20-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ddefa9fd6794a87e37d05827d299d4b53a3ec6f23258101907b96029bfef138a"}, + {file = "nh3-0.2.20-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ce3731c8f217685d33d9268362e5b4f770914e922bba94d368ab244a59a6c397"}, + {file = "nh3-0.2.20-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:09f037c02fc2c43b211ff1523de32801dcfb0918648d8e651c36ef890f1731ec"}, + {file = "nh3-0.2.20-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:813f1c8012dd64c990514b795508abb90789334f76a561fa0fd4ca32d2275330"}, + {file = "nh3-0.2.20-cp38-abi3-win32.whl", hash = "sha256:47b2946c0e13057855209daeffb45dc910bd0c55daf10190bb0b4b60e2999784"}, + {file = "nh3-0.2.20-cp38-abi3-win_amd64.whl", hash = "sha256:da87573f03084edae8eb87cfe811ec338606288f81d333c07d2a9a0b9b976c0b"}, + {file = "nh3-0.2.20.tar.gz", hash = "sha256:9705c42d7ff88a0bea546c82d7fe5e59135e3d3f057e485394f491248a1f8ed5"}, ] [[package]] @@ -3136,29 +2130,15 @@ rsa = ["cryptography (>=3.0.0)"] signals = ["blinker (>=1.4.0)"] signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] -[[package]] -name = "ordered-set" -version = "4.1.0" -description = "An OrderedSet is a custom MutableSet that remembers its order, so that every" -optional = false -python-versions = ">=3.7" -files = [ - {file = "ordered-set-4.1.0.tar.gz", hash = "sha256:694a8e44c87657c59292ede72891eb91d34131f6531463aab3009191c77364a8"}, - {file = "ordered_set-4.1.0-py3-none-any.whl", hash = "sha256:046e1132c71fcf3330438a539928932caf51ddbc582496833e23de611de14562"}, -] - -[package.extras] -dev = ["black", "mypy", "pytest"] - [[package]] name = "packaging" -version = "23.2" +version = "24.2" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, - {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] [[package]] @@ -3176,27 +2156,6 @@ files = [ dev = ["pytest", "tox"] lint = ["black"] -[[package]] -name = "paramiko" -version = "3.5.0" -description = "SSH2 protocol library" -optional = false -python-versions = ">=3.6" -files = [ - {file = "paramiko-3.5.0-py3-none-any.whl", hash = "sha256:1fedf06b085359051cd7d0d270cebe19e755a8a921cc2ddbfa647fb0cd7d68f9"}, - {file = "paramiko-3.5.0.tar.gz", hash = "sha256:ad11e540da4f55cedda52931f1a3f812a8238a7af7f62a60de538cd80bb28124"}, -] - -[package.dependencies] -bcrypt = ">=3.2" -cryptography = ">=3.3" -pynacl = ">=1.5" - -[package.extras] -all = ["gssapi (>=1.4.1)", "invoke (>=2.0)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8)"] -gssapi = ["gssapi (>=1.4.1)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8)"] -invoke = ["invoke (>=2.0)"] - [[package]] name = "parso" version = "0.8.4" @@ -3459,6 +2418,7 @@ files = [ {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909"}, {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1"}, {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142"}, {file = "psycopg2_binary-2.9.10-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:eb09aa7f9cecb45027683bb55aebaaf45a0df8bf6de68801a6afdc7947bb09d4"}, {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b73d6d7f0ccdad7bc43e6d34273f70d587ef62f824d7261c4ae9b8b1b6af90e8"}, {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce5ab4bf46a211a8e924d307c1b1fcda82368586a19d0a24f8ae166f5c784864"}, @@ -3528,32 +2488,15 @@ files = [ {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] -[[package]] -name = "pyeapi" -version = "1.0.4" -description = "Python Client for eAPI" -optional = false -python-versions = "*" -files = [ - {file = "pyeapi-1.0.4.tar.gz", hash = "sha256:05920677246823cd3dddf7d4d0f831fbc86fd416f356706a03bc56a291d78f3d"}, -] - -[package.dependencies] -netaddr = "*" - -[package.extras] -dev = ["check-manifest", "pep8", "pyflakes", "twine"] -test = ["coverage"] - [[package]] name = "pygments" -version = "2.18.0" +version = "2.19.1" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" files = [ - {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, - {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, + {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, + {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, ] [package.extras] @@ -3656,13 +2599,13 @@ pylint = ">=1.7" [[package]] name = "pymdown-extensions" -version = "10.12" +version = "10.14" description = "Extension pack for Python Markdown." optional = false python-versions = ">=3.8" files = [ - {file = "pymdown_extensions-10.12-py3-none-any.whl", hash = "sha256:49f81412242d3527b8b4967b990df395c89563043bc51a3d2d7d500e52123b77"}, - {file = "pymdown_extensions-10.12.tar.gz", hash = "sha256:b0ee1e0b2bef1071a47891ab17003bfe5bf824a398e13f49f8ed653b699369a7"}, + {file = "pymdown_extensions-10.14-py3-none-any.whl", hash = "sha256:202481f716cc8250e4be8fce997781ebf7917701b59652458ee47f2401f818b5"}, + {file = "pymdown_extensions-10.14.tar.gz", hash = "sha256:741bd7c4ff961ba40b7528d32284c53bc436b8b1645e8e37c3e57770b8700a34"}, ] [package.dependencies] @@ -3670,77 +2613,7 @@ markdown = ">=3.6" pyyaml = "*" [package.extras] -extra = ["pygments (>=2.12)"] - -[[package]] -name = "pynacl" -version = "1.5.0" -description = "Python binding to the Networking and Cryptography (NaCl) library" -optional = false -python-versions = ">=3.6" -files = [ - {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, - {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"}, - {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"}, - {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d"}, - {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"}, - {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"}, - {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"}, - {file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"}, - {file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"}, - {file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"}, -] - -[package.dependencies] -cffi = ">=1.4.1" - -[package.extras] -docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] -tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] - -[[package]] -name = "pynautobot" -version = "2.1.1" -description = "Nautobot API client library" -optional = false -python-versions = "<4.0,>=3.8" -files = [ - {file = "pynautobot-2.1.1-py3-none-any.whl", hash = "sha256:bcf56fee4733942a87dd07f956418f67580f45d08e5296c8fa3d11316c4ca419"}, - {file = "pynautobot-2.1.1.tar.gz", hash = "sha256:f01907a519689dc842f909f850737f68b53953818c97380a8101406d37e49d1b"}, -] - -[package.dependencies] -packaging = ">=23.2,<24.0" -requests = ">=2.30.0,<3.0.0" -urllib3 = ">=1.21.1,<1.27" - -[[package]] -name = "pyparsing" -version = "3.1.4" -description = "pyparsing module - Classes and methods to define and execute parsing grammars" -optional = false -python-versions = ">=3.6.8" -files = [ - {file = "pyparsing-3.1.4-py3-none-any.whl", hash = "sha256:a6a7ee4235a3f944aa1fa2249307708f893fe5717dc603503c6c7969c070fb7c"}, - {file = "pyparsing-3.1.4.tar.gz", hash = "sha256:f86ec8d1a83f11977c9a6ea7598e8c27fc5cddfa5b07ea2241edbbde1d7bc032"}, -] - -[package.extras] -diagrams = ["jinja2", "railroad-diagrams"] - -[[package]] -name = "pyserial" -version = "3.5" -description = "Python Serial Port Extension" -optional = false -python-versions = "*" -files = [ - {file = "pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0"}, - {file = "pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb"}, -] - -[package.extras] -cp2110 = ["hidapi"] +extra = ["pygments (>=2.19.1)"] [[package]] name = "python-crontab" @@ -3976,13 +2849,13 @@ pyyaml = "*" [[package]] name = "redis" -version = "5.2.0" +version = "5.2.1" description = "Python client for Redis database and key-value store" optional = false python-versions = ">=3.8" files = [ - {file = "redis-5.2.0-py3-none-any.whl", hash = "sha256:ae174f2bb3b1bf2b09d54bf3e51fbc1469cf6c10aa03e21141f51969801a7897"}, - {file = "redis-5.2.0.tar.gz", hash = "sha256:0b1087665a771b1ff2e003aa5bdd354f15a70c9e25d5a7dbf9c722c16528a7b0"}, + {file = "redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4"}, + {file = "redis-5.2.1.tar.gz", hash = "sha256:16f2e22dff21d5125e8481515e386711a34cbec50f0e44413dd7d9c060a54e0f"}, ] [package.dependencies] @@ -4261,83 +3134,6 @@ files = [ {file = "rpds_py-0.20.1.tar.gz", hash = "sha256:e1791c4aabd117653530dccd24108fa03cc6baf21f58b950d0a73c3b3b29a350"}, ] -[[package]] -name = "ruamel-yaml" -version = "0.18.6" -description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" -optional = false -python-versions = ">=3.7" -files = [ - {file = "ruamel.yaml-0.18.6-py3-none-any.whl", hash = "sha256:57b53ba33def16c4f3d807c0ccbc00f8a6081827e81ba2491691b76882d0c636"}, - {file = "ruamel.yaml-0.18.6.tar.gz", hash = "sha256:8b27e6a217e786c6fbe5634d8f3f11bc63e0f80f6a5890f28863d9c45aac311b"}, -] - -[package.dependencies] -"ruamel.yaml.clib" = {version = ">=0.2.7", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.13\""} - -[package.extras] -docs = ["mercurial (>5.7)", "ryd"] -jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] - -[[package]] -name = "ruamel-yaml-clib" -version = "0.2.8" -description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" -optional = false -python-versions = ">=3.6" -files = [ - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b42169467c42b692c19cf539c38d4602069d8c1505e97b86387fcf7afb766e1d"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:07238db9cbdf8fc1e9de2489a4f68474e70dffcb32232db7c08fa61ca0c7c462"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fff3573c2db359f091e1589c3d7c5fc2f86f5bdb6f24252c2d8e539d4e45f412"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:aa2267c6a303eb483de8d02db2871afb5c5fc15618d894300b88958f729ad74f"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:840f0c7f194986a63d2c2465ca63af8ccbbc90ab1c6001b1978f05119b5e7334"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:024cfe1fc7c7f4e1aff4a81e718109e13409767e4f871443cbff3dba3578203d"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win32.whl", hash = "sha256:c69212f63169ec1cfc9bb44723bf2917cbbd8f6191a00ef3410f5a7fe300722d"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win_amd64.whl", hash = "sha256:cabddb8d8ead485e255fe80429f833172b4cadf99274db39abc080e068cbcc31"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bef08cd86169d9eafb3ccb0a39edb11d8e25f3dae2b28f5c52fd997521133069"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:b16420e621d26fdfa949a8b4b47ade8810c56002f5389970db4ddda51dbff248"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:25c515e350e5b739842fc3228d662413ef28f295791af5e5110b543cf0b57d9b"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_24_aarch64.whl", hash = "sha256:1707814f0d9791df063f8c19bb51b0d1278b8e9a2353abbb676c2f685dee6afe"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:46d378daaac94f454b3a0e3d8d78cafd78a026b1d71443f4966c696b48a6d899"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:09b055c05697b38ecacb7ac50bdab2240bfca1a0c4872b0fd309bb07dc9aa3a9"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win32.whl", hash = "sha256:53a300ed9cea38cf5a2a9b069058137c2ca1ce658a874b79baceb8f892f915a7"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win_amd64.whl", hash = "sha256:c2a72e9109ea74e511e29032f3b670835f8a59bbdc9ce692c5b4ed91ccf1eedb"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ebc06178e8821efc9692ea7544aa5644217358490145629914d8020042c24aa1"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:edaef1c1200c4b4cb914583150dcaa3bc30e592e907c01117c08b13a07255ec2"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d176b57452ab5b7028ac47e7b3cf644bcfdc8cacfecf7e71759f7f51a59e5c92"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_24_aarch64.whl", hash = "sha256:1dc67314e7e1086c9fdf2680b7b6c2be1c0d8e3a8279f2e993ca2a7545fecf62"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3213ece08ea033eb159ac52ae052a4899b56ecc124bb80020d9bbceeb50258e9"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aab7fd643f71d7946f2ee58cc88c9b7bfc97debd71dcc93e03e2d174628e7e2d"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-win32.whl", hash = "sha256:5c365d91c88390c8d0a8545df0b5857172824b1c604e867161e6b3d59a827eaa"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-win_amd64.whl", hash = "sha256:1758ce7d8e1a29d23de54a16ae867abd370f01b5a69e1a3ba75223eaa3ca1a1b"}, - {file = "ruamel.yaml.clib-0.2.8-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a5aa27bad2bb83670b71683aae140a1f52b0857a2deff56ad3f6c13a017a26ed"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c58ecd827313af6864893e7af0a3bb85fd529f862b6adbefe14643947cfe2942"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-macosx_12_0_arm64.whl", hash = "sha256:f481f16baec5290e45aebdc2a5168ebc6d35189ae6fea7a58787613a25f6e875"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:77159f5d5b5c14f7c34073862a6b7d34944075d9f93e681638f6d753606c6ce6"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7f67a1ee819dc4562d444bbafb135832b0b909f81cc90f7aa00260968c9ca1b3"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4ecbf9c3e19f9562c7fdd462e8d18dd902a47ca046a2e64dba80699f0b6c09b7"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:87ea5ff66d8064301a154b3933ae406b0863402a799b16e4a1d24d9fbbcbe0d3"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-win32.whl", hash = "sha256:75e1ed13e1f9de23c5607fe6bd1aeaae21e523b32d83bb33918245361e9cc51b"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-win_amd64.whl", hash = "sha256:3f215c5daf6a9d7bbed4a0a4f760f3113b10e82ff4c5c44bec20a68c8014f675"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1b617618914cb00bf5c34d4357c37aa15183fa229b24767259657746c9077615"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:a6a9ffd280b71ad062eae53ac1659ad86a17f59a0fdc7699fd9be40525153337"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:305889baa4043a09e5b76f8e2a51d4ffba44259f6b4c72dec8ca56207d9c6fe1"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:700e4ebb569e59e16a976857c8798aee258dceac7c7d6b50cab63e080058df91"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e2b4c44b60eadec492926a7270abb100ef9f72798e18743939bdbf037aab8c28"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e79e5db08739731b0ce4850bed599235d601701d5694c36570a99a0c5ca41a9d"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-win32.whl", hash = "sha256:955eae71ac26c1ab35924203fda6220f84dce57d6d7884f189743e2abe3a9fbe"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-win_amd64.whl", hash = "sha256:56f4252222c067b4ce51ae12cbac231bce32aee1d33fbfc9d17e5b8d6966c312"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:03d1162b6d1df1caa3a4bd27aa51ce17c9afc2046c31b0ad60a0a96ec22f8001"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:bba64af9fa9cebe325a62fa398760f5c7206b215201b0ec825005f1b18b9bccf"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:a1a45e0bb052edf6a1d3a93baef85319733a888363938e1fc9924cb00c8df24c"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:da09ad1c359a728e112d60116f626cc9f29730ff3e0e7db72b9a2dbc2e4beed5"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:184565012b60405d93838167f425713180b949e9d8dd0bbc7b49f074407c5a8b"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a75879bacf2c987c003368cf14bed0ffe99e8e85acfa6c0bfffc21a090f16880"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-win32.whl", hash = "sha256:84b554931e932c46f94ab306913ad7e11bba988104c5cff26d90d03f68258cd5"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-win_amd64.whl", hash = "sha256:25ac8c08322002b06fa1d49d1646181f0b2c72f5cbc15a85e80b4c30a544bb15"}, - {file = "ruamel.yaml.clib-0.2.8.tar.gz", hash = "sha256:beb2e0404003de9a4cab9753a8805a8fe9320ee6673136ed7f04255fe60bb512"}, -] - [[package]] name = "ruff" version = "0.5.5" @@ -4375,20 +3171,6 @@ files = [ {file = "Rx-1.6.3.tar.gz", hash = "sha256:ca71b65d0fc0603a3b5cfaa9e33f5ba81e4aae10a58491133595088d7734b2da"}, ] -[[package]] -name = "scp" -version = "0.15.0" -description = "scp module for paramiko" -optional = false -python-versions = "*" -files = [ - {file = "scp-0.15.0-py2.py3-none-any.whl", hash = "sha256:9e7f721e5ac563c33eb0831d0f949c6342f1c28c3bdc3b02f39d77b5ea20df7e"}, - {file = "scp-0.15.0.tar.gz", hash = "sha256:f1b22e9932123ccf17eebf19e0953c6e9148f589f93d91b872941a696305c83f"}, -] - -[package.dependencies] -paramiko = "*" - [[package]] name = "setuptools" version = "75.3.0" @@ -4426,35 +3208,24 @@ testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", [[package]] name = "six" -version = "1.16.0" +version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] [[package]] name = "smmap" -version = "5.0.1" +version = "5.0.2" description = "A pure Python implementation of a sliding window memory map manager" optional = false python-versions = ">=3.7" files = [ - {file = "smmap-5.0.1-py3-none-any.whl", hash = "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da"}, - {file = "smmap-5.0.1.tar.gz", hash = "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62"}, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -description = "Sniff out which async library your code is running under" -optional = false -python-versions = ">=3.7" -files = [ - {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, - {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, + {file = "smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e"}, + {file = "smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5"}, ] [[package]] @@ -4500,13 +3271,13 @@ saml = ["python3-saml (>=1.5.0)"] [[package]] name = "sqlparse" -version = "0.5.2" +version = "0.5.3" description = "A non-validating SQL parser." optional = false python-versions = ">=3.8" files = [ - {file = "sqlparse-0.5.2-py3-none-any.whl", hash = "sha256:e99bc85c78160918c3e1d9230834ab8d80fc06c59d03f8db2618f65f65dda55e"}, - {file = "sqlparse-0.5.2.tar.gz", hash = "sha256:9e37b35e16d1cc652a2545f0997c1deb23ea28fa1f3eefe609eee3063c3b105f"}, + {file = "sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca"}, + {file = "sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272"}, ] [package.extras] @@ -4571,21 +3342,6 @@ files = [ {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, ] -[[package]] -name = "textfsm" -version = "1.1.3" -description = "Python module for parsing semi-structured text into python tables." -optional = false -python-versions = "*" -files = [ - {file = "textfsm-1.1.3-py2.py3-none-any.whl", hash = "sha256:dcbeebc6a6137bed561c71a56344d752e6dbc04ae5ea309252cb70fb97ccc9cd"}, - {file = "textfsm-1.1.3.tar.gz", hash = "sha256:577ef278a9237f5341ae9b682947cefa4a2c1b24dbe486f94f2c95addc6504b5"}, -] - -[package.dependencies] -future = "*" -six = "*" - [[package]] name = "to-json-schema" version = "1.0.1" @@ -4700,56 +3456,6 @@ files = [ docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"] -[[package]] -name = "transitions" -version = "0.9.2" -description = "A lightweight, object-oriented Python state machine implementation with many extensions." -optional = false -python-versions = "*" -files = [ - {file = "transitions-0.9.2-py2.py3-none-any.whl", hash = "sha256:f7b40c9b4a93869f36c4d1c33809aeb18cdeeb065fd1adba018ee39c3db216f3"}, - {file = "transitions-0.9.2.tar.gz", hash = "sha256:2f8490dbdbd419366cef1516032ab06d07ccb5839ef54905e842a472692d4204"}, -] - -[package.dependencies] -six = "*" - -[package.extras] -diagrams = ["pygraphviz"] -test = ["pytest"] - -[[package]] -name = "ttp" -version = "0.9.5" -description = "Template Text Parser" -optional = false -python-versions = ">=2.7,<4.0" -files = [ - {file = "ttp-0.9.5-py2.py3-none-any.whl", hash = "sha256:2c9fcf560b3f696e9fdd3554dc8e4622cbb10cac1d4fca13a7cf608c4a7fd137"}, - {file = "ttp-0.9.5.tar.gz", hash = "sha256:234414f4d3039d2d1cde09993f89f8db1b34d447f76c6a402555cefac2e59c4e"}, -] - -[package.extras] -docs = ["Sphinx (==4.3.0)", "readthedocs-sphinx-search (==0.1.1)", "sphinx_rtd_theme (==1.0.0)", "sphinxcontrib-applehelp (==1.0.1)", "sphinxcontrib-devhelp (==1.0.1)", "sphinxcontrib-htmlhelp (==2.0.0)", "sphinxcontrib-jsmath (==1.0.1)", "sphinxcontrib-napoleon (==0.7)", "sphinxcontrib-qthelp (==1.0.2)", "sphinxcontrib-serializinghtml (==1.1.5)", "sphinxcontrib-spelling (==7.2.1)"] -full = ["cerberus (>=1.3.0,<1.4.0)", "deepdiff (>=5.8.0,<5.9.0)", "jinja2 (>=3.0.0,<3.1.0)", "n2g (>=0.2.0,<0.3.0)", "openpyxl (>=3.0.0,<3.1.0)", "pyyaml (==6.0)", "tabulate (>=0.8.0,<0.9.0)", "ttp_templates (<1.0.0)", "yangson (>=1.4.0,<1.5.0)"] - -[[package]] -name = "ttp-templates" -version = "0.3.7" -description = "Template Text Parser Templates collections" -optional = false -python-versions = "<4.0,>=3.6" -files = [ - {file = "ttp_templates-0.3.7-py3-none-any.whl", hash = "sha256:2328304fb4c957ee60db6f301143e8a4556b22a12b3e2f30511e8ef97fc78f7e"}, - {file = "ttp_templates-0.3.7.tar.gz", hash = "sha256:f9103041a3683a0cb3811609ad990f679beadfc9a92c3e3fa05d6037414ad2bf"}, -] - -[package.dependencies] -ttp = ">=0.6.0" - -[package.extras] -docs = ["mkdocs (==1.2.4)", "mkdocs-material (==7.2.2)", "mkdocs-material-extensions (==1.0.1)", "mkdocstrings[python] (>=0.18.0,<0.19.0)", "pygments (==2.11)", "pymdown-extensions (==9.3)"] - [[package]] name = "typing-extensions" version = "4.12.2" @@ -4785,19 +3491,20 @@ files = [ [[package]] name = "urllib3" -version = "1.26.20" +version = "2.2.3" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +python-versions = ">=3.8" files = [ - {file = "urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e"}, - {file = "urllib3-1.26.20.tar.gz", hash = "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32"}, + {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, + {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, ] [package.extras] -brotli = ["brotli (==1.0.9)", "brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] -secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] -socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] [[package]] name = "vine" @@ -4956,24 +3663,6 @@ files = [ {file = "wrapt-1.17.0.tar.gz", hash = "sha256:16187aa2317c731170a88ef35e8937ae0f533c402872c1ee5e6d079fcf320801"}, ] -[[package]] -name = "xmldiff" -version = "2.7.0" -description = "Creates diffs of XML files" -optional = false -python-versions = ">=3.8" -files = [ - {file = "xmldiff-2.7.0-py3-none-any.whl", hash = "sha256:c8020e6aa4aa9fa13c72e5bf0eeafd0be998b0ab55d78b008abc75fbfebaca27"}, - {file = "xmldiff-2.7.0.tar.gz", hash = "sha256:c0910b1f800366dd7ec62923e5d06e8b06a1bd9120569a1c27f4f2446b9c68a2"}, -] - -[package.dependencies] -lxml = ">=3.1.0" -setuptools = "*" - -[package.extras] -devenv = ["black", "coverage", "flake8", "zest.releaser[recommended]"] - [[package]] name = "yamllint" version = "1.35.1" @@ -4992,20 +3681,6 @@ pyyaml = "*" [package.extras] dev = ["doc8", "flake8", "flake8-import-order", "rstcheck[sphinx]", "sphinx"] -[[package]] -name = "yamlordereddictloader" -version = "0.4.2" -description = "YAML loader and dumper for PyYAML allowing to keep keys order." -optional = false -python-versions = "*" -files = [ - {file = "yamlordereddictloader-0.4.2-py3-none-any.whl", hash = "sha256:dc048adb67026786cd24119bd71241f35bc8b0fd37d24b415c37bbc8049f9cd7"}, - {file = "yamlordereddictloader-0.4.2.tar.gz", hash = "sha256:36af2f6210fcff5da4fc4c12e1d815f973dceb41044e795e1f06115d634bca13"}, -] - -[package.dependencies] -pyyaml = "*" - [[package]] name = "zipp" version = "3.20.2" @@ -5031,4 +3706,4 @@ all = [] [metadata] lock-version = "2.0" python-versions = ">=3.8,<3.13" -content-hash = "21528eed38f42921aa5bc3f5a9a8a1f07446965f212289876cbebd0d00e9033a" +content-hash = "f2041fa5a92502d80e47c6a6e762583dceabb072a9c36f52abed52a7ef2da478" diff --git a/tasks.py b/tasks.py index f516af8a6..08c63e9c5 100644 --- a/tasks.py +++ b/tasks.py @@ -149,7 +149,7 @@ def docker_compose(context, command, **kwargs): return context.run(compose_command, env=build_env, **kwargs) -def run_command(context, command, **kwargs): +def run_command(context, command, service="nautobot", **kwargs): """Wrapper to run a command locally or inside the nautobot container.""" if is_truthy(context.nautobot_golden_config.local): if "command_env" in kwargs: @@ -159,7 +159,7 @@ def run_command(context, command, **kwargs): } return context.run(command, **kwargs) else: - # Check if nautobot is running, no need to start another nautobot container to run a command + # Check if service is running, no need to start another container to run a command docker_compose_status = "ps --services --filter status=running" results = docker_compose(context, docker_compose_status, hide="out") @@ -169,10 +169,10 @@ def run_command(context, command, **kwargs): for key, value in command_env.items(): command_env_args += f' --env="{key}={value}"' - if "nautobot" in results.stdout: - compose_command = f"exec{command_env_args} nautobot {command}" + if service in results.stdout: + compose_command = f"exec{command_env_args} {service} {command}" else: - compose_command = f"run{command_env_args} --rm --entrypoint='{command}' nautobot" + compose_command = f"run{command_env_args} --rm --entrypoint='{command}' {service}" pty = kwargs.pop("pty", True) @@ -411,10 +411,14 @@ def shell_plus(context): run_command(context, command) -@task -def cli(context): - """Launch a bash shell inside the Nautobot container.""" - run_command(context, "bash") +@task( + help={ + "service": "Docker compose service name to launch cli in (default: nautobot).", + } +) +def cli(context, service="nautobot"): + """Launch a bash shell inside the container.""" + run_command(context, "bash", service=service) @task( @@ -736,7 +740,8 @@ def pylint(context): else: print("No migrations directory found, skipping migrations checks.") - raise Exit(code=exit_code) + if exit_code != 0: + raise Exit(code=exit_code) @task(aliases=("a",)) @@ -780,7 +785,8 @@ def ruff(context, action=None, target=None, fix=False, output_format="concise"): if not run_command(context, command, warn=True): exit_code = 1 - raise Exit(code=exit_code) + if exit_code != 0: + raise Exit(code=exit_code) @task From fff9b7630b14f7a05813e7f0cf4cd180d728b558 Mon Sep 17 00:00:00 2001 From: Stephen Kiely Date: Wed, 8 Jan 2025 09:52:44 -0600 Subject: [PATCH 2/2] Back out some of the drift manager changes. --- nautobot_golden_config/api/serializers.py | 122 +- nautobot_golden_config/api/urls.py | 27 +- nautobot_golden_config/api/views.py | 363 ++++- nautobot_golden_config/filters.py | 447 +++++- nautobot_golden_config/forms.py | 619 +++++++- nautobot_golden_config/models.py | 844 +++++++++- nautobot_golden_config/navigation.py | 156 +- nautobot_golden_config/tables.py | 528 ++++++- .../compliancefeature_retrieve.html | 33 +- nautobot_golden_config/tests/fixtures.py | 10 - .../tests/test_api_views.py | 27 - .../tests/test_filter_compliancefeature.py | 28 - .../tests/test_form_compliancefeature.py | 33 - .../tests/test_model_compliancefeature.py | 22 - nautobot_golden_config/tests/test_views.py | 405 ++++- nautobot_golden_config/urls.py | 16 +- nautobot_golden_config/views.py | 597 ++++++- poetry.lock | 1389 ++++++++++++++++- 18 files changed, 5398 insertions(+), 268 deletions(-) delete mode 100644 nautobot_golden_config/tests/fixtures.py delete mode 100644 nautobot_golden_config/tests/test_api_views.py delete mode 100644 nautobot_golden_config/tests/test_filter_compliancefeature.py delete mode 100644 nautobot_golden_config/tests/test_form_compliancefeature.py delete mode 100644 nautobot_golden_config/tests/test_model_compliancefeature.py diff --git a/nautobot_golden_config/api/serializers.py b/nautobot_golden_config/api/serializers.py index f6b88ddc0..1d5aaf2b1 100644 --- a/nautobot_golden_config/api/serializers.py +++ b/nautobot_golden_config/api/serializers.py @@ -1,11 +1,22 @@ """API serializers for nautobot_golden_config.""" +# pylint: disable=too-many-ancestors from nautobot.apps.api import NautobotModelSerializer, TaggedModelSerializerMixin +from nautobot.dcim.api.serializers import DeviceSerializer +from nautobot.dcim.models import Device +from rest_framework import serializers from nautobot_golden_config import models +from nautobot_golden_config.utilities.config_postprocessing import get_config_postprocessing -class ComplianceFeatureSerializer(NautobotModelSerializer, TaggedModelSerializerMixin): # pylint: disable=too-many-ancestors +class GraphQLSerializer(serializers.Serializer): # pylint: disable=abstract-method + """Serializer for a GraphQL object.""" + + data = serializers.JSONField() + + +class ComplianceFeatureSerializer(NautobotModelSerializer, TaggedModelSerializerMixin): """ComplianceFeature Serializer.""" class Meta: @@ -16,3 +27,112 @@ class Meta: # Option for disabling write for certain fields: # read_only_fields = [] + + +class ComplianceRuleSerializer(NautobotModelSerializer, TaggedModelSerializerMixin): + """Serializer for ComplianceRule object.""" + + class Meta: + """Set Meta Data for ComplianceRule, will serialize all fields.""" + + model = models.ComplianceRule + fields = "__all__" + + +class ConfigComplianceSerializer(NautobotModelSerializer, TaggedModelSerializerMixin): + """Serializer for ConfigCompliance object.""" + + class Meta: + """Set Meta Data for ConfigCompliance, will serialize fields.""" + + model = models.ConfigCompliance + fields = "__all__" + + +class GoldenConfigSerializer(NautobotModelSerializer, TaggedModelSerializerMixin): + """Serializer for GoldenConfig object.""" + + class Meta: + """Set Meta Data for GoldenConfig, will serialize all fields.""" + + model = models.GoldenConfig + fields = "__all__" + + +class GoldenConfigSettingSerializer(NautobotModelSerializer, TaggedModelSerializerMixin): + """Serializer for GoldenConfigSetting object.""" + + class Meta: + """Set Meta Data for GoldenConfigSetting, will serialize all fields.""" + + model = models.GoldenConfigSetting + fields = "__all__" + + +class ConfigRemoveSerializer(NautobotModelSerializer, TaggedModelSerializerMixin): + """Serializer for ConfigRemove object.""" + + class Meta: + """Set Meta Data for ConfigRemove, will serialize all fields.""" + + model = models.ConfigRemove + fields = "__all__" + + +class ConfigReplaceSerializer(NautobotModelSerializer, TaggedModelSerializerMixin): + """Serializer for ConfigReplace object.""" + + class Meta: + """Set Meta Data for ConfigReplace, will serialize all fields.""" + + model = models.ConfigReplace + fields = "__all__" + + +class ConfigToPushSerializer(DeviceSerializer): # pylint: disable=nb-sub-class-name + """Serializer for ConfigToPush view.""" + + config = serializers.SerializerMethodField() + + class Meta(DeviceSerializer.Meta): + """Extend the Device serializer with the configuration after postprocessing.""" + + fields = "__all__" + model = Device + + def get_config(self, obj): + """Provide the intended configuration ready after postprocessing to the config field.""" + request = self.context.get("request") + config_details = models.GoldenConfig.objects.get(device=obj) + return get_config_postprocessing(config_details, request) + + +class RemediationSettingSerializer(NautobotModelSerializer, TaggedModelSerializerMixin): + """Serializer for RemediationSetting object.""" + + class Meta: + """Set Meta Data for RemediationSetting, will serialize all fields.""" + + model = models.RemediationSetting + fields = "__all__" + + +class ConfigPlanSerializer(NautobotModelSerializer, TaggedModelSerializerMixin): + """Serializer for ConfigPlan object.""" + + class Meta: + """Set Meta Data for ConfigPlan, will serialize all fields.""" + + model = models.ConfigPlan + fields = "__all__" + read_only_fields = ["device", "plan_type", "feature", "config_set"] + + +class GenerateIntendedConfigSerializer(serializers.Serializer): # pylint: disable=abstract-method + """Serializer for GenerateIntendedConfigView.""" + + intended_config = serializers.CharField(read_only=True) + intended_config_lines = serializers.ListField(read_only=True, child=serializers.CharField()) + graphql_data = serializers.JSONField(read_only=True) + diff = serializers.CharField(read_only=True) + diff_lines = serializers.ListField(read_only=True, child=serializers.CharField()) diff --git a/nautobot_golden_config/api/urls.py b/nautobot_golden_config/api/urls.py index 6ba20c08e..8c4652aff 100644 --- a/nautobot_golden_config/api/urls.py +++ b/nautobot_golden_config/api/urls.py @@ -1,11 +1,34 @@ """Django API urlpatterns declaration for nautobot_golden_config app.""" +from django.urls import path from nautobot.apps.api import OrderedDefaultRouter from nautobot_golden_config.api import views router = OrderedDefaultRouter() # add the name of your api endpoint, usually hyphenated model name in plural, e.g. "my-model-classes" -router.register("compliancefeature", views.ComplianceFeatureViewSet) +router.APIRootView = views.GoldenConfigRootView +router.register("compliance-feature", views.ComplianceFeatureViewSet) +router.register("compliance-rule", views.ComplianceRuleViewSet) +router.register("config-compliance", views.ConfigComplianceViewSet) +router.register("golden-config", views.GoldenConfigViewSet) +router.register("golden-config-settings", views.GoldenConfigSettingViewSet) +router.register("config-remove", views.ConfigRemoveViewSet) +router.register("config-replace", views.ConfigReplaceViewSet) +router.register("remediation-setting", views.RemediationSettingViewSet) +router.register("config-postprocessing", views.ConfigToPushViewSet) +router.register("config-plan", views.ConfigPlanViewSet) -urlpatterns = router.urls +urlpatterns = [ + path( + "sotagg//", + views.SOTAggDeviceDetailView.as_view(), + name="device_detail", + ), + path( + "generate-intended-config/", + views.GenerateIntendedConfigView.as_view(), + name="generate_intended_config", + ), +] +urlpatterns += router.urls diff --git a/nautobot_golden_config/api/views.py b/nautobot_golden_config/api/views.py index 225b061ec..01b103807 100644 --- a/nautobot_golden_config/api/views.py +++ b/nautobot_golden_config/api/views.py @@ -1,13 +1,78 @@ """API views for nautobot_golden_config.""" -from nautobot.apps.api import NautobotModelViewSet +import datetime +import difflib +import json +import logging +from pathlib import Path + +from django.contrib.contenttypes.models import ContentType +from django.utils.timezone import make_aware +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import OpenApiParameter, extend_schema +from jinja2.exceptions import TemplateError, TemplateSyntaxError +from nautobot.apps.api import NautobotModelViewSet, NotesViewSetMixin +from nautobot.apps.utils import render_jinja2 +from nautobot.core.api.views import ( + BulkDestroyModelMixin, + BulkUpdateModelMixin, + ModelViewSetMixin, + NautobotAPIVersionMixin, +) +from nautobot.dcim.models import Device +from nautobot.extras.datasources.git import ensure_git_repository +from nautobot.extras.models import GraphQLQuery +from nautobot_plugin_nornir.constants import NORNIR_SETTINGS +from nornir import InitNornir +from nornir_nautobot.plugins.tasks.dispatcher import dispatcher +from rest_framework import mixins, status, viewsets +from rest_framework.exceptions import APIException +from rest_framework.generics import GenericAPIView +from rest_framework.mixins import DestroyModelMixin, ListModelMixin, RetrieveModelMixin, UpdateModelMixin +from rest_framework.permissions import AllowAny, BasePermission, IsAuthenticated +from rest_framework.response import Response +from rest_framework.routers import APIRootView +from rest_framework.views import APIView +from rest_framework.viewsets import GenericViewSet from nautobot_golden_config import filters, models from nautobot_golden_config.api import serializers +from nautobot_golden_config.utilities.graphql import graph_ql_query +from nautobot_golden_config.utilities.helper import dispatch_params, get_device_to_settings_map, get_django_env + + +class GoldenConfigRootView(APIRootView): + """Golden Config API root view.""" + + def get_view_name(self): + """Golden Config API root view boilerplate.""" + return "Golden Config" + + +class SOTAggDeviceDetailView(APIView): + """Detail REST API view showing graphql, with a potential "transformer" of data on a specific device.""" + + permission_classes = [AllowAny] + + def get(self, request, *args, **kwargs): + """Get method serialize for a dictionary to json response.""" + device = Device.objects.get(pk=kwargs["pk"]) + settings = get_device_to_settings_map(queryset=Device.objects.filter(pk=device.pk))[device.id] + status_code, data = graph_ql_query(request, device, settings.sot_agg_query.query) + data = json.loads(json.dumps(data)) + return Response(serializers.GraphQLSerializer(data=data).initial_data, status=status_code) + +class ComplianceRuleViewSet(NautobotModelViewSet): # pylint:disable=too-many-ancestors + """API viewset for interacting with ComplianceRule objects.""" -class ComplianceFeatureViewSet(NautobotModelViewSet): # pylint: disable=too-many-ancestors - """ComplianceFeature viewset.""" + queryset = models.ComplianceRule.objects.all() + serializer_class = serializers.ComplianceRuleSerializer + filterset_class = filters.ComplianceRuleFilterSet + + +class ComplianceFeatureViewSet(NautobotModelViewSet): # pylint:disable=too-many-ancestors + """API viewset for interacting with ComplianceFeature objects.""" queryset = models.ComplianceFeature.objects.all() serializer_class = serializers.ComplianceFeatureSerializer @@ -15,3 +80,295 @@ class ComplianceFeatureViewSet(NautobotModelViewSet): # pylint: disable=too-man # Option for modifying the default HTTP methods: # http_method_names = ["get", "post", "put", "patch", "delete", "head", "options", "trace"] + + +class ConfigComplianceViewSet(NautobotModelViewSet): # pylint:disable=too-many-ancestors + """API viewset for interacting with ConfigCompliance objects.""" + + queryset = models.ConfigCompliance.objects.all() + serializer_class = serializers.ConfigComplianceSerializer + filterset_class = filters.ConfigComplianceFilterSet + + +class GoldenConfigViewSet(NautobotModelViewSet): # pylint:disable=too-many-ancestors + """API viewset for interacting with GoldenConfig objects.""" + + queryset = models.GoldenConfig.objects.all() + serializer_class = serializers.GoldenConfigSerializer + filterset_class = filters.GoldenConfigFilterSet + + +class GoldenConfigSettingViewSet(NautobotModelViewSet): # pylint:disable=too-many-ancestors + """API viewset for interacting with GoldenConfigSetting objects.""" + + queryset = models.GoldenConfigSetting.objects.all() + serializer_class = serializers.GoldenConfigSettingSerializer + filterset_class = filters.GoldenConfigSettingFilterSet + + +class ConfigRemoveViewSet(NautobotModelViewSet): # pylint:disable=too-many-ancestors + """API viewset for interacting with ConfigRemove objects.""" + + queryset = models.ConfigRemove.objects.all() + serializer_class = serializers.ConfigRemoveSerializer + filterset_class = filters.ConfigRemoveFilterSet + + +class ConfigReplaceViewSet(NautobotModelViewSet): # pylint:disable=too-many-ancestors + """API viewset for interacting with ConfigReplace objects.""" + + queryset = models.ConfigReplace.objects.all() + serializer_class = serializers.ConfigReplaceSerializer + filterset_class = filters.ConfigReplaceFilterSet + + +class ConfigPushPermissions(BasePermission): + """Permissions class to validate access to Devices and GoldenConfig view.""" + + def has_permission(self, request, view): + """Method to validated permissions to API view.""" + return request.user.has_perm("nautobot_golden_config.view_goldenconfig") + + def has_object_permission(self, request, view, obj): + """Validate user access to the object, taking into account constraints.""" + return request.user.has_perm("dcim.view_device", obj=obj) + + +class ConfigToPushViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet): + """Detail REST API view showing configuration after postprocessing.""" + + permission_classes = [IsAuthenticated & ConfigPushPermissions] + queryset = Device.objects.all() + serializer_class = serializers.ConfigToPushSerializer + + +class RemediationSettingViewSet(NautobotModelViewSet): # pylint:disable=too-many-ancestors + """API viewset for interacting with RemediationSetting objects.""" + + queryset = models.RemediationSetting.objects.all() + serializer_class = serializers.RemediationSettingSerializer + filterset_class = filters.RemediationSettingFilterSet + + +class ConfigPlanViewSet( + NautobotAPIVersionMixin, + NotesViewSetMixin, + ModelViewSetMixin, + RetrieveModelMixin, + UpdateModelMixin, + DestroyModelMixin, + ListModelMixin, + BulkUpdateModelMixin, + BulkDestroyModelMixin, + GenericViewSet, +): # pylint:disable=too-many-ancestors + """API viewset for interacting with ConfigPlan objects. Does not support POST to create objects.""" + + queryset = models.ConfigPlan.objects.all() + serializer_class = serializers.ConfigPlanSerializer + filterset_class = filters.ConfigPlanFilterSet + + def get_serializer_context(self): + """Gather all custom fields for the model. Copied from nautobot.extras.api.views.CustomFieldModelViewSet.""" + content_type = ContentType.objects.get_for_model(self.queryset.model) + custom_fields = content_type.custom_fields.all() + + context = super().get_serializer_context() + context.update( + { + "custom_fields": custom_fields, + } + ) + return context + + +class GenerateIntendedConfigException(APIException): + """Exception for when the intended config cannot be generated.""" + + status_code = 400 + default_detail = "Unable to generate the intended config for this device." + default_code = "error" + + +def _nornir_task_inject_graphql_data(task, graphql_data, **kwargs): + """Inject the GraphQL data into the Nornir task host data and then run nornir_nautobot.plugins.tasks.dispatcher.dispatcher subtask. + + This is a small stub of the logic in nautobot_golden_config.nornir_plays.config_intended.run_template. + """ + task.host.data.update(graphql_data) + generated_config = task.run(task=dispatcher, name="GENERATE CONFIG", **kwargs) + return generated_config + + +class GenerateIntendedConfigView(NautobotAPIVersionMixin, GenericAPIView): + """API view for generating the intended config for a Device.""" + + name = "Generate Intended Config for Device" + permission_classes = [IsAuthenticated] + serializer_class = serializers.GenerateIntendedConfigSerializer + + def _get_diff(self, device, intended_config): + """Generate a unified diff between the provided config and the intended config stored on the Device's GoldenConfig.intended_config.""" + diff = None + try: + golden_config = device.goldenconfig + if golden_config.intended_last_success_date is not None: + prior_intended_config = golden_config.intended_config + diff = "".join( + difflib.unified_diff( + prior_intended_config.splitlines(keepends=True), + intended_config.splitlines(keepends=True), + fromfile="prior intended config", + tofile="rendered config", + ) + ) + except models.GoldenConfig.DoesNotExist: + pass + + return diff + + def _get_object(self, request, model, query_param): + """Get the requested model instance, restricted to requesting user.""" + pk = request.query_params.get(query_param) + if not pk: + raise GenerateIntendedConfigException(f"Parameter {query_param} is required") + try: + return model.objects.restrict(request.user, "view").get(pk=pk) + except model.DoesNotExist as exc: + raise GenerateIntendedConfigException(f"{model.__name__} with id '{pk}' not found") from exc + + def _get_jinja_template_path(self, settings, device, git_repository): + """Get the Jinja template path for the device in the provided git repository.""" + try: + rendered_path = render_jinja2(template_code=settings.jinja_path_template, context={"obj": device}) + except (TemplateSyntaxError, TemplateError) as exc: + raise GenerateIntendedConfigException("Error rendering Jinja path template") from exc + filesystem_path = Path(git_repository.filesystem_path) / rendered_path + if not filesystem_path.is_file(): + msg = f"Jinja template {filesystem_path} not found in git repository {git_repository}" + raise GenerateIntendedConfigException(msg) + return filesystem_path + + @extend_schema( + parameters=[ + OpenApiParameter( + name="device_id", + required=True, + type=OpenApiTypes.UUID, + location=OpenApiParameter.QUERY, + ), + OpenApiParameter( + name="graphql_query_id", + required=False, + type=OpenApiTypes.UUID, + location=OpenApiParameter.QUERY, + ), + ] + ) + def get(self, request, *args, **kwargs): + """Generate intended configuration for a Device.""" + device = self._get_object(request, Device, "device_id") + graphql_query = None + graphql_query_id_param = request.query_params.get("graphql_query_id") + if graphql_query_id_param: + try: + graphql_query = GraphQLQuery.objects.get(pk=request.query_params.get("graphql_query_id")) + except GraphQLQuery.DoesNotExist as exc: + raise GenerateIntendedConfigException( + f"GraphQLQuery with id '{graphql_query_id_param}' not found" + ) from exc + settings = models.GoldenConfigSetting.objects.get_for_device(device) + if not settings: + raise GenerateIntendedConfigException("No Golden Config settings found for this device") + if not settings.jinja_repository: + raise GenerateIntendedConfigException("Golden Config settings jinja_repository not set") + + if graphql_query is None: + if settings.sot_agg_query is not None: + graphql_query = settings.sot_agg_query + else: + raise GenerateIntendedConfigException("Golden Config settings sot_agg_query not set") + + if "device_id" not in graphql_query.variables: + raise GenerateIntendedConfigException("The selected GraphQL query is missing a 'device_id' variable") + + try: + git_repository = settings.jinja_repository + ensure_git_repository(git_repository) + except Exception as exc: + raise GenerateIntendedConfigException("Error trying to sync git repository") from exc + + filesystem_path = self._get_jinja_template_path(settings, device, git_repository) + + status_code, graphql_data = graph_ql_query(request, device, graphql_query.query) + if status_code == status.HTTP_200_OK: + try: + intended_config = self._render_config_nornir_serial( + device=device, + jinja_template=filesystem_path.name, + jinja_root_path=filesystem_path.parent, + graphql_data=graphql_data, + ) + except Exception as exc: + raise GenerateIntendedConfigException(f"Error rendering Jinja template: {exc}") from exc + + diff = self._get_diff(device, intended_config) + + return Response( + data={ + "intended_config": intended_config, + "intended_config_lines": intended_config.split("\n"), + "graphql_data": graphql_data, + "diff": diff, + "diff_lines": diff.split("\n") if diff else [], + }, + status=status.HTTP_200_OK, + ) + + raise GenerateIntendedConfigException("Unable to generate the intended config for this device") + + def _render_config_nornir_serial(self, device, jinja_template, jinja_root_path, graphql_data): + """Render the Jinja template for the device using Nornir serial runner. + + This is a small stub of the logic in nornir_plays.config_intended.config_intended. + """ + jinja_env = get_django_env() + with InitNornir( + runner={"plugin": "serial"}, + logging={"enabled": False}, + inventory={ + "plugin": "nautobot-inventory", + "options": { + "credentials_class": NORNIR_SETTINGS.get("credentials"), + "params": NORNIR_SETTINGS.get("inventory_params"), + "queryset": Device.objects.filter(pk=device.pk), + "defaults": {"now": make_aware(datetime.datetime.now())}, + }, + }, + ) as nornir_obj: + results = nornir_obj.run( + task=_nornir_task_inject_graphql_data, + name="REST API GENERATE CONFIG", + graphql_data=graphql_data, + obj=device, # Used by the nornir tasks for logging to the logger below + logger=logging.getLogger( + dispatcher.__module__ + ), # The nornir tasks are built for logging to a JobResult, pass a standard logger here + jinja_template=jinja_template, + jinja_root_path=jinja_root_path, + output_file_location="/dev/null", # The nornir task outputs the templated config to a file, but this API doesn't need it + jinja_filters=jinja_env.filters, + jinja_env=jinja_env, + **dispatch_params( + "generate_config", device.platform.network_driver, logging.getLogger(dispatch_params.__module__) + ), + ) + if results[device.name].failed: + if results[device.name].exception: # pylint: disable=no-else-raise + raise results[device.name].exception + else: + raise GenerateIntendedConfigException( + f"Error generating intended config for {device.name}: {results[device.name].result}" + ) + else: + return results[device.name][1][1][0].result["config"] diff --git a/nautobot_golden_config/filters.py b/nautobot_golden_config/filters.py index 333644c96..e4ed3187d 100644 --- a/nautobot_golden_config/filters.py +++ b/nautobot_golden_config/filters.py @@ -1,17 +1,452 @@ """Filtering for nautobot_golden_config.""" -from nautobot.apps.filters import NameSearchFilterSet, NautobotFilterSet +import django_filters +from nautobot.apps.filters import ( + MultiValueDateTimeFilter, + NaturalKeyOrPKMultipleChoiceFilter, + NautobotFilterSet, + SearchFilter, + StatusFilter, + TreeNodeMultipleChoiceFilter, +) +from nautobot.dcim.models import Device, DeviceType, Location, Manufacturer, Platform, Rack, RackGroup +from nautobot.extras.models import JobResult, Role, Status +from nautobot.tenancy.models import Tenant, TenantGroup from nautobot_golden_config import models -class ComplianceFeatureFilterSet(NautobotFilterSet, NameSearchFilterSet): # pylint: disable=too-many-ancestors - """Filter for ComplianceFeature.""" +class GoldenConfigFilterSet(NautobotFilterSet): + """Filter capabilities for GoldenConfig instances.""" + + @staticmethod + def _get_filter_lookup_dict(existing_filter): + """Extend method to account for isnull on datetime types.""" + # Choose the lookup expression map based on the filter type + lookup_map = NautobotFilterSet._get_filter_lookup_dict(existing_filter) + if isinstance(existing_filter, MultiValueDateTimeFilter): + lookup_map.update({"isnull": "isnull"}) + return lookup_map + + q = SearchFilter( + filter_predicates={ + "device__name": { + "lookup_expr": "icontains", + "preprocessor": str, + }, + }, + ) + tenant_group_id = TreeNodeMultipleChoiceFilter( + queryset=TenantGroup.objects.all(), + field_name="device__tenant__tenant_group", + to_field_name="id", + label="Tenant Group (ID)", + ) + tenant_group = TreeNodeMultipleChoiceFilter( + queryset=TenantGroup.objects.all(), + field_name="device__tenant__tenant_group", + to_field_name="name", + label="Tenant Group (name)", + ) + tenant = NaturalKeyOrPKMultipleChoiceFilter( + queryset=Tenant.objects.all(), + field_name="device__tenant", + to_field_name="name", + label="Tenant (name or ID)", + ) + location_id = TreeNodeMultipleChoiceFilter( + # Not limiting to content_type=dcim.device to allow parent locations to be included + # i.e. include all Sites in a Region, even though Region can't be assigned to a Device + queryset=Location.objects.all(), + field_name="device__location", + to_field_name="id", + label="Location (ID)", + ) + location = TreeNodeMultipleChoiceFilter( + # Not limiting to content_type=dcim.device to allow parent locations to be included + # i.e. include all sites in a Region, even though Region can't be assigned to a Device + queryset=Location.objects.all(), + field_name="device__location", + to_field_name="name", + label="Location (name)", + ) + rack_group_id = TreeNodeMultipleChoiceFilter( + queryset=RackGroup.objects.all(), + field_name="device__rack__rack_group", + to_field_name="id", + label="Rack group (ID)", + ) + rack_group = TreeNodeMultipleChoiceFilter( + queryset=RackGroup.objects.all(), + field_name="device__rack__rack_group", + to_field_name="name", + label="Rack group (name)", + ) + rack = NaturalKeyOrPKMultipleChoiceFilter( + field_name="device__rack", + queryset=Rack.objects.all(), + to_field_name="name", + label="Rack (name or ID)", + ) + role = NaturalKeyOrPKMultipleChoiceFilter( + field_name="device__role", + queryset=Role.objects.filter(content_types__model="device"), + to_field_name="name", + label="Role (name or ID)", + ) + manufacturer = NaturalKeyOrPKMultipleChoiceFilter( + field_name="device__device_type__manufacturer", + queryset=Manufacturer.objects.all(), + to_field_name="name", + label="Manufacturer (name or ID)", + ) + platform = NaturalKeyOrPKMultipleChoiceFilter( + field_name="device__platform", + queryset=Platform.objects.all(), + to_field_name="name", + label="Platform (name or ID)", + ) + device_status = StatusFilter( + field_name="device__status", + queryset=Status.objects.all(), + label="Device Status", + ) + device_type = NaturalKeyOrPKMultipleChoiceFilter( + field_name="device__device_type", + queryset=DeviceType.objects.all(), + to_field_name="model", + label="DeviceType (model or ID)", + ) + device = NaturalKeyOrPKMultipleChoiceFilter( + field_name="device", + queryset=Device.objects.all(), + to_field_name="name", + label="Device (name or ID)", + ) class Meta: - """Meta attributes for filter.""" + """Meta class attributes for GoldenConfigFilter.""" + + model = models.GoldenConfig + distinct = True + fields = "__all__" + + +class ConfigComplianceFilterSet(GoldenConfigFilterSet): # pylint: disable=too-many-ancestors + """Filter capabilities for ConfigCompliance instances.""" + + feature_id = django_filters.ModelMultipleChoiceFilter( + field_name="rule__feature", + queryset=models.ComplianceFeature.objects.all(), + label="ComplianceFeature (ID)", + ) + feature = django_filters.ModelMultipleChoiceFilter( + field_name="rule__feature__slug", + queryset=models.ComplianceFeature.objects.all(), + to_field_name="slug", + label="ComplianceFeature (slug)", + ) + + class Meta: + """Meta class attributes for ConfigComplianceFilter.""" + + model = models.ConfigCompliance + fields = "__all__" + + +class ComplianceFeatureFilterSet(NautobotFilterSet): + """Inherits Base Class NautobotFilterSet.""" + + q = SearchFilter( + filter_predicates={ + "name": { + "lookup_expr": "icontains", + "preprocessor": str, + }, + }, + ) + + class Meta: + """Boilerplate filter Meta data for compliance feature.""" model = models.ComplianceFeature + fields = "__all__" + + +class ComplianceRuleFilterSet(NautobotFilterSet): + """Inherits Base Class NautobotFilterSet.""" + + q = SearchFilter( + filter_predicates={ + "feature__name": { + "lookup_expr": "icontains", + "preprocessor": str, + }, + }, + ) + platform = NaturalKeyOrPKMultipleChoiceFilter( + field_name="platform", + queryset=Platform.objects.all(), + to_field_name="name", + label="Platform (name or ID)", + ) + + class Meta: + """Boilerplate filter Meta data for compliance rule.""" + + model = models.ComplianceRule + fields = "__all__" + + +class ConfigRemoveFilterSet(NautobotFilterSet): + """Inherits Base Class NautobotFilterSet.""" + + q = SearchFilter( + filter_predicates={ + "name": { + "lookup_expr": "icontains", + "preprocessor": str, + }, + }, + ) + platform = NaturalKeyOrPKMultipleChoiceFilter( + field_name="platform", + queryset=Platform.objects.all(), + to_field_name="name", + label="Platform (name or ID)", + ) + + class Meta: + """Boilerplate filter Meta data for Config Remove.""" + + model = models.ConfigRemove + fields = "__all__" + + +class ConfigReplaceFilterSet(NautobotFilterSet): + """Inherits Base Class NautobotFilterSet.""" + + q = SearchFilter( + filter_predicates={ + "name": { + "lookup_expr": "icontains", + "preprocessor": str, + }, + }, + ) + platform = NaturalKeyOrPKMultipleChoiceFilter( + field_name="platform", + queryset=Platform.objects.all(), + to_field_name="name", + label="Platform (name or ID)", + ) + + class Meta: + """Boilerplate filter Meta data for Config Replace.""" + + model = models.ConfigReplace + fields = "__all__" + + +class GoldenConfigSettingFilterSet(NautobotFilterSet): + """Inherits Base Class NautobotFilterSet.""" + + device_id = django_filters.ModelMultipleChoiceFilter( + queryset=Device.objects.all(), + label="Device (ID)", + method="filter_device_id", + ) + + def filter_device_id(self, queryset, name, value): # pylint: disable=unused-argument + """Filter by Device ID.""" + if not value: + return queryset + golden_config_setting_ids = [] + for instance in value: + if isinstance(instance, Device): + device = instance + else: + device = Device.objects.get(id=instance) + golden_config_setting = models.GoldenConfigSetting.objects.get_for_device(device) + if golden_config_setting is not None: + golden_config_setting_ids.append(golden_config_setting.id) + return queryset.filter(id__in=golden_config_setting_ids) + + class Meta: + """Boilerplate filter Meta data for Config Remove.""" + + model = models.GoldenConfigSetting + fields = "__all__" + + +class RemediationSettingFilterSet(NautobotFilterSet): + """Inherits Base Class CustomFieldModelFilterSet.""" + + q = SearchFilter( + filter_predicates={ + "platform__name": { + "lookup_expr": "icontains", + "preprocessor": str, + }, + "remediation_type": { + "lookup_expr": "icontains", + "preprocessor": str, + }, + }, + ) + platform = django_filters.ModelMultipleChoiceFilter( + field_name="platform__name", + queryset=Platform.objects.all(), + to_field_name="name", + label="Platform Name", + ) + platform_id = django_filters.ModelMultipleChoiceFilter( + queryset=Platform.objects.all(), + label="Platform ID", + ) + + class Meta: + """Boilerplate filter Meta data for Remediation Setting.""" + + model = models.RemediationSetting + fields = "__all__" + + +class ConfigPlanFilterSet(NautobotFilterSet): + """Inherits Base Class NautobotFilterSet.""" + + q = SearchFilter( + filter_predicates={ + "device__name": { + "lookup_expr": "icontains", + "preprocessor": str, + }, + "change_control_id": { + "lookup_expr": "icontains", + "preprocessor": str, + }, + }, + ) + device_id = django_filters.ModelMultipleChoiceFilter( + queryset=Device.objects.all(), + label="Device ID", + ) + device = django_filters.ModelMultipleChoiceFilter( + field_name="device__name", + queryset=Device.objects.all(), + to_field_name="name", + label="Device Name", + ) + feature_id = django_filters.ModelMultipleChoiceFilter( + field_name="feature__id", + queryset=models.ComplianceFeature.objects.all(), + to_field_name="id", + label="Feature ID", + ) + feature = django_filters.ModelMultipleChoiceFilter( + field_name="feature__name", + queryset=models.ComplianceFeature.objects.all(), + to_field_name="name", + label="Feature Name", + ) + plan_result_id = django_filters.ModelMultipleChoiceFilter( + queryset=JobResult.objects.filter(config_plan__isnull=False).distinct(), + label="Plan JobResult ID", + to_field_name="id", + ) + tenant_group_id = TreeNodeMultipleChoiceFilter( + queryset=TenantGroup.objects.all(), + field_name="device__tenant__tenant_group", + to_field_name="id", + label="Tenant Group (ID)", + ) + tenant_group = TreeNodeMultipleChoiceFilter( + queryset=TenantGroup.objects.all(), + field_name="device__tenant__tenant_group", + to_field_name="name", + label="Tenant Group (name)", + ) + tenant = NaturalKeyOrPKMultipleChoiceFilter( + queryset=Tenant.objects.all(), + field_name="device__tenant", + to_field_name="name", + label="Tenant (name or ID)", + ) + manufacturer = NaturalKeyOrPKMultipleChoiceFilter( + field_name="device__device_type__manufacturer", + queryset=Manufacturer.objects.all(), + to_field_name="name", + label="Manufacturer (name or ID)", + ) + platform = NaturalKeyOrPKMultipleChoiceFilter( + field_name="device__platform", + queryset=Platform.objects.all(), + to_field_name="name", + label="Platform (name or ID)", + ) + location_id = TreeNodeMultipleChoiceFilter( + # Not limiting to content_type=dcim.device to allow parent locations to be included + # i.e. include all Sites in a Region, even though Region can't be assigned to a Device + queryset=Location.objects.all(), + field_name="device__location", + to_field_name="id", + label="Location (ID)", + ) + location = TreeNodeMultipleChoiceFilter( + # Not limiting to content_type=dcim.device to allow parent locations to be included + # i.e. include all sites in a Region, even though Region can't be assigned to a Device + queryset=Location.objects.all(), + field_name="device__location", + to_field_name="name", + label="Location (name)", + ) + deploy_result_id = django_filters.ModelMultipleChoiceFilter( + queryset=JobResult.objects.filter(config_plan__isnull=False).distinct(), + label="Deploy JobResult ID", + to_field_name="id", + ) + change_control_id = django_filters.CharFilter( + field_name="change_control_id", + lookup_expr="exact", + ) + rack_group_id = TreeNodeMultipleChoiceFilter( + queryset=RackGroup.objects.all(), + field_name="device__rack__rack_group", + to_field_name="id", + label="Rack group (ID)", + ) + rack_group = TreeNodeMultipleChoiceFilter( + queryset=RackGroup.objects.all(), + field_name="device__rack__rack_group", + to_field_name="name", + label="Rack group (name)", + ) + rack = NaturalKeyOrPKMultipleChoiceFilter( + field_name="device__rack", + queryset=Rack.objects.all(), + to_field_name="name", + label="Rack (name or ID)", + ) + role = NaturalKeyOrPKMultipleChoiceFilter( + field_name="device__role", + queryset=Role.objects.filter(content_types__model="device"), + to_field_name="name", + label="Role (name or ID)", + ) + status_id = django_filters.ModelMultipleChoiceFilter( + # field_name="status__id", + queryset=Status.objects.all(), + label="Status ID", + ) + status = django_filters.ModelMultipleChoiceFilter( + field_name="status__name", + queryset=Status.objects.all(), + to_field_name="name", + label="Status", + ) + + class Meta: + """Boilerplate filter Meta data for Config Plan.""" - # add any fields from the model that you would like to filter your searches by using those - fields = ["id", "name", "description"] + model = models.ConfigPlan + fields = "__all__" diff --git a/nautobot_golden_config/forms.py b/nautobot_golden_config/forms.py index e5a35c990..2bd455595 100644 --- a/nautobot_golden_config/forms.py +++ b/nautobot_golden_config/forms.py @@ -1,47 +1,616 @@ -"""Forms for nautobot_golden_config.""" +"""Forms for Device Configuration Backup.""" +# pylint: disable=too-many-ancestors -from django import forms -from nautobot.apps.forms import NautobotBulkEditForm, NautobotFilterForm, NautobotModelForm, TagsBulkEditFormMixin +import json + +import django.forms as django_forms +from nautobot.apps import forms +from nautobot.dcim.models import Device, DeviceType, Location, Manufacturer, Platform, Rack, RackGroup +from nautobot.extras.forms import NautobotBulkEditForm, NautobotFilterForm, NautobotModelForm +from nautobot.extras.models import DynamicGroup, GitRepository, GraphQLQuery, JobResult, Role, Status, Tag +from nautobot.tenancy.models import Tenant, TenantGroup from nautobot_golden_config import models +from nautobot_golden_config.choices import ComplianceRuleConfigTypeChoice, ConfigPlanTypeChoice, RemediationTypeChoice + +# ConfigCompliance + + +class DeviceRelatedFilterForm(NautobotFilterForm): # pylint: disable=nb-no-model-found + """Base FilterForm for below FilterForms.""" + + tenant_group_id = forms.DynamicModelMultipleChoiceField( + queryset=TenantGroup.objects.all(), to_field_name="id", required=False, label="Tenant group ID" + ) + tenant_group = forms.DynamicModelMultipleChoiceField( + queryset=TenantGroup.objects.all(), + to_field_name="name", + required=False, + label="Tenant group name", + null_option="None", + ) + tenant = forms.DynamicModelMultipleChoiceField( + queryset=Tenant.objects.all(), + to_field_name="name", + required=False, + null_option="None", + query_params={"group": "$tenant_group"}, + ) + location_id = forms.DynamicModelMultipleChoiceField( + # Not limiting to query_params={"content_type": "dcim.device" to allow parent locations to be included + # i.e. include all sites in a Region, even though Region can't be assigned to a Device + queryset=Location.objects.all(), + to_field_name="id", + required=False, + label="Location ID", + ) + location = forms.DynamicModelMultipleChoiceField( + queryset=Location.objects.all(), to_field_name="name", required=False, label="Location name" + ) + rack_group_id = forms.DynamicModelMultipleChoiceField( + queryset=RackGroup.objects.all(), + to_field_name="id", + required=False, + label="Rack group ID", + query_params={"location": "$location"}, + ) + rack_group = forms.DynamicModelMultipleChoiceField( + queryset=RackGroup.objects.all(), + to_field_name="name", + required=False, + label="Rack group name", + query_params={"location": "$location"}, + ) + rack_id = forms.DynamicModelMultipleChoiceField( + queryset=Rack.objects.all(), + required=False, + label="Rack", + null_option="None", + query_params={ + "location": "$location", + "group_id": "$rack_group_id", + }, + ) + role = forms.DynamicModelMultipleChoiceField( + queryset=Role.objects.all(), + to_field_name="name", + required=False, + query_params={"content_types": "dcim.device"}, + ) + manufacturer = forms.DynamicModelMultipleChoiceField( + queryset=Manufacturer.objects.all(), to_field_name="name", required=False, label="Manufacturer" + ) + device_type = forms.DynamicModelMultipleChoiceField( + queryset=DeviceType.objects.all(), + required=False, + label="Model", + display_field="model", + query_params={"manufacturer": "$manufacturer"}, + ) + platform = forms.DynamicModelMultipleChoiceField( + queryset=Platform.objects.all(), to_field_name="name", required=False, null_option="None" + ) + device = forms.DynamicModelMultipleChoiceField( + queryset=Device.objects.all(), required=False, null_option="None", label="Device", to_field_name="name" + ) + +class GoldenConfigFilterForm(DeviceRelatedFilterForm): + """Filter Form for GoldenConfig.""" -class ComplianceFeatureForm(NautobotModelForm): # pylint: disable=too-many-ancestors - """ComplianceFeature creation/edit form.""" + model = models.GoldenConfig + field_order = [ + "q", + "tenant_group", + "tenant", + "location_id", + "location", + "rack_group_id", + "rack_group", + "rack_id", + "role", + "manufacturer", + "platform", + "device_status", + "device_type", + "device", + ] + q = django_forms.CharField(required=False, label="Search") + + +class GoldenConfigBulkEditForm(NautobotBulkEditForm): + """BulkEdit form for GoldenConfig instances.""" + + pk = django_forms.ModelMultipleChoiceField( + queryset=models.GoldenConfig.objects.all(), widget=django_forms.MultipleHiddenInput + ) + # description = django_forms.CharField(max_length=200, required=False) class Meta: - """Meta attributes.""" + """Boilerplate form Meta data for GoldenConfig.""" - model = models.ComplianceFeature - fields = [ - "name", - "description", - ] + nullable_fields = [] + + +class ConfigComplianceFilterForm(DeviceRelatedFilterForm): + """Filter Form for ConfigCompliance instances.""" + + model = models.ConfigCompliance + # Set field order to be explicit + field_order = [ + "q", + "tenant_group", + "tenant", + "location_id", + "location", + "rack_group_id", + "rack_group", + "rack_id", + "role", + "manufacturer", + "platform", + "device_status", + "device_type", + "device", + ] + + q = django_forms.CharField(required=False, label="Search") + + def __init__(self, *args, **kwargs): + """Required for status to work.""" + super().__init__(*args, **kwargs) + self.fields["device_status"] = forms.DynamicModelMultipleChoiceField( + required=False, + queryset=Status.objects.all(), + query_params={"content_types": Device._meta.label_lower}, + display_field="label", + label="Device Status", + to_field_name="name", + ) + self.order_fields(self.field_order) # Reorder fields again + + +# ComplianceRule -class ComplianceFeatureBulkEditForm(TagsBulkEditFormMixin, NautobotBulkEditForm): # pylint: disable=too-many-ancestors - """ComplianceFeature bulk edit form.""" +class ComplianceRuleForm(NautobotModelForm): + """Filter Form for ComplianceRule instances.""" - pk = forms.ModelMultipleChoiceField(queryset=models.ComplianceFeature.objects.all(), widget=forms.MultipleHiddenInput) - description = forms.CharField(required=False) + platform = forms.DynamicModelChoiceField(queryset=Platform.objects.all()) class Meta: - """Meta attributes.""" + """Boilerplate form Meta data for compliance rule.""" + + model = models.ComplianceRule + fields = "__all__" + + +class ComplianceRuleFilterForm(NautobotFilterForm): + """Form for ComplianceRule instances.""" + + model = models.ComplianceRule + + q = django_forms.CharField(required=False, label="Search") + platform = forms.DynamicModelMultipleChoiceField( + queryset=Platform.objects.all(), to_field_name="name", required=False, null_option="None" + ) + + feature = forms.DynamicModelMultipleChoiceField(queryset=models.ComplianceFeature.objects.all(), required=False) + + +class ComplianceRuleBulkEditForm(NautobotBulkEditForm): + """BulkEdit form for ComplianceRule instances.""" + + pk = django_forms.ModelMultipleChoiceField( + queryset=models.ComplianceRule.objects.all(), widget=django_forms.MultipleHiddenInput + ) + description = django_forms.CharField(max_length=200, required=False) + config_type = django_forms.ChoiceField( + required=False, + choices=forms.add_blank_choice(ComplianceRuleConfigTypeChoice), + ) + config_ordered = django_forms.NullBooleanField(required=False, widget=forms.BulkEditNullBooleanSelect()) + custom_compliance = django_forms.NullBooleanField(required=False, widget=forms.BulkEditNullBooleanSelect()) + config_remediation = django_forms.NullBooleanField(required=False, widget=forms.BulkEditNullBooleanSelect()) + + class Meta: + """Boilerplate form Meta data for ComplianceRule.""" + + nullable_fields = [] + + +# ComplianceFeature - nullable_fields = [ - "description", - ] + +class ComplianceFeatureForm(NautobotModelForm): + """Filter Form for ComplianceFeature instances.""" + + slug = forms.SlugField() # TODO: 2.1: Change from slugs once django-pivot is figured out + + class Meta: + """Boilerplate form Meta data for compliance feature.""" + + model = models.ComplianceFeature + fields = "__all__" class ComplianceFeatureFilterForm(NautobotFilterForm): - """Filter form to filter searches.""" + """Form for ComplianceFeature instances.""" model = models.ComplianceFeature - field_order = ["q", "name"] + q = django_forms.CharField(required=False, label="Search") + name = forms.DynamicModelChoiceField(queryset=models.ComplianceFeature.objects.all(), required=False) + + +class ComplianceFeatureBulkEditForm(NautobotBulkEditForm): + """BulkEdit form for ComplianceFeature instances.""" + + pk = django_forms.ModelMultipleChoiceField( + queryset=models.ComplianceFeature.objects.all(), widget=django_forms.MultipleHiddenInput + ) + description = django_forms.CharField(max_length=200, required=False) + + class Meta: + """Boilerplate form Meta data for ComplianceFeature.""" + + nullable_fields = [] + + +# ConfigRemove + + +class ConfigRemoveForm(NautobotModelForm): + """Filter Form for Line Removal instances.""" + + platform = forms.DynamicModelChoiceField(queryset=Platform.objects.all()) + + class Meta: + """Boilerplate form Meta data for removal feature.""" - q = forms.CharField( + model = models.ConfigRemove + fields = "__all__" + + +class ConfigRemoveFilterForm(NautobotFilterForm): + """Filter Form for Line Removal.""" + + model = models.ConfigRemove + platform = forms.DynamicModelMultipleChoiceField( + queryset=Platform.objects.all(), to_field_name="name", required=False, null_option="None" + ) + name = forms.DynamicModelChoiceField( + queryset=models.ConfigRemove.objects.all(), to_field_name="name", required=False + ) + + +class ConfigRemoveBulkEditForm(NautobotBulkEditForm): + """BulkEdit form for ConfigRemove instances.""" + + pk = django_forms.ModelMultipleChoiceField( + queryset=models.ConfigRemove.objects.all(), widget=django_forms.MultipleHiddenInput + ) + description = django_forms.CharField(max_length=200, required=False) + + class Meta: + """Boilerplate form Meta data for ConfigRemove.""" + + nullable_fields = [] + + +# ConfigReplace + + +class ConfigReplaceForm(NautobotModelForm): + """Filter Form for Line Removal instances.""" + + platform = forms.DynamicModelChoiceField(queryset=Platform.objects.all()) + + class Meta: + """Boilerplate form Meta data for removal feature.""" + + model = models.ConfigReplace + fields = "__all__" + + +class ConfigReplaceFilterForm(NautobotFilterForm): + """Filter Form for Line Replacement.""" + + model = models.ConfigReplace + + platform = forms.DynamicModelMultipleChoiceField( + queryset=Platform.objects.all(), to_field_name="name", required=False, null_option="None" + ) + name = forms.DynamicModelChoiceField( + queryset=models.ConfigReplace.objects.all(), to_field_name="name", required=False + ) + + +class ConfigReplaceBulkEditForm(NautobotBulkEditForm): + """BulkEdit form for ConfigReplace instances.""" + + pk = django_forms.ModelMultipleChoiceField( + queryset=models.ConfigReplace.objects.all(), widget=django_forms.MultipleHiddenInput + ) + description = django_forms.CharField(max_length=200, required=False) + + class Meta: + """Boilerplate form Meta data for ConfigReplace.""" + + nullable_fields = [] + + +# GoldenConfigSetting + + +class GoldenConfigSettingForm(NautobotModelForm): + """Filter Form for GoldenConfigSettingForm instances.""" + + slug = forms.SlugField() + dynamic_group = django_forms.ModelChoiceField(queryset=DynamicGroup.objects.all()) + + class Meta: + """Filter Form Meta Data for GoldenConfigSettingForm instances.""" + + model = models.GoldenConfigSetting + fields = "__all__" + + +class GoldenConfigSettingFilterForm(NautobotFilterForm): + """Form for GoldenConfigSetting instances.""" + + model = models.GoldenConfigSetting + + q = django_forms.CharField(required=False, label="Search") + name = django_forms.CharField(required=False) + weight = django_forms.IntegerField(required=False) + backup_repository = django_forms.ModelChoiceField( + queryset=GitRepository.objects.filter(provided_contents__contains="nautobot_golden_config.backupconfigs"), + required=False, + ) + intended_repository = django_forms.ModelChoiceField( + queryset=GitRepository.objects.filter(provided_contents__contains="nautobot_golden_config.intendedconfigs"), required=False, - label="Search", - help_text="Search within Name or Slug.", ) - name = forms.CharField(required=False, label="Name") + jinja_repository = django_forms.ModelChoiceField( + queryset=GitRepository.objects.filter(provided_contents__contains="nautobot_golden_config.jinjatemplate"), + required=False, + ) + + +class GoldenConfigSettingBulkEditForm(NautobotBulkEditForm): + """BulkEdit form for GoldenConfigSetting instances.""" + + pk = django_forms.ModelMultipleChoiceField( + queryset=models.GoldenConfigSetting.objects.all(), widget=django_forms.MultipleHiddenInput + ) + + class Meta: + """Boilerplate form Meta data for GoldenConfigSetting.""" + + nullable_fields = [] + + +# Remediation Setting +class RemediationSettingForm(NautobotModelForm): + """Create/Update Form for Remediation Settings instances.""" + + class Meta: + """Boilerplate form Meta data for Remediation Settings.""" + + model = models.RemediationSetting + fields = "__all__" + + +class RemediationSettingFilterForm(NautobotFilterForm): + """Filter Form for Remediation Settings.""" + + model = models.RemediationSetting + q = django_forms.CharField(required=False, label="Search") + platform = forms.DynamicModelMultipleChoiceField( + queryset=Platform.objects.all(), required=False, display_field="name", to_field_name="name" + ) + remediation_type = django_forms.ChoiceField( + choices=forms.add_blank_choice(RemediationTypeChoice), + required=False, + widget=django_forms.Select(), + label="Remediation Type", + ) + + +class RemediationSettingBulkEditForm(NautobotBulkEditForm): + """BulkEdit form for RemediationSetting instances.""" + + pk = django_forms.ModelMultipleChoiceField( + queryset=models.RemediationSetting.objects.all(), widget=django_forms.MultipleHiddenInput + ) + remediation_type = django_forms.ChoiceField(choices=RemediationTypeChoice, label="Remediation Type") + + class Meta: + """Boilerplate form Meta data for RemediationSetting.""" + + nullable_fields = [] + + +# ConfigPlan + + +class ConfigPlanForm(NautobotModelForm): + """Form for ConfigPlan instances.""" + + feature = forms.DynamicModelMultipleChoiceField( + queryset=models.ComplianceFeature.objects.all(), + display_field="name", + help_text="Note: Selecting no features will generate plans for all applicable features.", + ) + commands = django_forms.CharField( + widget=django_forms.Textarea, + help_text=( + "Enter your configuration template here representing CLI configuration.
" + 'You may use Jinja2 templating. Example: {% if "foo" in bar %}foo{% endif %}
' + "You can also reference the device object with obj.
" + "For example: hostname {{ obj.name }} or ip address {{ obj.primary_ip4.host }}" + ), + ) + + tenant_group = forms.DynamicModelMultipleChoiceField(queryset=TenantGroup.objects.all(), required=False) + tenant = forms.DynamicModelMultipleChoiceField( + queryset=Tenant.objects.all(), required=False, query_params={"tenant_group": "$tenant_group"} + ) + # Requires https://github.com/nautobot/nautobot-app-golden-config/issues/430 + location = forms.DynamicModelMultipleChoiceField(queryset=Location.objects.all(), required=False) + rack_group = forms.DynamicModelMultipleChoiceField( + queryset=RackGroup.objects.all(), required=False, query_params={"location": "$location"} + ) + rack = forms.DynamicModelMultipleChoiceField( + queryset=Rack.objects.all(), required=False, query_params={"rack_group": "$rack_group", "location": "$location"} + ) + role = forms.DynamicModelMultipleChoiceField( + queryset=Role.objects.all(), required=False, query_params={"content_types": "dcim.device"} + ) + manufacturer = forms.DynamicModelMultipleChoiceField(queryset=Manufacturer.objects.all(), required=False) + platform = forms.DynamicModelMultipleChoiceField(queryset=Platform.objects.all(), required=False) + device_type = forms.DynamicModelMultipleChoiceField(queryset=DeviceType.objects.all(), required=False) + device = forms.DynamicModelMultipleChoiceField(queryset=Device.objects.all(), required=False) + tags = forms.DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), query_params={"content_types": "dcim.device"}, required=False + ) + status = forms.DynamicModelMultipleChoiceField( + queryset=Status.objects.all(), query_params={"content_types": "dcim.device"}, required=False + ) + + def __init__(self, *args, **kwargs): + """Method to get data from Python -> Django template -> JS in support of toggle form fields.""" + super().__init__(*args, **kwargs) + hide_form_data = [ + { + "event_field": "id_plan_type", + "values": [ + {"name": "manual", "show": ["id_commands"], "hide": ["id_feature"]}, + {"name": "missing", "show": ["id_feature"], "hide": ["id_commands"]}, + {"name": "intended", "show": ["id_feature"], "hide": ["id_commands"]}, + {"name": "remediation", "show": ["id_feature"], "hide": ["id_commands"]}, + {"name": "", "show": [], "hide": ["id_commands", "id_feature"]}, + ], + } + ] + # Example of how to use this `JSON.parse('{{ form.hide_form_data|safe }}')` + self.hide_form_data = json.dumps(hide_form_data) + + class Meta: + """Boilerplate form Meta data for ConfigPlan.""" + + model = models.ConfigPlan + fields = "__all__" + + +class ConfigPlanUpdateForm(NautobotModelForm): # pylint: disable=nb-sub-class-name + """Form for ConfigPlan instances.""" + + status = forms.DynamicModelChoiceField( + queryset=Status.objects.all(), + query_params={"content_types": models.ConfigPlan._meta.label_lower}, + ) + tags = forms.DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), query_params={"content_types": "dcim.device"}, required=False + ) + + class Meta: + """Boilerplate form Meta data for ConfigPlan.""" + + model = models.ConfigPlan + fields = ( # pylint: disable=nb-use-fields-all + "change_control_id", + "change_control_url", + "status", + "tags", + ) + + +class ConfigPlanFilterForm(DeviceRelatedFilterForm): + """Filter Form for ConfigPlan.""" + + model = models.ConfigPlan + + q = django_forms.CharField(required=False, label="Search") + # device_id = forms.DynamicModelMultipleChoiceField( + # queryset=Device.objects.all(), required=False, null_option="None", label="Device" + # ) + created__lte = django_forms.DateTimeField(label="Created Before", required=False, widget=forms.DatePicker()) + created__gte = django_forms.DateTimeField(label="Created After", required=False, widget=forms.DatePicker()) + plan_type = django_forms.ChoiceField( + choices=forms.add_blank_choice(ConfigPlanTypeChoice), + required=False, + widget=django_forms.Select(), + label="Plan Type", + ) + feature = forms.DynamicModelMultipleChoiceField( + queryset=models.ComplianceFeature.objects.all(), + required=False, + null_option="None", + label="Feature", + to_field_name="name", + ) + change_control_id = django_forms.CharField(required=False, label="Change Control ID") + plan_result_id = forms.DynamicModelMultipleChoiceField( + queryset=JobResult.objects.all(), + query_params={"job_model": "Generate Config Plans"}, + label="Plan Result", + required=False, + display_field="date_created", + ) + deploy_result_id = forms.DynamicModelMultipleChoiceField( + queryset=JobResult.objects.all(), + query_params={"job_model": "Deploy Config Plans"}, + label="Deploy Result", + required=False, + display_field="date_created", + ) + status = forms.DynamicModelMultipleChoiceField( + required=False, + queryset=Status.objects.all(), + query_params={"content_types": models.ConfigPlan._meta.label_lower}, + display_field="label", + label="Status", + to_field_name="name", + ) + tags = forms.TagFilterField(model) + + +class ConfigPlanBulkEditForm(NautobotBulkEditForm): + """BulkEdit form for ConfigPlan instances.""" + + pk = django_forms.ModelMultipleChoiceField( + queryset=models.ConfigPlan.objects.all(), widget=django_forms.MultipleHiddenInput + ) + status = forms.DynamicModelChoiceField( + queryset=Status.objects.all(), + query_params={"content_types": models.ConfigPlan._meta.label_lower}, + required=False, + ) + change_control_id = django_forms.CharField(required=False, label="Change Control ID") + change_control_url = django_forms.URLField(required=False, label="Change Control URL") + + class Meta: + """Boilerplate form Meta data for ConfigPlan.""" + + nullable_fields = [ + "change_control_id", + "change_control_url", + "tags", + ] + + +class GenerateIntendedConfigForm(django_forms.Form): + """Form for generating intended configuration.""" + + device = forms.DynamicModelChoiceField( + queryset=Device.objects.all(), + required=True, + label="Device", + ) + graphql_query = forms.DynamicModelChoiceField( + queryset=GraphQLQuery.objects.all(), + required=True, + label="GraphQL Query", + query_params={"nautobot_golden_config_graphql_query_variables": "device_id"}, + ) diff --git a/nautobot_golden_config/models.py b/nautobot_golden_config/models.py index afc72d2c7..bbedd8957 100644 --- a/nautobot_golden_config/models.py +++ b/nautobot_golden_config/models.py @@ -1,38 +1,842 @@ -"""Models for Golden Config.""" +"""Django Models for tracking the configuration compliance per feature and device.""" -# Django imports +import json +import logging +import os + +from deepdiff import DeepDiff +from django.core.exceptions import ValidationError from django.db import models +from django.db.models.manager import BaseManager +from django.utils.module_loading import import_string +from hier_config import Host as HierConfigHost +from nautobot.apps.models import RestrictedQuerySet +from nautobot.apps.utils import render_jinja2 +from nautobot.core.models.generics import PrimaryModel +from nautobot.core.models.utils import serialize_object, serialize_object_v2 +from nautobot.dcim.models import Device +from nautobot.extras.models import ObjectChange +from nautobot.extras.models.statuses import StatusField +from nautobot.extras.utils import extras_features +from netutils.config.compliance import feature_compliance +from xmldiff import actions, main + +from nautobot_golden_config.choices import ComplianceRuleConfigTypeChoice, ConfigPlanTypeChoice, RemediationTypeChoice +from nautobot_golden_config.utilities.constant import ENABLE_SOTAGG, PLUGIN_CFG + +LOGGER = logging.getLogger(__name__) +GRAPHQL_STR_START = "query ($device_id: ID!)" + +ERROR_MSG = ( + "There was an issue with the data that was returned by your get_custom_compliance function. " + "This is a local issue that requires the attention of your systems administrator and not something " + "that can be fixed within the Golden Config app. " +) +MISSING_MSG = ( + ERROR_MSG + "Specifically the `{}` key was not found in value the get_custom_compliance function provided." +) +VALIDATION_MSG = ( + ERROR_MSG + "Specifically the key {} was expected to be of type(s) {} and the value of {} was not that type(s)." +) + +CUSTOM_FUNCTIONS = { + "get_custom_compliance": "custom", + "get_custom_remediation": RemediationTypeChoice.TYPE_CUSTOM, +} + + +def _is_jsonable(val): + """Check is value can be converted to json.""" + try: + json.dumps(val) + return True + except (TypeError, OverflowError): + return False + + +def _null_to_empty(val): + """Convert to empty string if the value is currently null.""" + if not val: + return "" + return val + + +def _get_cli_compliance(obj): + """This function performs the actual compliance for cli configuration.""" + feature = { + "ordered": obj.rule.config_ordered, + "name": obj.rule, + } + feature.update({"section": obj.rule.match_config.splitlines()}) + value = feature_compliance( + feature, obj.actual, obj.intended, obj.device.platform.network_driver_mappings.get("netutils_parser") + ) + compliance = value["compliant"] + if compliance: + compliance_int = 1 + ordered = value["ordered_compliant"] + else: + compliance_int = 0 + ordered = value["ordered_compliant"] + missing = _null_to_empty(value["missing"]) + extra = _null_to_empty(value["extra"]) + return { + "compliance": compliance, + "compliance_int": compliance_int, + "ordered": ordered, + "missing": missing, + "extra": extra, + } + + +def _get_json_compliance(obj): + """This function performs the actual compliance for json serializable data.""" + + def _normalize_diff(diff, path_to_diff): + """Normalizes the diff to a list of keys and list indexes that have changed.""" + dictionary_items = list(diff.get(f"dictionary_item_{path_to_diff}", [])) + list_items = list(diff.get(f"iterable_item_{path_to_diff}", {}).keys()) + values_changed = list(diff.get("values_changed", {}).keys()) + type_changes = list(diff.get("type_changes", {}).keys()) + return dictionary_items + list_items + values_changed + type_changes + + diff = DeepDiff(obj.actual, obj.intended, ignore_order=obj.ordered, report_repetition=True) + if not diff: + compliance_int = 1 + compliance = True + ordered = True + missing = "" + extra = "" + else: + compliance_int = 0 + compliance = False + ordered = False + missing = _null_to_empty(_normalize_diff(diff, "added")) + extra = _null_to_empty(_normalize_diff(diff, "removed")) + + return { + "compliance": compliance, + "compliance_int": compliance_int, + "ordered": ordered, + "missing": missing, + "extra": extra, + } + -# Nautobot imports -from nautobot.apps.models import PrimaryModel +def _get_xml_compliance(obj): + """This function performs the actual compliance for xml serializable data.""" -# from nautobot.apps.models import extras_features -# If you want to use the extras_features decorator please reference the following documentation -# https://docs.nautobot.com/projects/core/en/latest/plugins/development/#using-the-extras_features-decorator-for-graphql -# Then based on your reading you may decide to put the following decorator before the declaration of your class -# @extras_features("custom_fields", "custom_validators", "relationships", "graphql") + def _normalize_diff(diff): + """Format the diff output to a list of nodes with values that have updated.""" + formatted_diff = [] + for operation in diff: + if isinstance(operation, actions.UpdateTextIn): + formatted_operation = f"{operation.node}, {operation.text}" + formatted_diff.append(formatted_operation) + return "\n".join(formatted_diff) + # Options for the diff operation. These are set to prefer updates over node insertions/deletions. + diff_options = { + "F": 0.1, + "fast_match": True, + } + missing = main.diff_texts(obj.actual, obj.intended, diff_options=diff_options) + extra = main.diff_texts(obj.intended, obj.actual, diff_options=diff_options) -# If you want to choose a specific model to overload in your class declaration, please reference the following documentation: -# how to chose a database model: https://docs.nautobot.com/projects/core/en/stable/plugins/development/#database-models + compliance = not missing and not extra + compliance_int = int(compliance) + ordered = obj.ordered + missing = _null_to_empty(_normalize_diff(missing)) + extra = _null_to_empty(_normalize_diff(extra)) + + return { + "compliance": compliance, + "compliance_int": compliance_int, + "ordered": ordered, + "missing": missing, + "extra": extra, + } + + +def _verify_get_custom_compliance_data(compliance_details): + """This function verifies the data is as expected when a custom function is used.""" + for val in ["compliance", "compliance_int", "ordered", "missing", "extra"]: + try: + compliance_details[val] + except KeyError: + raise ValidationError(MISSING_MSG.format(val)) from KeyError + for val in ["compliance", "ordered"]: + if compliance_details[val] not in [True, False]: + raise ValidationError(VALIDATION_MSG.format(val, "Boolean", compliance_details[val])) + if compliance_details["compliance_int"] not in [0, 1]: + raise ValidationError(VALIDATION_MSG.format("compliance_int", "0 or 1", compliance_details["compliance_int"])) + for val in ["missing", "extra"]: + if not isinstance(compliance_details[val], str) and not _is_jsonable(compliance_details[val]): + raise ValidationError(VALIDATION_MSG.format(val, "String or Json", compliance_details[val])) + + +def _get_hierconfig_remediation(obj): + """Returns the remediating config.""" + hierconfig_os = obj.device.platform.network_driver_mappings["hier_config"] + if not hierconfig_os: + raise ValidationError(f"platform {obj.network_driver} is not supported by hierconfig.") + + try: + remediation_setting_obj = RemediationSetting.objects.get(platform=obj.rule.platform) + except Exception as err: # pylint: disable=broad-except: + raise ValidationError(f"Platform {obj.network_driver} has no Remediation Settings defined.") from err + + remediation_options = remediation_setting_obj.remediation_options + + try: + hc_kwargs = {"hostname": obj.device.name, "os": hierconfig_os} + if remediation_options: + hc_kwargs.update(hconfig_options=remediation_options) + host = HierConfigHost(**hc_kwargs) + + except Exception as err: # pylint: disable=broad-except: + raise Exception( # pylint: disable=broad-exception-raised + f"Cannot instantiate HierConfig on {obj.device.name}, check Device, Platform and Hier Options." + ) from err + + host.load_generated_config(obj.intended) + host.load_running_config(obj.actual) + host.remediation_config() + remediation_config = host.remediation_config_filtered_text(include_tags={}, exclude_tags={}) + + return remediation_config + + +# The below maps the provided compliance types +FUNC_MAPPER = { + ComplianceRuleConfigTypeChoice.TYPE_CLI: _get_cli_compliance, + ComplianceRuleConfigTypeChoice.TYPE_JSON: _get_json_compliance, + ComplianceRuleConfigTypeChoice.TYPE_XML: _get_xml_compliance, + RemediationTypeChoice.TYPE_HIERCONFIG: _get_hierconfig_remediation, +} +# The below conditionally add the custom provided compliance type +for custom_function, custom_type in CUSTOM_FUNCTIONS.items(): + if PLUGIN_CFG.get(custom_function): + try: + FUNC_MAPPER[custom_type] = import_string(PLUGIN_CFG[custom_function]) + except Exception as error: # pylint: disable=broad-except + msg = ( + "There was an issue attempting to import the custom function of" + f"{PLUGIN_CFG[custom_function]}, this is expected with a local configuration issue " + "and not related to the Golden Configuration App, please contact your system admin for further details" + ) + raise Exception(msg).with_traceback(error.__traceback__) + + +@extras_features( + "custom_fields", + "custom_validators", + "export_templates", + "graphql", + "relationships", + "webhooks", +) class ComplianceFeature(PrimaryModel): # pylint: disable=too-many-ancestors - """Base model for Golden Config app.""" + """ComplianceFeature details.""" name = models.CharField(max_length=100, unique=True) + slug = models.SlugField(max_length=100, unique=True) description = models.CharField(max_length=200, blank=True) - # additional model fields class Meta: - """Meta class.""" + """Meta information for ComplianceFeature model.""" + + ordering = ("slug",) + + def __str__(self): + """Return a sane string representation of the instance.""" + return self.slug + + +@extras_features( + "custom_fields", + "custom_validators", + "export_templates", + "graphql", + "relationships", + "webhooks", +) +class ComplianceRule(PrimaryModel): # pylint: disable=too-many-ancestors + """ComplianceRule details.""" + + feature = models.ForeignKey(to="ComplianceFeature", on_delete=models.CASCADE, related_name="feature") + + platform = models.ForeignKey( + to="dcim.Platform", + on_delete=models.CASCADE, + related_name="compliance_rules", + ) + description = models.CharField( + max_length=200, + blank=True, + ) + config_ordered = models.BooleanField( + verbose_name="Configured Ordered", + help_text="Whether or not the configuration order matters, such as in ACLs.", + default=False, + ) + + config_remediation = models.BooleanField( + default=False, + verbose_name="Config Remediation", + help_text="Whether or not the config remediation is executed for this compliance rule.", + ) + + match_config = models.TextField( + blank=True, + verbose_name="Config to Match", + help_text="The config to match that is matched based on the parent most configuration. E.g.: For CLI `router bgp` or `ntp`. For JSON this is a top level key name. For XML this is a xpath query.", + ) + config_type = models.CharField( + max_length=20, + default=ComplianceRuleConfigTypeChoice.TYPE_CLI, + choices=ComplianceRuleConfigTypeChoice, + help_text="Whether the configuration is in CLI, JSON, or XML format.", + ) + custom_compliance = models.BooleanField( + default=False, help_text="Whether this Compliance Rule is proceeded as custom." + ) + + @property + def remediation_setting(self): + """Returns remediation settings for a particular platform.""" + return RemediationSetting.objects.filter(platform=self.platform).first() + + class Meta: + """Meta information for ComplianceRule model.""" + + ordering = ("platform", "feature__name") + unique_together = ( + "feature", + "platform", + ) + + def __str__(self): + """Return a sane string representation of the instance.""" + return f"{self.platform} - {self.feature.name}" + + def clean(self): + """Verify that if cli, then match_config is set.""" + if self.config_type == ComplianceRuleConfigTypeChoice.TYPE_CLI and not self.match_config: + raise ValidationError("CLI configuration set, but no configuration set to match.") + + +@extras_features( + "custom_fields", + "custom_links", + "custom_validators", + "export_templates", + "graphql", + "relationships", + "webhooks", +) +class ConfigCompliance(PrimaryModel): # pylint: disable=too-many-ancestors + """Configuration compliance details.""" + + device = models.ForeignKey(to="dcim.Device", on_delete=models.CASCADE, help_text="The device") + rule = models.ForeignKey(to="ComplianceRule", on_delete=models.CASCADE, related_name="rule") + compliance = models.BooleanField(blank=True) + actual = models.JSONField(blank=True, help_text="Actual Configuration for feature") + intended = models.JSONField(blank=True, help_text="Intended Configuration for feature") + # these three are config snippets exposed for the ConfigDeployment. + remediation = models.JSONField(blank=True, help_text="Remediation Configuration for the device") + missing = models.JSONField(blank=True, help_text="Configuration that should be on the device.") + extra = models.JSONField(blank=True, help_text="Configuration that should not be on the device.") + ordered = models.BooleanField(default=False) + # Used for django-pivot, both compliance and compliance_int should be set. + compliance_int = models.IntegerField(blank=True) - ordering = ["name"] + def to_objectchange(self, action, *, related_object=None, object_data_extra=None, object_data_exclude=None): # pylint: disable=arguments-differ + """Remove actual and intended configuration from changelog.""" + fields_to_exclude = ["actual", "intended"] + if not object_data_exclude: + object_data_exclude = fields_to_exclude + data_v2 = serialize_object_v2(self) + for field in fields_to_exclude: + data_v2.pop(field) + return ObjectChange( + changed_object=self, + object_repr=str(self), + action=action, + object_data=serialize_object(self, extra=object_data_extra, exclude=object_data_exclude), + object_data_v2=data_v2, + related_object=related_object, + ) - # Option for fixing capitalization (i.e. "Snmp" vs "SNMP") - # verbose_name = "Golden Config" + class Meta: + """Set unique together fields for model.""" - # Option for fixing plural name (i.e. "Chicken Tenders" vs "Chicken Tendies") - # verbose_name_plural = "Golden Configs" + ordering = ["device", "rule"] + unique_together = ("device", "rule") def __str__(self): - """Stringify instance.""" + """String representation of a the compliance.""" + return f"{self.device} -> {self.rule} -> {self.compliance}" + + def compliance_on_save(self): + """The actual configuration compliance happens here, but the details for actual compliance job would be found in FUNC_MAPPER.""" + if self.rule.custom_compliance: + if not FUNC_MAPPER.get("custom"): + raise ValidationError( + "Custom type provided, but no `get_custom_compliance` config set, please contact system admin." + ) + compliance_details = FUNC_MAPPER["custom"](obj=self) + _verify_get_custom_compliance_data(compliance_details) + else: + compliance_details = FUNC_MAPPER[self.rule.config_type](obj=self) + + self.compliance = compliance_details["compliance"] + self.compliance_int = compliance_details["compliance_int"] + self.ordered = compliance_details["ordered"] + self.missing = compliance_details["missing"] + self.extra = compliance_details["extra"] + + def remediation_on_save(self): + """The actual remediation happens here, before saving the object.""" + if self.compliance: + self.remediation = "" + return + + if not self.rule.config_remediation: + self.remediation = "" + return + + if not self.rule.remediation_setting: + self.remediation = "" + return + + remediation_config = FUNC_MAPPER[self.rule.remediation_setting.remediation_type](obj=self) + self.remediation = remediation_config + + def save(self, *args, **kwargs): + """The actual configuration compliance happens here, but the details for actual compliance job would be found in FUNC_MAPPER.""" + self.compliance_on_save() + self.remediation_on_save() + self.full_clean() + + # This accounts for django 4.2 `Setting update_fields in Model.save() may now be required` change + # in behavior + if kwargs.get("update_fields"): + kwargs["update_fields"].update( + {"compliance", "compliance_int", "ordered", "missing", "extra", "remediation"} + ) + + super().save(*args, **kwargs) + + +@extras_features( + "custom_fields", + "custom_links", + "custom_validators", + "export_templates", + "graphql", + "relationships", + "webhooks", +) +class GoldenConfig(PrimaryModel): # pylint: disable=too-many-ancestors + """Configuration Management Model.""" + + device = models.OneToOneField( + to="dcim.Device", + on_delete=models.CASCADE, + help_text="device", + blank=False, + ) + backup_config = models.TextField(blank=True, help_text="Full backup config for device.") + backup_last_attempt_date = models.DateTimeField(null=True, blank=True) + backup_last_success_date = models.DateTimeField(null=True, blank=True) + + intended_config = models.TextField(blank=True, help_text="Intended config for the device.") + intended_last_attempt_date = models.DateTimeField(null=True, blank=True) + intended_last_success_date = models.DateTimeField(null=True, blank=True) + + compliance_config = models.TextField(blank=True, help_text="Full config diff for device.") + compliance_last_attempt_date = models.DateTimeField(null=True, blank=True) + compliance_last_success_date = models.DateTimeField(null=True, blank=True) + + def to_objectchange(self, action, *, related_object=None, object_data_extra=None, object_data_exclude=None): # pylint: disable=arguments-differ + """Remove actual and intended configuration from changelog.""" + fields_to_exclude = ["backup_config", "intended_config", "compliance_config"] + if not object_data_exclude: + object_data_exclude = fields_to_exclude + data_v2 = serialize_object_v2(self) + for field in fields_to_exclude: + data_v2.pop(field) + return ObjectChange( + changed_object=self, + object_repr=str(self), + action=action, + object_data=serialize_object(self, extra=object_data_extra, exclude=object_data_exclude), + object_data_v2=data_v2, + related_object=related_object, + ) + + @staticmethod + def get_dynamic_group_device_pks(): + """Get all Device PKs associated with GoldenConfigSetting DynamicGroups.""" + gc_dynamic_group_device_queryset = Device.objects.none() + for setting in GoldenConfigSetting.objects.all(): + # using "|" should not require calling distinct afterwards + gc_dynamic_group_device_queryset = gc_dynamic_group_device_queryset | setting.dynamic_group.members + + return set(gc_dynamic_group_device_queryset.values_list("pk", flat=True)) + + @classmethod + def get_golden_config_device_ids(cls): + """Get all Device PKs associated with GoldenConfig entries.""" + return set(cls.objects.values_list("device__pk", flat=True)) + + class Meta: + """Set unique together fields for model.""" + + ordering = ["device"] + + def __str__(self): + """String representation of a the compliance.""" + return f"{self.device}" + + +class GoldenConfigSettingManager(BaseManager.from_queryset(RestrictedQuerySet)): + """Manager for GoldenConfigSetting.""" + + def get_for_device(self, device): + """Return the highest weighted GoldenConfigSetting assigned to a device.""" + if not isinstance(device, Device): + raise ValueError("The device argument must be a Device instance.") + dynamic_group = device.dynamic_groups.exclude(golden_config_setting__isnull=True) + if dynamic_group.exists(): + return dynamic_group.order_by("-golden_config_setting__weight").first().golden_config_setting + return None + + +@extras_features( + "graphql", +) +class GoldenConfigSetting(PrimaryModel): # pylint: disable=too-many-ancestors + """GoldenConfigSetting Model definition. This provides global configs instead of via configs.py.""" + + name = models.CharField(max_length=100, unique=True) + slug = models.SlugField(max_length=100, unique=True) + weight = models.PositiveSmallIntegerField(default=1000) + description = models.CharField( + max_length=200, + blank=True, + ) + backup_repository = models.ForeignKey( + to="extras.GitRepository", + on_delete=models.PROTECT, + null=True, + blank=True, + related_name="backup_repository", + limit_choices_to={"provided_contents__contains": "nautobot_golden_config.backupconfigs"}, + ) + backup_path_template = models.CharField( + max_length=255, + blank=True, + verbose_name="Backup Path in Jinja Template Form", + help_text="The Jinja path representation of where the backup file will be found. The variable `obj` is available as the device instance object of a given device, as is the case for all Jinja templates. e.g. `{{obj.location.name|slugify}}/{{obj.name}}.cfg`", + ) + intended_repository = models.ForeignKey( + to="extras.GitRepository", + on_delete=models.PROTECT, + null=True, + blank=True, + related_name="intended_repository", + limit_choices_to={"provided_contents__contains": "nautobot_golden_config.intendedconfigs"}, + ) + intended_path_template = models.CharField( + max_length=255, + blank=True, + verbose_name="Intended Path in Jinja Template Form", + help_text="The Jinja path representation of where the generated file will be placed. e.g. `{{obj.location.name|slugify}}/{{obj.name}}.cfg`", + ) + jinja_repository = models.ForeignKey( + to="extras.GitRepository", + on_delete=models.PROTECT, + null=True, + blank=True, + related_name="jinja_template", + limit_choices_to={"provided_contents__contains": "nautobot_golden_config.jinjatemplate"}, + ) + jinja_path_template = models.CharField( + max_length=255, + blank=True, + verbose_name="Template Path in Jinja Template Form", + help_text="The Jinja path representation of where the Jinja template can be found. e.g. `{{obj.platform.network_driver}}.j2`", + ) + backup_test_connectivity = models.BooleanField( + default=True, + verbose_name="Backup Test", + help_text="Whether or not to pretest the connectivity of the device by verifying there is a resolvable IP that can connect to port 22.", + ) + sot_agg_query = models.ForeignKey( + to="extras.GraphQLQuery", + on_delete=models.PROTECT, + null=True, + blank=True, + related_name="sot_aggregation", + ) + dynamic_group = models.OneToOneField( + to="extras.DynamicGroup", + on_delete=models.PROTECT, + related_name="golden_config_setting", + ) + is_dynamic_group_associable_model = False + + objects = GoldenConfigSettingManager() + + def __str__(self): + """Return a simple string if model is called.""" + return f"Golden Config Setting - {self.name}" + + class Meta: + """Set unique fields for model. + + Provide ordering used in tables and get_device_to_settings_map. + Sorting on weight is performed from the highest weight value to the lowest weight value. + This is to ensure only one app settings could be applied per single device based on priority and name. + """ + + verbose_name = "Golden Config Setting" + ordering = ["-weight", "name"] # Refer to weight comment in class docstring. + + def clean(self): + """Validate the scope and GraphQL query.""" + super().clean() + + if ENABLE_SOTAGG and not self.sot_agg_query: + raise ValidationError("A GraphQL query must be defined when `ENABLE_SOTAGG` is True") + + if self.sot_agg_query: + LOGGER.debug("GraphQL - test query start with: `%s`", GRAPHQL_STR_START) + if not str(self.sot_agg_query.query.lstrip()).startswith(GRAPHQL_STR_START): + raise ValidationError(f"The GraphQL query must start with exactly `{GRAPHQL_STR_START}`") + + def get_queryset(self): + """Generate a Device QuerySet from the filter.""" + return self.dynamic_group.members + + def device_count(self): + """Return the number of devices in the group.""" + return self.dynamic_group.count + + def get_url_to_filtered_device_list(self): + """Get url to all devices that are matching the filter.""" + return self.dynamic_group.get_group_members_url() + + def get_jinja_template_path_for_device(self, device): + """Get the Jinja template path for a device.""" + if self.jinja_repository is not None: + rendered_path = render_jinja2(template_code=self.jinja_path_template, context={"obj": device}) + return f"{self.jinja_repository.filesystem_path}{os.path.sep}{rendered_path}" + return None + + +@extras_features( + "custom_fields", + "custom_links", + "custom_validators", + "export_templates", + "graphql", + "relationships", + "webhooks", +) +class ConfigRemove(PrimaryModel): # pylint: disable=too-many-ancestors + """ConfigRemove for Regex Line Removals from Backup Configuration Model definition.""" + + name = models.CharField(max_length=255) + platform = models.ForeignKey( + to="dcim.Platform", + on_delete=models.CASCADE, + related_name="backup_line_remove", + ) + description = models.CharField( + max_length=200, + blank=True, + ) + regex = models.CharField( + max_length=200, + verbose_name="Regex Pattern", + help_text="Regex pattern used to remove a line from the backup configuration.", + ) + + clone_fields = ["platform", "description", "regex"] + + class Meta: + """Meta information for ConfigRemove model.""" + + ordering = ("platform", "name") + unique_together = ("name", "platform") + + def __str__(self): + """Return a simple string if model is called.""" return self.name + + +@extras_features( + "custom_fields", + "custom_links", + "custom_validators", + "export_templates", + "graphql", + "relationships", + "webhooks", +) +class ConfigReplace(PrimaryModel): # pylint: disable=too-many-ancestors + """ConfigReplace for Regex Line Replacements from Backup Configuration Model definition.""" + + name = models.CharField(max_length=255) + platform = models.ForeignKey( + to="dcim.Platform", + on_delete=models.CASCADE, + related_name="backup_line_replace", + ) + description = models.CharField( + max_length=200, + blank=True, + ) + regex = models.CharField( + max_length=200, + verbose_name="Regex Pattern to Substitute", + help_text="Regex pattern that will be found and replaced with 'replaced text'.", + ) + replace = models.CharField( + max_length=200, + verbose_name="Replaced Text", + help_text="Text that will be inserted in place of Regex pattern match.", + ) + + clone_fields = ["platform", "description", "regex", "replace"] + + class Meta: + """Meta information for ConfigReplace model.""" + + ordering = ("platform", "name") + unique_together = ("name", "platform") + + def __str__(self): + """Return a simple string if model is called.""" + return self.name + + +@extras_features( + "custom_fields", + "custom_links", + "custom_validators", + "export_templates", + "graphql", + "relationships", + "webhooks", +) +class RemediationSetting(PrimaryModel): # pylint: disable=too-many-ancestors + """RemediationSetting details.""" + + # Remediation points to the platform + platform = models.OneToOneField( + to="dcim.Platform", + on_delete=models.CASCADE, + related_name="remediation_settings", + ) + + remediation_type = models.CharField( + max_length=50, + default=RemediationTypeChoice.TYPE_HIERCONFIG, + choices=RemediationTypeChoice, + help_text="Whether the remediation setting is type HierConfig or custom.", + ) + + # takes options.json. + remediation_options = models.JSONField( + blank=True, + default=dict, + help_text="Remediation Configuration for the device", + ) + + csv_headers = [ + "platform", + "remediation_type", + ] + + class Meta: + """Meta information for RemediationSettings model.""" + + ordering = ("platform", "remediation_type") + + def to_csv(self): + """Indicates model fields to return as csv.""" + return ( + self.platform, + self.remediation_type, + ) + + def __str__(self): + """Return a sane string representation of the instance.""" + return str(self.platform) + + +@extras_features( + "custom_fields", + "custom_links", + "custom_validators", + "export_templates", + "graphql", + "relationships", + "webhooks", + "statuses", +) +class ConfigPlan(PrimaryModel): # pylint: disable=too-many-ancestors + """ConfigPlan for Golden Configuration Plan Model definition.""" + + plan_type = models.CharField(max_length=20, choices=ConfigPlanTypeChoice, verbose_name="Plan Type") + device = models.ForeignKey( + to="dcim.Device", + on_delete=models.CASCADE, + related_name="config_plan", + ) + config_set = models.TextField(help_text="Configuration set to be applied to device.") + feature = models.ManyToManyField( + to=ComplianceFeature, + related_name="config_plan", + blank=True, + ) + plan_result = models.ForeignKey( + to="extras.JobResult", + on_delete=models.CASCADE, + related_name="config_plan", + verbose_name="Plan Result", + ) + deploy_result = models.ForeignKey( + to="extras.JobResult", + on_delete=models.PROTECT, + related_name="config_plan_deploy_result", + verbose_name="Deploy Result", + blank=True, + null=True, + ) + change_control_id = models.CharField( + max_length=50, + blank=True, + verbose_name="Change Control ID", + help_text="Change Control ID for this configuration plan.", + ) + change_control_url = models.URLField(blank=True, verbose_name="Change Control URL") + status = StatusField(blank=True, null=True, on_delete=models.PROTECT) + + class Meta: + """Meta information for ConfigPlan model.""" + + ordering = ("-created", "device") + unique_together = ( + "plan_type", + "device", + "created", + ) + + def __str__(self): + """Return a simple string if model is called.""" + return f"{self.device.name}-{self.plan_type}-{self.created}" diff --git a/nautobot_golden_config/navigation.py b/nautobot_golden_config/navigation.py index e28f24c43..0b3aa46d2 100644 --- a/nautobot_golden_config/navigation.py +++ b/nautobot_golden_config/navigation.py @@ -1,24 +1,162 @@ -"""Menu items.""" +"""Add the configuration compliance buttons to the Apps Navigation.""" from nautobot.apps.ui import NavMenuAddButton, NavMenuGroup, NavMenuItem, NavMenuTab -items = ( +from nautobot_golden_config.utilities.constant import ENABLE_BACKUP, ENABLE_COMPLIANCE, ENABLE_PLAN + +items_operate = [ NavMenuItem( - link="plugins:nautobot_golden_config:compliancefeature_list", - name="Golden Config", - permissions=["nautobot_golden_config.view_compliancefeature"], + link="plugins:nautobot_golden_config:goldenconfig_list", + name="Config Overview", + permissions=["nautobot_golden_config.view_goldenconfig"], + ) +] + +items_setup = [] + +if ENABLE_COMPLIANCE: + items_operate.append( + NavMenuItem( + link="plugins:nautobot_golden_config:configcompliance_list", + name="Config Compliance", + permissions=["nautobot_golden_config.view_configcompliance"], + ) + ) + +if ENABLE_COMPLIANCE: + items_setup.append( + NavMenuItem( + link="plugins:nautobot_golden_config:compliancerule_list", + name="Compliance Rules", + permissions=["nautobot_golden_config.view_compliancerule"], + buttons=( + NavMenuAddButton( + link="plugins:nautobot_golden_config:compliancerule_add", + permissions=["nautobot_golden_config.add_compliancerule"], + ), + ), + ) + ) + +if ENABLE_COMPLIANCE: + items_setup.append( + NavMenuItem( + link="plugins:nautobot_golden_config:compliancefeature_list", + name="Compliance Features", + permissions=["nautobot_golden_config.view_compliancefeature"], + buttons=( + NavMenuAddButton( + link="plugins:nautobot_golden_config:compliancefeature_add", + permissions=["nautobot_golden_config.add_compliancefeature"], + ), + ), + ) + ) + + +if ENABLE_COMPLIANCE: + items_operate.append( + NavMenuItem( + link="plugins:nautobot_golden_config:configcompliance_overview", + name="Compliance Report", + permissions=["nautobot_golden_config.view_configcompliance"], + ) + ) + +if ENABLE_PLAN: + items_operate.append( + NavMenuItem( + link="plugins:nautobot_golden_config:configplan_list", + name="Config Plans", + permissions=["nautobot_golden_config.view_configplan"], + buttons=( + NavMenuAddButton( + link="plugins:nautobot_golden_config:configplan_add", + permissions=["nautobot_golden_config.add_configplan"], + ), + ), + ) + ) + +if ENABLE_BACKUP: + items_setup.append( + NavMenuItem( + link="plugins:nautobot_golden_config:configremove_list", + name="Config Removals", + permissions=["nautobot_golden_config.view_configremove"], + buttons=( + NavMenuAddButton( + link="plugins:nautobot_golden_config:configremove_add", + permissions=["nautobot_golden_config.add_configremove"], + ), + ), + ) + ) + +if ENABLE_BACKUP: + items_setup.append( + NavMenuItem( + link="plugins:nautobot_golden_config:configreplace_list", + name="Config Replacements", + permissions=["nautobot_golden_config.view_configreplace"], + buttons=( + NavMenuAddButton( + link="plugins:nautobot_golden_config:configreplace_add", + permissions=["nautobot_golden_config.add_configreplace"], + ), + ), + ) + ) + + +if ENABLE_COMPLIANCE: + items_setup.append( + NavMenuItem( + link="plugins:nautobot_golden_config:remediationsetting_list", + name="Remediation Settings", + permissions=["nautobot_golden_config.view_remediationsetting"], + buttons=( + NavMenuAddButton( + link="plugins:nautobot_golden_config:remediationsetting_add", + permissions=["nautobot_golden_config.add_remediationsetting"], + ), + ), + ) + ) + +items_setup.append( + NavMenuItem( + link="plugins:nautobot_golden_config:goldenconfigsetting_list", + name="Golden Config Settings", + permissions=["nautobot_golden_config.view_goldenconfigsetting"], buttons=( NavMenuAddButton( - link="plugins:nautobot_golden_config:compliancefeature_add", - permissions=["nautobot_golden_config.add_compliancefeature"], + link="plugins:nautobot_golden_config:goldenconfigsetting_add", + permissions=["nautobot_golden_config.change_goldenconfigsetting"], ), ), ), ) + menu_items = ( NavMenuTab( - name="Apps", - groups=(NavMenuGroup(name="Golden Config", items=tuple(items)),), + name="Golden Config", + weight=1000, + groups=( + NavMenuGroup(name="Manage", weight=100, items=tuple(items_operate)), + NavMenuGroup(name="Setup", weight=100, items=tuple(items_setup)), + NavMenuGroup( + name="Tools", + weight=300, + items=( + NavMenuItem( + link="plugins:nautobot_golden_config:generate_intended_config", + name="Generate Intended Config", + permissions=["dcim.view_device", "extras.view_gitrepository"], + ), + ), + ), + ), ), ) diff --git a/nautobot_golden_config/tables.py b/nautobot_golden_config/tables.py index e662cb33d..57adbaaf2 100644 --- a/nautobot_golden_config/tables.py +++ b/nautobot_golden_config/tables.py @@ -1,38 +1,532 @@ -"""Tables for nautobot_golden_config.""" +"""Django Tables2 classes for golden_config app.""" -import django_tables2 as tables -from nautobot.apps.tables import BaseTable, ButtonsColumn, ToggleColumn +import copy + +from django.utils.html import format_html +from django_tables2 import Column, LinkColumn, TemplateColumn +from django_tables2.utils import A +from nautobot.apps.tables import BaseTable, BooleanColumn, TagColumn, ToggleColumn +from nautobot.extras.tables import StatusTableMixin from nautobot_golden_config import models +from nautobot_golden_config.utilities.constant import CONFIG_FEATURES, ENABLE_BACKUP, ENABLE_COMPLIANCE, ENABLE_INTENDED + +ALL_ACTIONS = """ +{% if backup == True %} + {% if record.config_type == 'json' %} + + {% else %} + {% if record.backup_config %} + + + + {% else %} + + {% endif %} + {% endif %} +{% endif %} +{% if intended == True %} + {% if record.config_type == 'json' %} + + {% else %} + {% if record.intended_config %} + + + + {% else %} + + {% endif %} + {% endif %} +{% endif %} +{% if postprocessing == True %} + {% if record.intended_config %} + + + + {% else %} + + {% endif %} +{% endif %} +{% if compliance == True %} + {% if record.intended_config and record.backup_config %} + + + + {% else %} + + {% endif %} +{% endif %} +{% if sotagg == True %} + + + + {% if record.config_type == 'json' %} + + {% else %} + + + + + + {% endif %} +{% endif %} +""" + +CONFIG_SET_BUTTON = """ + + + + +""" + +MATCH_CONFIG = """{{ record.match_config|linebreaksbr }}""" + + +def actual_fields(): + """Convienance function to conditionally toggle columns.""" + active_fields = ["pk", "name"] + if ENABLE_BACKUP: + active_fields.append("backup_last_success_date") + if ENABLE_INTENDED: + active_fields.append("intended_last_success_date") + if ENABLE_COMPLIANCE: + active_fields.append("compliance_last_success_date") + active_fields.append("actions") + return tuple(active_fields) + + +# +# Columns +# + + +class PercentageColumn(Column): + """Column used to display percentage.""" + + def render(self, value): + """Render percentage value.""" + return f"{value} %" + + +class ComplianceColumn(Column): + """Column used to display config compliance status (True/False/None).""" + + def render(self, value): + """Render an entry in this column.""" + if value == 1: # pylint: disable=no-else-return + return format_html('') + elif value == 0: + return format_html('') + else: # value is None + return format_html('') + + +# +# Tables +# + + +# ConfigCompliance +class ConfigComplianceTable(BaseTable): + """Table for rendering a listing of Device entries and their associated ConfigCompliance record status.""" + + pk = ToggleColumn(accessor=A("device")) + device = TemplateColumn( + template_code="""{{ record.device__name }} """ + ) + + def __init__(self, *args, **kwargs): + """Override default values to dynamically add columns.""" + # Used ConfigCompliance.objects on purpose, vs queryset (set in args[0]), as there were issues with that as + # well as not as expected from user standpoint (e.g. not always the same values on columns depending on + # filtering) + features = list( + models.ConfigCompliance.objects.order_by("rule__feature__slug") + .values_list("rule__feature__slug", flat=True) + .distinct() + ) + extra_columns = [(feature, ComplianceColumn(verbose_name=feature)) for feature in features] + kwargs["extra_columns"] = extra_columns + # Nautobot's BaseTable.configurable_columns() only recognizes columns in self.base_columns, + # so override the class's base_columns to include our additional columns as configurable. + self.base_columns = copy.deepcopy(self.base_columns) + for feature, column in extra_columns: + self.base_columns[feature] = column + super().__init__(*args, **kwargs) + + class Meta(BaseTable.Meta): + """Metaclass attributes of ConfigComplianceTable.""" + + model = models.ConfigCompliance + fields = ( + "pk", + "device", + ) + # All other fields (ConfigCompliance names) are constructed dynamically at instantiation time - see views.py + + +class ConfigComplianceGlobalFeatureTable(BaseTable): # pylint: disable=nb-sub-class-name + """Table for feature compliance report.""" + + name = Column(accessor="rule__feature__slug", verbose_name="Feature") + count = Column(accessor="count", verbose_name="Total") + compliant = Column(accessor="compliant", verbose_name="Compliant") + non_compliant = Column(accessor="non_compliant", verbose_name="Non-Compliant") + comp_percent = PercentageColumn(accessor="comp_percent", verbose_name="Compliance (%)") + + class Meta(BaseTable.Meta): + """Metaclass attributes of ConfigComplianceGlobalFeatureTable.""" + + model = models.ConfigCompliance + fields = ["name", "count", "compliant", "non_compliant", "comp_percent"] + default_columns = [ + "name", + "count", + "compliant", + "non_compliant", + "comp_percent", + ] + + +class ConfigComplianceDeleteTable(BaseTable): # pylint: disable=nb-sub-class-name + """Table for device compliance report.""" + + feature = Column(accessor="rule__feature__name", verbose_name="Feature") + + class Meta(BaseTable.Meta): + """Metaclass attributes of ConfigComplianceDeleteTable.""" + + device = Column(accessor="device__name", verbose_name="Device Name") + model = models.ConfigCompliance + fields = ("device", "feature") + + +class DeleteGoldenConfigTable(BaseTable): # pylint: disable=nb-sub-class-name + """ + Table used in bulk delete confirmation. + + This is required since there model is different when deleting the record compared to when viewing the records initially via Device. + """ + + pk = ToggleColumn() + + def __init__(self, *args, **kwargs): + """Remove all fields from showing except device .""" + super().__init__(*args, **kwargs) + for feature in list(self.base_columns.keys()): # pylint: disable=no-member + if feature not in ["pk", "device"]: + self.base_columns.pop(feature) # pylint: disable=no-member + self.sequence.remove(feature) + + class Meta(BaseTable.Meta): + """Meta for class DeleteGoldenConfigTable.""" + + model = models.GoldenConfig + + +# GoldenConfig + + +class GoldenConfigTable(BaseTable): + """Table to display Config Management Status.""" + + pk = ToggleColumn() + name = LinkColumn( + "plugins:nautobot_golden_config:goldenconfig", + args=[A("pk")], + text=lambda record: record.device.name, + verbose_name="Device", + ) + + if ENABLE_BACKUP: + backup_last_success_date = Column( + verbose_name="Backup Status", empty_values=(), order_by="backup_last_success_date" + ) + if ENABLE_INTENDED: + intended_last_success_date = Column( + verbose_name="Intended Status", + empty_values=(), + order_by="intended_last_success_date", + ) + if ENABLE_COMPLIANCE: + compliance_last_success_date = Column( + verbose_name="Compliance Status", + empty_values=(), + order_by="compliance_last_success_date", + ) + + actions = TemplateColumn( + template_code=ALL_ACTIONS, verbose_name="Actions", extra_context=CONFIG_FEATURES, orderable=False + ) + + def _render_last_success_date(self, record, column, value): + """Abstract method to get last success per row record.""" + last_success_date = getattr(record, f"{value}_last_success_date", None) + last_attempt_date = getattr(record, f"{value}_last_attempt_date", None) + if not last_success_date or not last_attempt_date: + column.attrs = {"td": {"style": "color:black"}} + return "--" + if not last_success_date and not last_attempt_date: + column.attrs = {"td": {"style": "color:black"}} + return "--" + if last_success_date and last_attempt_date == last_success_date: + column.attrs = {"td": {"style": "color:green"}} + return last_success_date + column.attrs = {"td": {"style": "color:red"}} + return last_success_date + + def render_backup_last_success_date(self, record, column): + """Pull back backup last success per row record.""" + return self._render_last_success_date(record, column, "backup") + + def render_intended_last_success_date(self, record, column): + """Pull back intended last success per row record.""" + return self._render_last_success_date(record, column, "intended") + + def render_compliance_last_success_date(self, record, column): + """Pull back compliance last success per row record.""" + return self._render_last_success_date(record, column, "compliance") + + class Meta(BaseTable.Meta): + """Meta for class GoldenConfigTable.""" + + model = models.GoldenConfig + fields = actual_fields() + + +# ComplianceFeature class ComplianceFeatureTable(BaseTable): + """Table to display Compliance Features.""" + + pk = ToggleColumn() + name = LinkColumn("plugins:nautobot_golden_config:compliancefeature", args=[A("pk")]) + + class Meta(BaseTable.Meta): + """Table to display Compliance Features Meta Data.""" + + model = models.ComplianceFeature + fields = ("pk", "name", "slug", "description") + default_columns = ("pk", "name", "slug", "description") + + +# ComplianceRule + + +class ComplianceRuleTable(BaseTable): + """Table to display Compliance Rules.""" + + pk = ToggleColumn() + feature = LinkColumn("plugins:nautobot_golden_config:compliancerule", args=[A("pk")]) + match_config = TemplateColumn(template_code=MATCH_CONFIG) + config_ordered = BooleanColumn() + custom_compliance = BooleanColumn() + config_remediation = BooleanColumn() + + class Meta(BaseTable.Meta): + """Table to display Compliance Rules Meta Data.""" + + model = models.ComplianceRule + fields = ( + "pk", + "feature", + "platform", + "description", + "config_ordered", + "match_config", + "config_type", + "custom_compliance", + "config_remediation", + ) + default_columns = ( + "pk", + "feature", + "platform", + "description", + "config_ordered", + "match_config", + "config_type", + "custom_compliance", + "config_remediation", + ) + + +# ConfigRemove + + +class ConfigRemoveTable(BaseTable): + """Table to display Compliance Rules.""" + + pk = ToggleColumn() + name = LinkColumn("plugins:nautobot_golden_config:configremove", args=[A("pk")]) + + class Meta(BaseTable.Meta): + """Table to display Compliance Rules Meta Data.""" + + model = models.ConfigRemove + fields = ("pk", "name", "platform", "description", "regex") + default_columns = ("pk", "name", "platform", "description", "regex") + + +# ConfigReplace + + +class ConfigReplaceTable(BaseTable): + """Table to display Compliance Rules.""" + + pk = ToggleColumn() + name = LinkColumn("plugins:nautobot_golden_config:configreplace", args=[A("pk")]) + + class Meta(BaseTable.Meta): + """Table to display Compliance Rules Meta Data.""" + + model = models.ConfigReplace + fields = ("pk", "name", "platform", "description", "regex", "replace") + default_columns = ("pk", "name", "platform", "description", "regex", "replace") + + +class GoldenConfigSettingTable(BaseTable): # pylint: disable=R0903 """Table for list view.""" pk = ToggleColumn() - name = tables.Column(linkify=True) - actions = ButtonsColumn( - models.ComplianceFeature, - # Option for modifying the default action buttons on each row: - # buttons=("changelog", "edit", "delete"), - # Option for modifying the pk for the action buttons: - pk_field="pk", + name = Column(order_by=("_name",), linkify=True) + jinja_repository = Column( + verbose_name="Jinja Repository", + empty_values=(), + ) + intended_repository = Column( + verbose_name="Intended Repository", + empty_values=(), + ) + backup_repository = Column( + verbose_name="Backup Repository", + empty_values=(), ) + def _render_capability(self, record, column, record_attribute): # pylint: disable=unused-argument + if getattr(record, record_attribute, None): + return format_html('') + return format_html('') + + def render_backup_repository(self, record, column): + """Render backup repository boolean value.""" + return self._render_capability(record=record, column=column, record_attribute="backup_repository") + + def render_intended_repository(self, record, column): + """Render intended repository boolean value.""" + return self._render_capability(record=record, column=column, record_attribute="intended_repository") + + def render_jinja_repository(self, record, column): + """Render jinja repository boolean value.""" + return self._render_capability(record=record, column=column, record_attribute="jinja_repository") + class Meta(BaseTable.Meta): """Meta attributes.""" - model = models.ComplianceFeature + model = models.GoldenConfigSetting fields = ( "pk", "name", + "weight", "description", + "backup_repository", + "intended_repository", + "jinja_repository", ) - # Option for modifying the columns that show up in the list view by default: - # default_columns = ( - # "pk", - # "name", - # "description", - # ) + +class RemediationSettingTable(BaseTable): + """Table to display RemediationSetting Rules.""" + + pk = ToggleColumn() + platform = LinkColumn("plugins:nautobot_golden_config:remediationsetting", args=[A("pk")]) + + class Meta(BaseTable.Meta): + """Table to display RemediationSetting Meta Data.""" + + model = models.RemediationSetting + fields = ("pk", "platform", "remediation_type") + default_columns = ("pk", "platform", "remediation_type") + + +# ConfigPlan + + +class ConfigPlanTable(StatusTableMixin, BaseTable): + """Table to display Config Plans.""" + + pk = ToggleColumn() + device = LinkColumn("plugins:nautobot_golden_config:configplan", args=[A("pk")]) + plan_result = TemplateColumn( + template_code="""""" + ) + deploy_result = TemplateColumn( + template_code=""" + {% if record.deploy_result %} + + {% else %} + — + {% endif %} + """ + ) + config_set = TemplateColumn(template_code=CONFIG_SET_BUTTON, verbose_name="Config Set", orderable=False) + tags = TagColumn(url_name="plugins:nautobot_golden_config:configplan_list") + + class Meta(BaseTable.Meta): + """Table to display Config Plans Meta Data.""" + + model = models.ConfigPlan + fields = ( + "pk", + "device", + "created", + "plan_type", + "feature", + "change_control_id", + "change_control_url", + "plan_result", + "deploy_result", + "config_set", + "status", + "tags", + ) + default_columns = ( + "pk", + "device", + "created", + "plan_type", + "feature", + "change_control_id", + "change_control_url", + "plan_result", + "deploy_result", + "config_set", + "status", + ) diff --git a/nautobot_golden_config/templates/nautobot_golden_config/compliancefeature_retrieve.html b/nautobot_golden_config/templates/nautobot_golden_config/compliancefeature_retrieve.html index 8832fd432..72efc8110 100644 --- a/nautobot_golden_config/templates/nautobot_golden_config/compliancefeature_retrieve.html +++ b/nautobot_golden_config/templates/nautobot_golden_config/compliancefeature_retrieve.html @@ -1,26 +1,37 @@ - -{% extends 'generic/object_retrieve.html' %} +{% extends 'generic/object_detail.html' %} {% load helpers %} +{% load buttons %} + +{% block buttons %} + {% if perms.nautobot_golden_config.add_compliancefeature %} + {% clone_button object %} + {% endif %} + {% if perms.nautobot_golden_config.change_compliancefeature %} + {% edit_button object key="pk" %} + {% endif %} + {% if perms.nautobot_golden_config.delete_compliancefeature %} + {% delete_button object key="pk" %} + {% endif %} +{% endblock buttons %} {% block content_left_page %}
- ComplianceFeature + Compliance Feature Details
- + + + + + - +
Name - {{ object.name }} - {{ object.name }}
Slug{{ object.slug }}
Description - {{ object.description|placeholder }} - {{ object.description|placeholder }}
-{% endblock content_left_page %} - +{% endblock %} \ No newline at end of file diff --git a/nautobot_golden_config/tests/fixtures.py b/nautobot_golden_config/tests/fixtures.py deleted file mode 100644 index 2e8f570b7..000000000 --- a/nautobot_golden_config/tests/fixtures.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Create fixtures for tests.""" - -from nautobot_golden_config.models import ComplianceFeature - - -def create_compliancefeature(): - """Fixture to create necessary number of ComplianceFeature for tests.""" - ComplianceFeature.objects.create(name="Test One") - ComplianceFeature.objects.create(name="Test Two") - ComplianceFeature.objects.create(name="Test Three") diff --git a/nautobot_golden_config/tests/test_api_views.py b/nautobot_golden_config/tests/test_api_views.py deleted file mode 100644 index d6aeed74a..000000000 --- a/nautobot_golden_config/tests/test_api_views.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Unit tests for nautobot_golden_config.""" - -from nautobot.apps.testing import APIViewTestCases - -from nautobot_golden_config import models -from nautobot_golden_config.tests import fixtures - - -class ComplianceFeatureAPIViewTest(APIViewTestCases.APIViewTestCase): - # pylint: disable=too-many-ancestors - """Test the API viewsets for ComplianceFeature.""" - - model = models.ComplianceFeature - create_data = [ - { - "name": "Test Model 1", - "description": "test description", - }, - { - "name": "Test Model 2", - }, - ] - bulk_update_data = {"description": "Test Bulk Update"} - - @classmethod - def setUpTestData(cls): - fixtures.create_compliancefeature() diff --git a/nautobot_golden_config/tests/test_filter_compliancefeature.py b/nautobot_golden_config/tests/test_filter_compliancefeature.py deleted file mode 100644 index 552778574..000000000 --- a/nautobot_golden_config/tests/test_filter_compliancefeature.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Test ComplianceFeature Filter.""" - -from django.test import TestCase - -from nautobot_golden_config import filters, models -from nautobot_golden_config.tests import fixtures - - -class ComplianceFeatureFilterTestCase(TestCase): - """ComplianceFeature Filter Test Case.""" - - queryset = models.ComplianceFeature.objects.all() - filterset = filters.ComplianceFeatureFilterSet - - @classmethod - def setUpTestData(cls): - """Setup test data for ComplianceFeature Model.""" - fixtures.create_compliancefeature() - - def test_q_search_name(self): - """Test using Q search with name of ComplianceFeature.""" - params = {"q": "Test One"} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) - - def test_q_invalid(self): - """Test using invalid Q search for ComplianceFeature.""" - params = {"q": "test-five"} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0) diff --git a/nautobot_golden_config/tests/test_form_compliancefeature.py b/nautobot_golden_config/tests/test_form_compliancefeature.py deleted file mode 100644 index cdeb6daa7..000000000 --- a/nautobot_golden_config/tests/test_form_compliancefeature.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Test compliancefeature forms.""" - -from django.test import TestCase - -from nautobot_golden_config import forms - - -class ComplianceFeatureTest(TestCase): - """Test ComplianceFeature forms.""" - - def test_specifying_all_fields_success(self): - form = forms.ComplianceFeatureForm( - data={ - "name": "Development", - "description": "Development Testing", - } - ) - self.assertTrue(form.is_valid()) - self.assertTrue(form.save()) - - def test_specifying_only_required_success(self): - form = forms.ComplianceFeatureForm( - data={ - "name": "Development", - } - ) - self.assertTrue(form.is_valid()) - self.assertTrue(form.save()) - - def test_validate_name_compliancefeature_is_required(self): - form = forms.ComplianceFeatureForm(data={"description": "Development Testing"}) - self.assertFalse(form.is_valid()) - self.assertIn("This field is required.", form.errors["name"]) diff --git a/nautobot_golden_config/tests/test_model_compliancefeature.py b/nautobot_golden_config/tests/test_model_compliancefeature.py deleted file mode 100644 index 819b52647..000000000 --- a/nautobot_golden_config/tests/test_model_compliancefeature.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Test ComplianceFeature.""" - -from django.test import TestCase - -from nautobot_golden_config import models - - -class TestComplianceFeature(TestCase): - """Test ComplianceFeature.""" - - def test_create_compliancefeature_only_required(self): - """Create with only required fields, and validate null description and __str__.""" - compliancefeature = models.ComplianceFeature.objects.create(name="Development") - self.assertEqual(compliancefeature.name, "Development") - self.assertEqual(compliancefeature.description, "") - self.assertEqual(str(compliancefeature), "Development") - - def test_create_compliancefeature_all_fields_success(self): - """Create ComplianceFeature with all fields.""" - compliancefeature = models.ComplianceFeature.objects.create(name="Development", description="Development Test") - self.assertEqual(compliancefeature.name, "Development") - self.assertEqual(compliancefeature.description, "Development Test") diff --git a/nautobot_golden_config/tests/test_views.py b/nautobot_golden_config/tests/test_views.py index 02d5e7aaa..937826b7f 100644 --- a/nautobot_golden_config/tests/test_views.py +++ b/nautobot_golden_config/tests/test_views.py @@ -1,28 +1,395 @@ -"""Unit tests for views.""" +"""Unit tests for nautobot_golden_config views.""" -from nautobot.apps.testing import ViewTestCases +import datetime +from unittest import mock, skip -from nautobot_golden_config import models -from nautobot_golden_config.tests import fixtures +import nautobot +from django.contrib.auth import get_user_model +from django.contrib.contenttypes.models import ContentType +from django.test import RequestFactory, override_settings +from django.urls import reverse +from lxml import html +from nautobot.apps.models import RestrictedQuerySet +from nautobot.apps.testing import TestCase, ViewTestCases +from nautobot.dcim.models import Device +from nautobot.extras.models import Relationship, RelationshipAssociation, Status +from nautobot_golden_config import models, views -class ComplianceFeatureViewTest(ViewTestCases.PrimaryObjectViewTestCase): - # pylint: disable=too-many-ancestors - """Test the ComplianceFeature views.""" +from .conftest import create_device_data, create_feature_rule_json, create_job_result - model = models.ComplianceFeature - bulk_edit_data = {"description": "Bulk edit views"} - form_data = { - "name": "Test 1", - "description": "Initial model", +User = get_user_model() + + +@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"]) +class ConfigComplianceOverviewHelperTestCase(TestCase): + """Test ConfigComplianceOverviewHelper.""" + + @classmethod + def setUpTestData(cls): + """Set up base objects.""" + create_device_data() + dev01 = Device.objects.get(name="Device 1") + dev02 = Device.objects.get(name="Device 2") + dev03 = Device.objects.get(name="Device 3") + dev04 = Device.objects.get(name="Device 4") + + feature_dev01 = create_feature_rule_json(dev01) + feature_dev02 = create_feature_rule_json(dev02) + feature_dev03 = create_feature_rule_json(dev03) + + updates = [ + {"device": dev01, "feature": feature_dev01}, + {"device": dev02, "feature": feature_dev02}, + {"device": dev03, "feature": feature_dev03}, + {"device": dev04, "feature": feature_dev01}, + ] + for update in updates: + models.ConfigCompliance.objects.create( + device=update["device"], + rule=update["feature"], + actual={"foo": {"bar-1": "baz"}}, + intended={"foo": {"bar-2": "baz"}}, + ) + + # TODO: 2.0 turn this back on. + # cls.ccoh = views.ConfigComplianceOverviewOverviewHelper + + def test_plot_visual_no_devices(self): + # TODO: 2.0 turn this back on. + self.assertEqual(True, True) + # aggr = {"comp_percents": 0, "compliants": 0, "non_compliants": 0, "total": 0} + # self.assertEqual(self.ccoh.plot_visual(aggr), None) + + @mock.patch.dict("nautobot_golden_config.tables.CONFIG_FEATURES", {"sotagg": True}) + def test_config_compliance_list_view_with_sotagg_enabled(self): + models.GoldenConfig.objects.create(device=Device.objects.first()) + request = self.client.get("/plugins/golden-config/golden-config/") + self.assertContains(request, '') + + @mock.patch.dict("nautobot_golden_config.tables.CONFIG_FEATURES", {"sotagg": False}) + def test_config_compliance_list_view_with_sotagg_disabled(self): + models.GoldenConfig.objects.create(device=Device.objects.first()) + request = self.client.get("/plugins/golden-config/golden-config/") + self.assertNotContains(request, '') + + @mock.patch.object(views, "graph_ql_query") + @mock.patch.object(views, "get_device_to_settings_map") + @mock.patch("nautobot_golden_config.models.GoldenConfigSetting") + def test_config_compliance_details_sotagg_error( + self, mock_gc_setting, mock_get_device_to_settings_map, mock_graphql_query + ): + device = Device.objects.first() + mock_gc_setting.sot_agg_query = None + mock_get_device_to_settings_map.return_value = {device.id: mock_gc_setting} + request = self.client.get(f"/plugins/golden-config/golden-config/{device.pk}/sotagg/") + expected = "{\n "Error": "No saved `GraphQL Query` query was configured in the `Golden Config Setting`"\n}" + self.assertContains(request, expected) + mock_graphql_query.assert_not_called() + + @mock.patch.object(views, "graph_ql_query") + @mock.patch.object(views, "get_device_to_settings_map") + @mock.patch("nautobot_golden_config.models.GoldenConfigSetting") + def test_config_compliance_details_sotagg_no_error( + self, mock_gc_setting, mock_get_device_to_settings_map, mock_graph_ql_query + ): + device = Device.objects.first() + mock_get_device_to_settings_map.return_value = {device.id: mock_gc_setting} + mock_graph_ql_query.return_value = ("discard value", "This is a mock graphql result") + request = self.client.get(f"/plugins/golden-config/golden-config/{device.pk}/sotagg/") + expected = "This is a mock graphql result" + self.assertContains(request, expected) + mock_graph_ql_query.assert_called() + + +class ConfigReplaceUIViewSetTestCase(ViewTestCases.PrimaryObjectViewTestCase): # pylint: disable=too-many-ancestors + """Test ConfigReplaceUIViewSet.""" + + model = models.ConfigReplace + + bulk_edit_data = { + "description": "new description", } - csv_data = ( - "name", - "Test csv1", - "Test csv2", - "Test csv3", - ) @classmethod def setUpTestData(cls): - fixtures.create_compliancefeature() + """Set up base objects.""" + create_device_data() + platform = Device.objects.first().platform + for num in range(3): + models.ConfigReplace.objects.create( + name=f"test configreplace {num}", + platform=platform, + description="test description", + regex="^(.*)$", + replace="xyz", + ) + cls.form_data = { + "name": "new name", + "platform": platform.pk, + "description": "new description", + "regex": "^NEW (.*)$", + "replace": "NEW replaced text", + } + + # For compatibility with Nautobot lower than v2.2.0 + cls.csv_data = ( + "name,regex,replace,platform", + f"test configreplace 4,^(.*)$,xyz,{platform.pk}", + f"test configreplace 5,^(.*)$,xyz,{platform.pk}", + f"test configreplace 6,^(.*)$,xyz,{platform.pk}", + ) + + +class GoldenConfigListViewTestCase(TestCase): + """Test GoldenConfigListView.""" + + user_permissions = ["nautobot_golden_config.view_goldenconfig", "nautobot_golden_config.change_goldenconfig"] + + @classmethod + def setUpTestData(cls): + """Set up base objects.""" + create_device_data() + cls.gc_settings = models.GoldenConfigSetting.objects.first() + cls.gc_dynamic_group = cls.gc_settings.dynamic_group + cls.gc_dynamic_group.filter = {"name": [dev.name for dev in Device.objects.all()]} + cls.gc_dynamic_group.validated_save() + models.GoldenConfig.objects.create(device=Device.objects.first()) + + def _get_golden_config_table_header(self): + response = self.client.get(f"{self._url}") + html_parsed = html.fromstring(response.content.decode()) + golden_config_table = html_parsed.find_class("table")[0] + return golden_config_table.find("thead") + + @property + def _text_table_headers(self): + if nautobot.__version__ >= "2.3.0": + return ["Device", "Backup Status", "Intended Status", "Compliance Status", "Dynamic Groups", "Actions"] + return ["Device", "Backup Status", "Intended Status", "Compliance Status", "Actions"] + + @property + def _url(self): + return reverse("plugins:nautobot_golden_config:goldenconfig_list") + + def test_page_ok(self): + response = self.client.get(f"{self._url}") + self.assertEqual(response.status_code, 200) + + def test_headers_in_table(self): + table_header = self._get_golden_config_table_header() + headers = table_header.iterdescendants("th") + checkbox_header = next(headers) + checkbox_element = checkbox_header.find("input") + self.assertEqual(checkbox_element.type, "checkbox") + text_headers = [header.text_content() for header in headers] + self.assertEqual(text_headers, self._text_table_headers) + + def test_device_relationship_not_included_in_golden_config_table(self): + # Create a RelationshipAssociation to Device Model to setup test case + device_content_type = ContentType.objects.get_for_model(Device) + platform_content_type = ContentType.objects.get(app_label="dcim", model="platform") + device = Device.objects.first() + relationship = Relationship.objects.create( + label="test platform to dev", + type="one-to-many", + source_type_id=platform_content_type.id, + destination_type_id=device_content_type.id, + ) + RelationshipAssociation.objects.create( + source_type_id=platform_content_type.id, + source_id=device.platform.id, + destination_type_id=device_content_type.id, + destination_id=device.id, + relationship_id=relationship.id, + ) + table_header = self._get_golden_config_table_header() + # xpath expression excludes the pk checkbox column (i.e. the first column) + text_headers = [header.text_content() for header in table_header.xpath("tr/th[position()>1]")] + # This will fail if the Relationships to Device objects showed up in the Golden Config table + self.assertEqual(text_headers, self._text_table_headers) + + @skip("TODO: 2.0 Figure out how do csv tests.") + def test_csv_export(self): + # verify GoldenConfig table is empty + self.assertEqual(models.GoldenConfig.objects.count(), 0) + intended_datetime = datetime.datetime.now() + first_device = self.gc_dynamic_group.members.first() + models.GoldenConfig.objects.create( + device=first_device, + intended_last_attempt_date=intended_datetime, + intended_last_success_date=intended_datetime, + ) + response = self.client.get(f"{self._url}?format=csv") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.headers["Content-Type"], "text/csv") + csv_data = response.content.decode().splitlines() + csv_headers = "Device Name,backup attempt,backup successful,intended attempt,intended successful,compliance attempt,compliance successful" + self.assertEqual(csv_headers, csv_data[0]) + intended_datetime_formated = intended_datetime.strftime("%Y-%m-%dT%H:%M:%S.%f+00:00") + # Test single entry in GoldenConfig table has data + expected_first_row = f"{first_device.name},,,{intended_datetime_formated},{intended_datetime_formated},," + self.assertEqual(expected_first_row, csv_data[1]) + # Test Devices in scope but without entries in GoldenConfig have empty entries + empty_csv_rows = [ + f"{device.name},,,,,," for device in self.gc_dynamic_group.members.exclude(pk=first_device.pk) + ] + self.assertEqual(empty_csv_rows, csv_data[2:]) + + @skip("TODO: 2.0 Figure out how do csv tests.") + def test_csv_export_with_filter(self): + devices_in_site_1 = Device.objects.filter(site__name="Site 1") + golden_config_devices = self.gc_dynamic_group.members.all() + # Test that there are Devices in GC that are not related to Site 1 + self.assertNotEqual(devices_in_site_1, golden_config_devices) + response = self.client.get(f"{self._url}?site={Device.objects.first().site.slug}&format=csv") + self.assertEqual(response.status_code, 200) + csv_data = response.content.decode().splitlines() + device_names_in_export = [entry.split(",")[0] for entry in csv_data[1:]] + device_names_in_site_1 = [device.name for device in devices_in_site_1] + self.assertEqual(device_names_in_export, device_names_in_site_1) + + +# pylint: disable=too-many-ancestors,too-many-locals +class ConfigPlanTestCase( + ViewTestCases.GetObjectViewTestCase, + ViewTestCases.GetObjectChangelogViewTestCase, + ViewTestCases.ListObjectsViewTestCase, + # Disabling Create tests because ConfigPlans are created via Job + # ViewTestCases.CreateObjectViewTestCase, + ViewTestCases.DeleteObjectViewTestCase, + ViewTestCases.EditObjectViewTestCase, +): + """Test ConfigPlan views.""" + + model = models.ConfigPlan + + @classmethod + def setUpTestData(cls): + create_device_data() + device1 = Device.objects.get(name="Device 1") + device2 = Device.objects.get(name="Device 2") + device3 = Device.objects.get(name="Device 3") + + rule1 = create_feature_rule_json(device1, feature="Test Feature 1") + rule2 = create_feature_rule_json(device2, feature="Test Feature 2") + rule3 = create_feature_rule_json(device3, feature="Test Feature 3") + rule4 = create_feature_rule_json(device3, feature="Test Feature 4") + + job_result1 = create_job_result() + job_result2 = create_job_result() + job_result3 = create_job_result() + + not_approved_status = Status.objects.get(name="Not Approved") + approved_status = Status.objects.get(name="Approved") + + plan1 = models.ConfigPlan.objects.create( + device=device1, + plan_type="intended", + config_set="Test Config Set 1", + change_control_id="Test Change Control ID 1", + change_control_url="https://1.example.com/", + status=not_approved_status, + plan_result_id=job_result1.id, + ) + plan1.feature.add(rule1.feature) + plan1.validated_save() + plan2 = models.ConfigPlan.objects.create( + device=device2, + plan_type="missing", + config_set="Test Config Set 2", + change_control_id="Test Change Control ID 2", + change_control_url="https://2.example.com/", + status=not_approved_status, + plan_result_id=job_result2.id, + ) + plan2.feature.add(rule2.feature) + plan2.validated_save() + plan3 = models.ConfigPlan.objects.create( + device=device3, + plan_type="remediation", + config_set="Test Config Set 3", + change_control_id="Test Change Control ID 3", + change_control_url="https://3.example.com/", + status=not_approved_status, + plan_result_id=job_result3.id, + ) + plan3.feature.set([rule3.feature, rule4.feature]) + plan3.validated_save() + + # Used for EditObjectViewTestCase + cls.form_data = { + "change_control_id": "Test Change Control ID 4", + "change_control_url": "https://4.example.com/", + "status": approved_status.pk, + } + + @skip("TODO: 2.0 Figure out how to have pass.") + def test_list_objects_with_constrained_permission(self): + pass + + +@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"]) +class ConfigComplianceUIViewSetTestCase( + ViewTestCases.BulkDeleteObjectsViewTestCase, + # ViewTestCases.ListObjectsViewTestCase, # generic list view tests won't work for this view since the queryset is pivoted +): + """Test ConfigComplianceUIViewSet views.""" + + model = models.ConfigCompliance + + @classmethod + def setUpTestData(cls): + create_device_data() + dev01 = Device.objects.get(name="Device 1") + dev02 = Device.objects.get(name="Device 2") + dev03 = Device.objects.get(name="Device 3") + dev04 = Device.objects.get(name="Device 4") + + for iterator_i in range(4): + feature_dev01 = create_feature_rule_json(dev01, feature=f"TestFeature{iterator_i}") + feature_dev02 = create_feature_rule_json(dev02, feature=f"TestFeature{iterator_i}") + feature_dev03 = create_feature_rule_json(dev03, feature=f"TestFeature{iterator_i}") + + updates = [ + {"device": dev01, "feature": feature_dev01}, + {"device": dev02, "feature": feature_dev02}, + {"device": dev03, "feature": feature_dev03}, + {"device": dev04, "feature": feature_dev01}, + ] + for iterator_j, update in enumerate(updates): + compliance_int = iterator_j % 2 + models.ConfigCompliance.objects.create( + device=update["device"], + rule=update["feature"], + actual={"foo": {"bar-1": "baz"}}, + intended={"foo": {f"bar-{compliance_int}": "baz"}}, + compliance=bool(compliance_int), + compliance_int=compliance_int, + ) + + def test_alter_queryset(self): + """Test alter_queryset method returns the expected pivoted queryset.""" + + unused_features = ( + models.ComplianceFeature.objects.create(slug="unused-feature-1", name="Unused Feature 1"), + models.ComplianceFeature.objects.create(slug="unused-feature-2", name="Unused Feature 2"), + ) + request = RequestFactory(SERVER_NAME="nautobot.example.com").get( + reverse("plugins:nautobot_golden_config:configcompliance_list") + ) + request.user = self.user + queryset = views.ConfigComplianceUIViewSet(request=request).alter_queryset(request) + features = ( + models.ComplianceFeature.objects.filter(feature__rule__isnull=False) + .values_list("slug", flat=True) + .distinct() + ) + self.assertNotIn(unused_features[0].slug, features) + self.assertNotIn(unused_features[1].slug, features) + self.assertGreater(len(features), 0) + self.assertIsInstance(queryset, RestrictedQuerySet) + for device in queryset: + self.assertSequenceEqual(list(device.keys()), ["device", "device__name", *features]) + for feature in features: + self.assertIn(device[feature], [0, 1]) diff --git a/nautobot_golden_config/urls.py b/nautobot_golden_config/urls.py index 22eccbd5c..af7a316c8 100644 --- a/nautobot_golden_config/urls.py +++ b/nautobot_golden_config/urls.py @@ -5,16 +5,26 @@ from django.views.generic import RedirectView from nautobot.apps.urls import NautobotUIViewSetRouter - from nautobot_golden_config import views +app_name = "nautobot_golden_config" router = NautobotUIViewSetRouter() -router.register("compliancefeature", views.ComplianceFeatureUIViewSet) - +router.register("compliance-feature", views.ComplianceFeatureUIViewSet) +router.register("compliance-rule", views.ComplianceRuleUIViewSet) +router.register("golden-config-setting", views.GoldenConfigSettingUIViewSet) +router.register("config-remove", views.ConfigRemoveUIViewSet) +router.register("config-replace", views.ConfigReplaceUIViewSet) +router.register("remediation-setting", views.RemediationSettingUIViewSet) +router.register("config-plan", views.ConfigPlanUIViewSet) +router.register("config-compliance", views.ConfigComplianceUIViewSet) +router.register("golden-config", views.GoldenConfigUIViewSet) urlpatterns = [ + path("config-compliance/overview/", views.ConfigComplianceOverview.as_view(), name="configcompliance_overview"), + path("config-plan/bulk_deploy/", views.ConfigPlanBulkDeploy.as_view(), name="configplan_bulk-deploy"), + path("generate-intended-config/", views.GenerateIntendedConfigView.as_view(), name="generate_intended_config"), path("docs/", RedirectView.as_view(url=static("nautobot_golden_config/docs/index.html")), name="docs"), ] diff --git a/nautobot_golden_config/views.py b/nautobot_golden_config/views.py index 26a9cd52f..e094ad663 100644 --- a/nautobot_golden_config/views.py +++ b/nautobot_golden_config/views.py @@ -1,19 +1,606 @@ -"""Views for nautobot_golden_config.""" +"""Django views for Nautobot Golden Configuration.""" # pylint: disable=too-many-lines -from nautobot.apps.views import NautobotUIViewSet +import json +import logging +from datetime import datetime + +import yaml +from django.contrib import messages +from django.contrib.auth.mixins import PermissionRequiredMixin +from django.core.exceptions import ObjectDoesNotExist +from django.db.models import Count, ExpressionWrapper, F, FloatField, Max, Q +from django.shortcuts import redirect, render +from django.urls import reverse +from django.utils.html import format_html +from django.utils.timezone import make_aware +from django.views.generic import TemplateView, View +from django_pivot.pivot import pivot +from nautobot.apps import views +from nautobot.core.views import generic +from nautobot.core.views.mixins import PERMISSIONS_ACTION_MAP, ObjectPermissionRequiredMixin +from nautobot.dcim.models import Device +from nautobot.extras.models import Job, JobResult +from rest_framework.decorators import action +from rest_framework.response import Response from nautobot_golden_config import filters, forms, models, tables from nautobot_golden_config.api import serializers +from nautobot_golden_config.utilities import constant +from nautobot_golden_config.utilities.config_postprocessing import get_config_postprocessing +from nautobot_golden_config.utilities.graphql import graph_ql_query +from nautobot_golden_config.utilities.helper import add_message, get_device_to_settings_map +from nautobot_golden_config.utilities.mat_plot import get_global_aggr, plot_barchart_visual, plot_visual + +# TODO: Future #4512 +PERMISSIONS_ACTION_MAP.update( + { + "backup": "view", + "compliance": "view", + "intended": "view", + "sotagg": "view", + "postprocessing": "view", + "devicetab": "view", + } +) +LOGGER = logging.getLogger(__name__) + +# +# GoldenConfig +# + + +class GoldenConfigUIViewSet( # pylint: disable=abstract-method + views.ObjectDetailViewMixin, + views.ObjectDestroyViewMixin, + views.ObjectBulkDestroyViewMixin, + views.ObjectListViewMixin, # TODO: Changing the order of the mixins breaks things... why? +): + """Views for the GoldenConfig model.""" + + bulk_update_form_class = forms.GoldenConfigBulkEditForm + table_class = tables.GoldenConfigTable + filterset_class = filters.GoldenConfigFilterSet + filterset_form_class = forms.GoldenConfigFilterForm + queryset = models.GoldenConfig.objects.all() + serializer_class = serializers.GoldenConfigSerializer + action_buttons = ("export",) + + def __init__(self, *args, **kwargs): + """Used to set default variables on GoldenConfigUIViewSet.""" + super().__init__(*args, **kwargs) + self.device = None + self.output = "" + self.structured_format = None + self.title_name = None + self.is_modal = None + self.config_details = None + self.action_template_name = None + + def filter_queryset(self, queryset): + """Add a warning message when GoldenConfig Table is out of sync.""" + queryset = super().filter_queryset(queryset) + # Only adding a message when no filters are applied + if self.filter_params: + return queryset + + sync_job = Job.objects.get( + module_name="nautobot_golden_config.jobs", job_class_name="SyncGoldenConfigWithDynamicGroups" + ) + sync_job_url = f"{sync_job.name}" + out_of_sync_message = format_html( + "The expected devices and actual devices here are not in sync. " + f"Running the job {sync_job_url} will put it back in sync." + ) + + gc_dynamic_group_device_pks = models.GoldenConfig.get_dynamic_group_device_pks() + gc_device_pks = models.GoldenConfig.get_golden_config_device_ids() + if gc_dynamic_group_device_pks != gc_device_pks: + messages.warning(self.request, message=out_of_sync_message) + + return queryset + + def get_extra_context(self, request, instance=None, **kwargs): + """Get extra context data.""" + context = super().get_extra_context(request, instance) + context["compliance"] = constant.ENABLE_COMPLIANCE + context["backup"] = constant.ENABLE_BACKUP + context["intended"] = constant.ENABLE_INTENDED + jobs = [] + jobs.append(["BackupJob", constant.ENABLE_BACKUP]) + jobs.append(["IntendedJob", constant.ENABLE_INTENDED]) + jobs.append(["ComplianceJob", constant.ENABLE_COMPLIANCE]) + add_message(jobs, request) + return context + + def _pre_helper(self, pk, request): + self.device = Device.objects.get(pk=pk) + self.config_details = models.GoldenConfig.objects.filter(device=self.device).first() + self.action_template_name = "nautobot_golden_config/goldenconfig_details.html" + self.structured_format = "json" + self.is_modal = False + if request.GET.get("modal") == "true": + self.action_template_name = "nautobot_golden_config/goldenconfig_detailsmodal.html" + self.is_modal = True + + def _post_render(self, request): + context = { + "output": self.output, + "device": self.device, + "device_name": self.device.name, + "format": self.structured_format, + "title_name": self.title_name, + "is_modal": self.is_modal, + } + return render(request, self.action_template_name, context) + + @action(detail=True, methods=["get"]) + def backup(self, request, pk, *args, **kwargs): + """Additional action to handle backup_config.""" + self._pre_helper(pk, request) + self.output = self.config_details.backup_config + self.structured_format = "cli" + self.title_name = "Backup Configuration Details" + return self._post_render(request) + + @action(detail=True, methods=["get"]) + def intended(self, request, pk, *args, **kwargs): + """Additional action to handle intended_config.""" + self._pre_helper(pk, request) + self.output = self.config_details.intended_config + self.structured_format = "cli" + self.title_name = "Intended Configuration Details" + return self._post_render(request) + + @action(detail=True, methods=["get"]) + def postprocessing(self, request, pk, *args, **kwargs): + """Additional action to handle postprocessing.""" + self._pre_helper(pk, request) + self.output = get_config_postprocessing(self.config_details, request) + self.structured_format = "cli" + self.title_name = "Post Processing" + return self._post_render(request) + + @action(detail=True, methods=["get"]) + def sotagg(self, request, pk, *args, **kwargs): + """Additional action to handle sotagg.""" + self._pre_helper(pk, request) + self.structured_format = "json" + if request.GET.get("format") in ["json", "yaml"]: + self.structured_format = request.GET.get("format") + + settings = get_device_to_settings_map(queryset=Device.objects.filter(pk=self.device.pk)) + if self.device.id in settings: + sot_agg_query_setting = settings[self.device.id].sot_agg_query + if sot_agg_query_setting is not None: + _, self.output = graph_ql_query(request, self.device, sot_agg_query_setting.query) + else: + self.output = {"Error": "No saved `GraphQL Query` query was configured in the `Golden Config Setting`"} + else: + raise ObjectDoesNotExist(f"{self.device.name} does not map to a Golden Config Setting.") + + if self.structured_format == "yaml": + self.output = yaml.dump(json.loads(json.dumps(self.output)), default_flow_style=False) + else: + self.output = json.dumps(self.output, indent=4) + self.title_name = "Aggregate Data" + return self._post_render(request) + + @action(detail=True, methods=["get"]) + def compliance(self, request, pk, *args, **kwargs): + """Additional action to handle compliance.""" + self._pre_helper(pk, request) + + self.output = self.config_details.compliance_config + if self.config_details.backup_last_success_date: + backup_date = str(self.config_details.backup_last_success_date.strftime("%b %d %Y")) + else: + backup_date = make_aware(datetime.now()).strftime("%b %d %Y") + if self.config_details.intended_last_success_date: + intended_date = str(self.config_details.intended_last_success_date.strftime("%b %d %Y")) + else: + intended_date = make_aware(datetime.now()).strftime("%b %d %Y") + + diff_type = "File" + self.structured_format = "diff" + + if self.output == "": + # This is used if all config snippets are in compliance and no diff exist. + self.output = f"--- Backup {diff_type} - " + backup_date + f"\n+++ Intended {diff_type} - " + intended_date + else: + first_occurence = self.output.index("@@") + second_occurence = self.output.index("@@", first_occurence) + # This is logic to match diff2html's expected input. + self.output = ( + f"--- Backup {diff_type} - " + + backup_date + + f"\n+++ Intended {diff_type} - " + + intended_date + + "\n" + + self.output[first_occurence:second_occurence] + + "@@" + + self.output[second_occurence + 2 :] # noqa: E203 + ) + self.title_name = "Compliance Details" + return self._post_render(request) + + +# +# ConfigCompliance +# + + +class ConfigComplianceUIViewSet( # pylint: disable=abstract-method + views.ObjectDetailViewMixin, + views.ObjectDestroyViewMixin, + views.ObjectBulkDestroyViewMixin, + views.ObjectListViewMixin, +): + """Views for the ConfigCompliance model.""" + + filterset_class = filters.ConfigComplianceFilterSet + filterset_form_class = forms.ConfigComplianceFilterForm + queryset = models.ConfigCompliance.objects.all().order_by("device__name") + serializer_class = serializers.ConfigComplianceSerializer + table_class = tables.ConfigComplianceTable + table_delete_class = tables.ConfigComplianceDeleteTable + + custom_action_permission_map = None + action_buttons = ("export",) + + def __init__(self, *args, **kwargs): + """Used to set default variables on ConfigComplianceUIViewSet.""" + super().__init__(*args, **kwargs) + self.pk_list = None + self.report_context = None + self.store_table = None # Used to store the table for bulk delete. No longer required in Nautobot 2.3.11 + + def get_extra_context(self, request, instance=None, **kwargs): + """A ConfigCompliance helper function to warn if the Job is not enabled to run.""" + context = super().get_extra_context(request, instance) + if self.action == "overview": + context = {**context, **self.report_context} + # TODO Remove when dropping support for Nautobot < 2.3.11 + if self.action == "bulk_destroy": + context["table"] = self.store_table + + context["compliance"] = constant.ENABLE_COMPLIANCE + context["backup"] = constant.ENABLE_BACKUP + context["intended"] = constant.ENABLE_INTENDED + add_message([["ComplianceJob", constant.ENABLE_COMPLIANCE]], request) + return context + + def alter_queryset(self, request): + """Build actual runtime queryset as the build time queryset of table `pivoted`.""" + return pivot( + self.queryset, + ["device", "device__name"], + "rule__feature__slug", + "compliance_int", + aggregation=Max, + ) + + def perform_bulk_destroy(self, request, **kwargs): + """Overwrite perform_bulk_destroy to handle special use case in which the UI shows devices but want to delete ConfigCompliance objects.""" + model = self.queryset.model + # Are we deleting *all* objects in the queryset or just a selected subset? + if request.POST.get("_all"): + filter_params = self.get_filter_params(request) + if not filter_params: + compliance_objects = model.objects.only("pk").all().values_list("pk", flat=True) + elif self.filterset_class is None: + raise NotImplementedError("filterset_class must be defined to use _all") + else: + compliance_objects = self.filterset_class(filter_params, model.objects.only("pk")).qs + # When selecting *all* the resulting request args are ConfigCompliance object PKs + self.pk_list = [item[0] for item in self.queryset.filter(pk__in=compliance_objects).values_list("id")] + elif "_confirm" not in request.POST: + # When it is not being confirmed, the pk's are the device objects. + device_objects = request.POST.getlist("pk") + self.pk_list = [item[0] for item in self.queryset.filter(device__pk__in=device_objects).values_list("id")] + else: + self.pk_list = request.POST.getlist("pk") + + form_class = self.get_form_class(**kwargs) + data = {} + if "_confirm" in request.POST: + form = form_class(request.POST) + if form.is_valid(): + return self.form_valid(form) + return self.form_invalid(form) + + table = self.table_delete_class(self.queryset.filter(pk__in=self.pk_list), orderable=False) + + if not table.rows: + messages.warning( + request, + f"No {self.queryset.model._meta.verbose_name_plural} were selected for deletion.", + ) + return redirect(self.get_return_url(request)) + + # TODO Remove when dropping support for Nautobot < 2.3.11 + self.store_table = table + + if not request.POST.get("_all"): + data.update({"table": table, "total_objs_to_delete": len(table.rows)}) + else: + data.update({"table": None, "delete_all": True, "total_objs_to_delete": len(table.rows)}) + return Response(data) + + @action(detail=True, methods=["get"]) + def devicetab(self, request, pk, *args, **kwargs): + """Additional action to handle backup_config.""" + device = Device.objects.get(pk=pk) + context = {} + compliance_details = models.ConfigCompliance.objects.filter(device=device) + context["compliance_details"] = compliance_details + if request.GET.get("compliance") == "compliant": + context["compliance_details"] = compliance_details.filter(compliance=True) + elif request.GET.get("compliance") == "non-compliant": + context["compliance_details"] = compliance_details.filter(compliance=False) + + context["active_tab"] = request.GET.get("tab") + context["device"] = device + context["object"] = device + context["verbose_name"] = "Device" + return render(request, "nautobot_golden_config/configcompliance_devicetab.html", context) + + +class ConfigComplianceOverview(generic.ObjectListView): + """View for executive report on configuration compliance.""" + + action_buttons = ("export",) + filterset = filters.ConfigComplianceFilterSet + filterset_form = forms.ConfigComplianceFilterForm + table = tables.ConfigComplianceGlobalFeatureTable + template_name = "nautobot_golden_config/configcompliance_overview.html" + # kind = "Features" + + queryset = ( + models.ConfigCompliance.objects.values("rule__feature__slug") + .annotate( + count=Count("rule__feature__slug"), + compliant=Count("rule__feature__slug", filter=Q(compliance=True)), + non_compliant=Count("rule__feature__slug", filter=~Q(compliance=True)), + comp_percent=ExpressionWrapper(100 * F("compliant") / F("count"), output_field=FloatField()), + ) + .order_by("-comp_percent") + ) + extra_content = {} + + # Once https://github.com/nautobot/nautobot/issues/4529 is addressed, can turn this on. + # Permalink reference: https://github.com/nautobot/nautobot-app-golden-config/blob/017d5e1526fa9f642b9e02bfc7161f27d4948bef/nautobot_golden_config/views.py#L383 + # @action(detail=False, methods=["get"]) + # def overview(self, request, *args, **kwargs): + def setup(self, request, *args, **kwargs): + """Using request object to perform filtering based on query params.""" + super().setup(request, *args, **kwargs) + filter_params = self.get_filter_params(request) + main_qs = models.ConfigCompliance.objects + device_aggr, feature_aggr = get_global_aggr(main_qs, self.filterset, filter_params) + feature_qs = self.filterset(request.GET, self.queryset).qs + self.extra_content = { + "bar_chart": plot_barchart_visual(feature_qs), + "device_aggr": device_aggr, + "device_visual": plot_visual(device_aggr), + "feature_aggr": feature_aggr, + "feature_visual": plot_visual(feature_aggr), + "compliance": constant.ENABLE_COMPLIANCE, + } + def extra_context(self): + """Extra content method on.""" + # add global aggregations to extra context. + return self.extra_content -class ComplianceFeatureUIViewSet(NautobotUIViewSet): - """ViewSet for ComplianceFeature views.""" + +class ComplianceFeatureUIViewSet(views.NautobotUIViewSet): + """Views for the ComplianceFeature model.""" bulk_update_form_class = forms.ComplianceFeatureBulkEditForm filterset_class = filters.ComplianceFeatureFilterSet filterset_form_class = forms.ComplianceFeatureFilterForm form_class = forms.ComplianceFeatureForm - lookup_field = "pk" queryset = models.ComplianceFeature.objects.all() serializer_class = serializers.ComplianceFeatureSerializer table_class = tables.ComplianceFeatureTable + lookup_field = "pk" + + def get_extra_context(self, request, instance=None): + """A ComplianceFeature helper function to warn if the Job is not enabled to run.""" + add_message([["ComplianceJob", constant.ENABLE_COMPLIANCE]], request) + return {} + + +class ComplianceRuleUIViewSet(views.NautobotUIViewSet): + """Views for the ComplianceRule model.""" + + bulk_update_form_class = forms.ComplianceRuleBulkEditForm + filterset_class = filters.ComplianceRuleFilterSet + filterset_form_class = forms.ComplianceRuleFilterForm + form_class = forms.ComplianceRuleForm + queryset = models.ComplianceRule.objects.all() + serializer_class = serializers.ComplianceRuleSerializer + table_class = tables.ComplianceRuleTable + lookup_field = "pk" + + def get_extra_context(self, request, instance=None): + """A ComplianceRule helper function to warn if the Job is not enabled to run.""" + add_message([["ComplianceJob", constant.ENABLE_COMPLIANCE]], request) + return {} + + +class GoldenConfigSettingUIViewSet(views.NautobotUIViewSet): + """Views for the GoldenConfigSetting model.""" + + bulk_update_form_class = forms.GoldenConfigSettingBulkEditForm + filterset_class = filters.GoldenConfigSettingFilterSet + filterset_form_class = forms.GoldenConfigSettingFilterForm + form_class = forms.GoldenConfigSettingForm + queryset = models.GoldenConfigSetting.objects.all() + serializer_class = serializers.GoldenConfigSettingSerializer + table_class = tables.GoldenConfigSettingTable + lookup_field = "pk" + + def get_extra_context(self, request, instance=None): + """A GoldenConfig helper function to warn if the Job is not enabled to run.""" + jobs = [] + jobs.append(["BackupJob", constant.ENABLE_BACKUP]) + jobs.append(["IntendedJob", constant.ENABLE_INTENDED]) + jobs.append(["DeployConfigPlans", constant.ENABLE_DEPLOY]) + jobs.append(["ComplianceJob", constant.ENABLE_COMPLIANCE]) + jobs.append( + [ + "AllGoldenConfig", + [ + constant.ENABLE_BACKUP, + constant.ENABLE_COMPLIANCE, + constant.ENABLE_DEPLOY, + constant.ENABLE_INTENDED, + constant.ENABLE_SOTAGG, + ], + ] + ) + jobs.append( + [ + "AllDevicesGoldenConfig", + [ + constant.ENABLE_BACKUP, + constant.ENABLE_COMPLIANCE, + constant.ENABLE_DEPLOY, + constant.ENABLE_INTENDED, + constant.ENABLE_SOTAGG, + ], + ] + ) + add_message(jobs, request) + return {} + + +class ConfigRemoveUIViewSet(views.NautobotUIViewSet): + """Views for the ConfigRemove model.""" + + bulk_update_form_class = forms.ConfigRemoveBulkEditForm + filterset_class = filters.ConfigRemoveFilterSet + filterset_form_class = forms.ConfigRemoveFilterForm + form_class = forms.ConfigRemoveForm + queryset = models.ConfigRemove.objects.all() + serializer_class = serializers.ConfigRemoveSerializer + table_class = tables.ConfigRemoveTable + lookup_field = "pk" + + def get_extra_context(self, request, instance=None): + """A ConfigRemove helper function to warn if the Job is not enabled to run.""" + add_message([["BackupJob", constant.ENABLE_BACKUP]], request) + return {} + + +class ConfigReplaceUIViewSet(views.NautobotUIViewSet): + """Views for the ConfigReplace model.""" + + bulk_update_form_class = forms.ConfigReplaceBulkEditForm + filterset_class = filters.ConfigReplaceFilterSet + filterset_form_class = forms.ConfigReplaceFilterForm + form_class = forms.ConfigReplaceForm + queryset = models.ConfigReplace.objects.all() + serializer_class = serializers.ConfigReplaceSerializer + table_class = tables.ConfigReplaceTable + lookup_field = "pk" + + def get_extra_context(self, request, instance=None): + """A ConfigReplace helper function to warn if the Job is not enabled to run.""" + add_message([["BackupJob", constant.ENABLE_BACKUP]], request) + return {} + + +class RemediationSettingUIViewSet(views.NautobotUIViewSet): + """Views for the RemediationSetting model.""" + + # bulk_create_form_class = forms.RemediationSettingCSVForm + bulk_update_form_class = forms.RemediationSettingBulkEditForm + filterset_class = filters.RemediationSettingFilterSet + filterset_form_class = forms.RemediationSettingFilterForm + form_class = forms.RemediationSettingForm + queryset = models.RemediationSetting.objects.all() + serializer_class = serializers.RemediationSettingSerializer + table_class = tables.RemediationSettingTable + lookup_field = "pk" + + def get_extra_context(self, request, instance=None): + """A RemediationSetting helper function to warn if the Job is not enabled to run.""" + add_message([["ComplianceJob", constant.ENABLE_COMPLIANCE]], request) + return {} + + +class ConfigPlanUIViewSet(views.NautobotUIViewSet): + """Views for the ConfigPlan model.""" + + bulk_update_form_class = forms.ConfigPlanBulkEditForm + filterset_class = filters.ConfigPlanFilterSet + filterset_form_class = forms.ConfigPlanFilterForm + form_class = forms.ConfigPlanForm + queryset = models.ConfigPlan.objects.all() + serializer_class = serializers.ConfigPlanSerializer + table_class = tables.ConfigPlanTable + lookup_field = "pk" + action_buttons = ("add",) + update_form_class = forms.ConfigPlanUpdateForm + + def alter_queryset(self, request): + """Build actual runtime queryset to automatically remove `Completed` by default.""" + if "Completed" not in request.GET.getlist("status"): + return self.queryset.exclude(status__name="Completed") + return self.queryset + + def get_extra_context(self, request, instance=None): + """A ConfigPlan helper function to warn if the Job is not enabled to run.""" + jobs = [] + jobs.append(["GenerateConfigPlans", constant.ENABLE_PLAN]) + jobs.append(["DeployConfigPlans", constant.ENABLE_DEPLOY]) + jobs.append(["DeployConfigPlanJobButtonReceiver", constant.ENABLE_DEPLOY]) + add_message(jobs, request) + return {} + + +class ConfigPlanBulkDeploy(ObjectPermissionRequiredMixin, View): + """View to run the Config Plan Deploy Job.""" + + queryset = models.ConfigPlan.objects.all() + + def get_required_permission(self): + """Permissions required for the view.""" + return "extras.run_job" + + # Once https://github.com/nautobot/nautobot/issues/4529 is addressed, can turn this on. + # Permalink reference: https://github.com/nautobot/nautobot-app-golden-config/blob/017d5e1526fa9f642b9e02bfc7161f27d4948bef/nautobot_golden_config/views.py#L609-L612 + # @action(detail=False, methods=["post"]) + # def bulk_deploy(self, request): + def post(self, request): + """Enqueue the job and redirect to the job results page.""" + config_plan_pks = request.POST.getlist("pk") + if not config_plan_pks: + messages.warning(request, "No Config Plans selected for deployment.") + return redirect("plugins:nautobot_golden_config:configplan_list") + + job_data = {"config_plan": config_plan_pks} + job = Job.objects.get(name="Generate Config Plans") + + job_result = JobResult.enqueue_job( + job, + request.user, + data=job_data, + **job.job_class.serialize_data(request), + ) + return redirect(job_result.get_absolute_url()) + + +class GenerateIntendedConfigView(PermissionRequiredMixin, TemplateView): + """View to generate the intended configuration.""" + + template_name = "nautobot_golden_config/generate_intended_config.html" + permission_required = ["dcim.view_device", "extras.view_gitrepository"] + + def get_context_data(self, **kwargs): + """Get the context data for the view.""" + context = super().get_context_data(**kwargs) + context["form"] = forms.GenerateIntendedConfigForm() + return context diff --git a/poetry.lock b/poetry.lock index d7321b2f2..f3bc1a82c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "amqp" @@ -25,6 +25,28 @@ files = [ {file = "aniso8601-7.0.0.tar.gz", hash = "sha256:513d2b6637b7853806ae79ffaca6f3e8754bdd547048f5ccc1420aec4b714f1e"}, ] +[[package]] +name = "anyio" +version = "4.5.2" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.8" +files = [ + {file = "anyio-4.5.2-py3-none-any.whl", hash = "sha256:c011ee36bc1e8ba40e5a81cb9df91925c218fe9b778554e0b56a21e1b5d4716f"}, + {file = "anyio-4.5.2.tar.gz", hash = "sha256:23009af4ed04ce05991845451e11ef02fc7c5ed29179ac9a420e5ad0ac7ddc5b"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} + +[package.extras] +doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"] +trio = ["trio (>=0.26.1)"] + [[package]] name = "appnope" version = "0.1.4" @@ -206,6 +228,44 @@ tzdata = {version = "*", optional = true, markers = "extra == \"tzdata\""} [package.extras] tzdata = ["tzdata"] +[[package]] +name = "bcrypt" +version = "4.2.1" +description = "Modern password hashing for your software and your servers" +optional = false +python-versions = ">=3.7" +files = [ + {file = "bcrypt-4.2.1-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:1340411a0894b7d3ef562fb233e4b6ed58add185228650942bdc885362f32c17"}, + {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1ee315739bc8387aa36ff127afc99120ee452924e0df517a8f3e4c0187a0f5f"}, + {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dbd0747208912b1e4ce730c6725cb56c07ac734b3629b60d4398f082ea718ad"}, + {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:aaa2e285be097050dba798d537b6efd9b698aa88eef52ec98d23dcd6d7cf6fea"}, + {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:76d3e352b32f4eeb34703370e370997065d28a561e4a18afe4fef07249cb4396"}, + {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:b7703ede632dc945ed1172d6f24e9f30f27b1b1a067f32f68bf169c5f08d0425"}, + {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:89df2aea2c43be1e1fa066df5f86c8ce822ab70a30e4c210968669565c0f4685"}, + {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:04e56e3fe8308a88b77e0afd20bec516f74aecf391cdd6e374f15cbed32783d6"}, + {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:cfdf3d7530c790432046c40cda41dfee8c83e29482e6a604f8930b9930e94139"}, + {file = "bcrypt-4.2.1-cp37-abi3-win32.whl", hash = "sha256:adadd36274510a01f33e6dc08f5824b97c9580583bd4487c564fc4617b328005"}, + {file = "bcrypt-4.2.1-cp37-abi3-win_amd64.whl", hash = "sha256:8c458cd103e6c5d1d85cf600e546a639f234964d0228909d8f8dbeebff82d526"}, + {file = "bcrypt-4.2.1-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:8ad2f4528cbf0febe80e5a3a57d7a74e6635e41af1ea5675282a33d769fba413"}, + {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:909faa1027900f2252a9ca5dfebd25fc0ef1417943824783d1c8418dd7d6df4a"}, + {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cde78d385d5e93ece5479a0a87f73cd6fa26b171c786a884f955e165032b262c"}, + {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:533e7f3bcf2f07caee7ad98124fab7499cb3333ba2274f7a36cf1daee7409d99"}, + {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:687cf30e6681eeda39548a93ce9bfbb300e48b4d445a43db4298d2474d2a1e54"}, + {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:041fa0155c9004eb98a232d54da05c0b41d4b8e66b6fc3cb71b4b3f6144ba837"}, + {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f85b1ffa09240c89aa2e1ae9f3b1c687104f7b2b9d2098da4e923f1b7082d331"}, + {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c6f5fa3775966cca251848d4d5393ab016b3afed251163c1436fefdec3b02c84"}, + {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:807261df60a8b1ccd13e6599c779014a362ae4e795f5c59747f60208daddd96d"}, + {file = "bcrypt-4.2.1-cp39-abi3-win32.whl", hash = "sha256:b588af02b89d9fad33e5f98f7838bf590d6d692df7153647724a7f20c186f6bf"}, + {file = "bcrypt-4.2.1-cp39-abi3-win_amd64.whl", hash = "sha256:e84e0e6f8e40a242b11bce56c313edc2be121cec3e0ec2d76fce01f6af33c07c"}, + {file = "bcrypt-4.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:76132c176a6d9953cdc83c296aeaed65e1a708485fd55abf163e0d9f8f16ce0e"}, + {file = "bcrypt-4.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e158009a54c4c8bc91d5e0da80920d048f918c61a581f0a63e4e93bb556d362f"}, + {file = "bcrypt-4.2.1.tar.gz", hash = "sha256:6765386e3ab87f569b276988742039baab087b2cdb01e809d74e74503c2faafe"}, +] + +[package.extras] +tests = ["pytest (>=3.2.1,!=3.3.0)"] +typecheck = ["mypy"] + [[package]] name = "billiard" version = "4.2.1" @@ -555,6 +615,80 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "contourpy" +version = "1.1.1" +description = "Python library for calculating contours of 2D quadrilateral grids" +optional = false +python-versions = ">=3.8" +files = [ + {file = "contourpy-1.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:46e24f5412c948d81736509377e255f6040e94216bf1a9b5ea1eaa9d29f6ec1b"}, + {file = "contourpy-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e48694d6a9c5a26ee85b10130c77a011a4fedf50a7279fa0bdaf44bafb4299d"}, + {file = "contourpy-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a66045af6cf00e19d02191ab578a50cb93b2028c3eefed999793698e9ea768ae"}, + {file = "contourpy-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4ebf42695f75ee1a952f98ce9775c873e4971732a87334b099dde90b6af6a916"}, + {file = "contourpy-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6aec19457617ef468ff091669cca01fa7ea557b12b59a7908b9474bb9674cf0"}, + {file = "contourpy-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:462c59914dc6d81e0b11f37e560b8a7c2dbab6aca4f38be31519d442d6cde1a1"}, + {file = "contourpy-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6d0a8efc258659edc5299f9ef32d8d81de8b53b45d67bf4bfa3067f31366764d"}, + {file = "contourpy-1.1.1-cp310-cp310-win32.whl", hash = "sha256:d6ab42f223e58b7dac1bb0af32194a7b9311065583cc75ff59dcf301afd8a431"}, + {file = "contourpy-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:549174b0713d49871c6dee90a4b499d3f12f5e5f69641cd23c50a4542e2ca1eb"}, + {file = "contourpy-1.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:407d864db716a067cc696d61fa1ef6637fedf03606e8417fe2aeed20a061e6b2"}, + {file = "contourpy-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe80c017973e6a4c367e037cb31601044dd55e6bfacd57370674867d15a899b"}, + {file = "contourpy-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e30aaf2b8a2bac57eb7e1650df1b3a4130e8d0c66fc2f861039d507a11760e1b"}, + {file = "contourpy-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3de23ca4f381c3770dee6d10ead6fff524d540c0f662e763ad1530bde5112532"}, + {file = "contourpy-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:566f0e41df06dfef2431defcfaa155f0acfa1ca4acbf8fd80895b1e7e2ada40e"}, + {file = "contourpy-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b04c2f0adaf255bf756cf08ebef1be132d3c7a06fe6f9877d55640c5e60c72c5"}, + {file = "contourpy-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d0c188ae66b772d9d61d43c6030500344c13e3f73a00d1dc241da896f379bb62"}, + {file = "contourpy-1.1.1-cp311-cp311-win32.whl", hash = "sha256:0683e1ae20dc038075d92e0e0148f09ffcefab120e57f6b4c9c0f477ec171f33"}, + {file = "contourpy-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:8636cd2fc5da0fb102a2504fa2c4bea3cbc149533b345d72cdf0e7a924decc45"}, + {file = "contourpy-1.1.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:560f1d68a33e89c62da5da4077ba98137a5e4d3a271b29f2f195d0fba2adcb6a"}, + {file = "contourpy-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:24216552104ae8f3b34120ef84825400b16eb6133af2e27a190fdc13529f023e"}, + {file = "contourpy-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56de98a2fb23025882a18b60c7f0ea2d2d70bbbcfcf878f9067234b1c4818442"}, + {file = "contourpy-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:07d6f11dfaf80a84c97f1a5ba50d129d9303c5b4206f776e94037332e298dda8"}, + {file = "contourpy-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1eaac5257a8f8a047248d60e8f9315c6cff58f7803971170d952555ef6344a7"}, + {file = "contourpy-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19557fa407e70f20bfaba7d55b4d97b14f9480856c4fb65812e8a05fe1c6f9bf"}, + {file = "contourpy-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:081f3c0880712e40effc5f4c3b08feca6d064cb8cfbb372ca548105b86fd6c3d"}, + {file = "contourpy-1.1.1-cp312-cp312-win32.whl", hash = "sha256:059c3d2a94b930f4dafe8105bcdc1b21de99b30b51b5bce74c753686de858cb6"}, + {file = "contourpy-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f44d78b61740e4e8c71db1cf1fd56d9050a4747681c59ec1094750a658ceb970"}, + {file = "contourpy-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:70e5a10f8093d228bb2b552beeb318b8928b8a94763ef03b858ef3612b29395d"}, + {file = "contourpy-1.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8394e652925a18ef0091115e3cc191fef350ab6dc3cc417f06da66bf98071ae9"}, + {file = "contourpy-1.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5bd5680f844c3ff0008523a71949a3ff5e4953eb7701b28760805bc9bcff217"}, + {file = "contourpy-1.1.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66544f853bfa85c0d07a68f6c648b2ec81dafd30f272565c37ab47a33b220684"}, + {file = "contourpy-1.1.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0c02b75acfea5cab07585d25069207e478d12309557f90a61b5a3b4f77f46ce"}, + {file = "contourpy-1.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41339b24471c58dc1499e56783fedc1afa4bb018bcd035cfb0ee2ad2a7501ef8"}, + {file = "contourpy-1.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f29fb0b3f1217dfe9362ec55440d0743fe868497359f2cf93293f4b2701b8251"}, + {file = "contourpy-1.1.1-cp38-cp38-win32.whl", hash = "sha256:f9dc7f933975367251c1b34da882c4f0e0b2e24bb35dc906d2f598a40b72bfc7"}, + {file = "contourpy-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:498e53573e8b94b1caeb9e62d7c2d053c263ebb6aa259c81050766beb50ff8d9"}, + {file = "contourpy-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ba42e3810999a0ddd0439e6e5dbf6d034055cdc72b7c5c839f37a7c274cb4eba"}, + {file = "contourpy-1.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c06e4c6e234fcc65435223c7b2a90f286b7f1b2733058bdf1345d218cc59e34"}, + {file = "contourpy-1.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca6fab080484e419528e98624fb5c4282148b847e3602dc8dbe0cb0669469887"}, + {file = "contourpy-1.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93df44ab351119d14cd1e6b52a5063d3336f0754b72736cc63db59307dabb718"}, + {file = "contourpy-1.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eafbef886566dc1047d7b3d4b14db0d5b7deb99638d8e1be4e23a7c7ac59ff0f"}, + {file = "contourpy-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efe0fab26d598e1ec07d72cf03eaeeba8e42b4ecf6b9ccb5a356fde60ff08b85"}, + {file = "contourpy-1.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f08e469821a5e4751c97fcd34bcb586bc243c39c2e39321822060ba902eac49e"}, + {file = "contourpy-1.1.1-cp39-cp39-win32.whl", hash = "sha256:bfc8a5e9238232a45ebc5cb3bfee71f1167064c8d382cadd6076f0d51cff1da0"}, + {file = "contourpy-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:c84fdf3da00c2827d634de4fcf17e3e067490c4aea82833625c4c8e6cdea0887"}, + {file = "contourpy-1.1.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:229a25f68046c5cf8067d6d6351c8b99e40da11b04d8416bf8d2b1d75922521e"}, + {file = "contourpy-1.1.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a10dab5ea1bd4401c9483450b5b0ba5416be799bbd50fc7a6cc5e2a15e03e8a3"}, + {file = "contourpy-1.1.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4f9147051cb8fdb29a51dc2482d792b3b23e50f8f57e3720ca2e3d438b7adf23"}, + {file = "contourpy-1.1.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a75cc163a5f4531a256f2c523bd80db509a49fc23721b36dd1ef2f60ff41c3cb"}, + {file = "contourpy-1.1.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b53d5769aa1f2d4ea407c65f2d1d08002952fac1d9e9d307aa2e1023554a163"}, + {file = "contourpy-1.1.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:11b836b7dbfb74e049c302bbf74b4b8f6cb9d0b6ca1bf86cfa8ba144aedadd9c"}, + {file = "contourpy-1.1.1.tar.gz", hash = "sha256:96ba37c2e24b7212a77da85004c38e7c4d155d3e72a45eeaf22c1f03f607e8ab"}, +] + +[package.dependencies] +numpy = [ + {version = ">=1.16,<2.0", markers = "python_version <= \"3.11\""}, + {version = ">=1.26.0rc1,<2.0", markers = "python_version >= \"3.12\""}, +] + +[package.extras] +bokeh = ["bokeh", "selenium"] +docs = ["furo", "sphinx (>=7.2)", "sphinx-copybutton"] +mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.4.1)", "types-Pillow"] +test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] +test-no-images = ["pytest", "pytest-cov", "wurlitzer"] + [[package]] name = "coverage" version = "7.6.1" @@ -702,6 +836,21 @@ ssh = ["bcrypt (>=3.1.5)"] test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] test-randomorder = ["pytest-randomly"] +[[package]] +name = "cycler" +version = "0.12.1" +description = "Composable style cycles" +optional = false +python-versions = ">=3.8" +files = [ + {file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"}, + {file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"}, +] + +[package.extras] +docs = ["ipython", "matplotlib", "numpydoc", "sphinx"] +tests = ["pytest", "pytest-cov", "pytest-xdist"] + [[package]] name = "decorator" version = "5.1.1" @@ -713,6 +862,24 @@ files = [ {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, ] +[[package]] +name = "deepdiff" +version = "7.0.1" +description = "Deep Difference and Search of any Python object/data. Recreate objects by adding adding deltas to each other." +optional = false +python-versions = ">=3.8" +files = [ + {file = "deepdiff-7.0.1-py3-none-any.whl", hash = "sha256:447760081918216aa4fd4ca78a4b6a848b81307b2ea94c810255334b759e1dc3"}, + {file = "deepdiff-7.0.1.tar.gz", hash = "sha256:260c16f052d4badbf60351b4f77e8390bee03a0b516246f6839bc813fb429ddf"}, +] + +[package.dependencies] +ordered-set = ">=4.1.0,<4.2.0" + +[package.extras] +cli = ["click (==8.1.7)", "pyyaml (==6.0.1)"] +optimize = ["orjson"] + [[package]] name = "defusedxml" version = "0.7.1" @@ -853,17 +1020,17 @@ Django = "*" [[package]] name = "django-debug-toolbar" -version = "4.4.6" +version = "4.3.0" description = "A configurable set of panels that display various debug information about the current request/response." optional = false python-versions = ">=3.8" files = [ - {file = "django_debug_toolbar-4.4.6-py3-none-any.whl", hash = "sha256:3beb671c9ec44ffb817fad2780667f172bd1c067dbcabad6268ce39a81335f45"}, - {file = "django_debug_toolbar-4.4.6.tar.gz", hash = "sha256:36e421cb908c2f0675e07f9f41e3d1d8618dc386392ec82d23bcfcd5d29c7044"}, + {file = "django_debug_toolbar-4.3.0-py3-none-any.whl", hash = "sha256:e09b7dcb8417b743234dfc57c95a7c1d1d87a88844abd13b4c5387f807b31bf6"}, + {file = "django_debug_toolbar-4.3.0.tar.gz", hash = "sha256:0b0dddee5ea29b9cb678593bc0d7a6d76b21d7799cb68e091a2148341a80f3c4"}, ] [package.dependencies] -django = ">=4.2.9" +django = ">=3.2.4" sqlparse = ">=0.2" [[package]] @@ -958,6 +1125,20 @@ Django = ">=3.2" [package.extras] tests = ["tox"] +[[package]] +name = "django-pivot" +version = "1.9.0" +description = "Create pivot tables and histograms from ORM querysets" +optional = false +python-versions = "*" +files = [ + {file = "django-pivot-1.9.0.tar.gz", hash = "sha256:5e985d32d9ff2a6b89419dd0292c0fa2822d494ee479b5fd16cdb542abf66a88"}, + {file = "django_pivot-1.9.0-py3-none-any.whl", hash = "sha256:1c60e18e7d5f7e42856faee0961748082ddd05b01ae7c8a4baed64d2bbacd051"}, +] + +[package.dependencies] +django = ">=2.2.0" + [[package]] name = "django-prometheus" version = "2.3.1" @@ -1212,6 +1393,20 @@ typing-extensions = ">=4.7.0" [package.extras] dev = ["coverage", "pytest (>=7.4.4)"] +[[package]] +name = "exceptiongroup" +version = "1.2.2" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[package.extras] +test = ["pytest (>=6)"] + [[package]] name = "executing" version = "2.1.0" @@ -1226,6 +1421,90 @@ files = [ [package.extras] tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] +[[package]] +name = "fonttools" +version = "4.55.3" +description = "Tools to manipulate font files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fonttools-4.55.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1dcc07934a2165ccdc3a5a608db56fb3c24b609658a5b340aee4ecf3ba679dc0"}, + {file = "fonttools-4.55.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f7d66c15ba875432a2d2fb419523f5d3d347f91f48f57b8b08a2dfc3c39b8a3f"}, + {file = "fonttools-4.55.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e4ae3592e62eba83cd2c4ccd9462dcfa603ff78e09110680a5444c6925d841"}, + {file = "fonttools-4.55.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62d65a3022c35e404d19ca14f291c89cc5890032ff04f6c17af0bd1927299674"}, + {file = "fonttools-4.55.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d342e88764fb201286d185093781bf6628bbe380a913c24adf772d901baa8276"}, + {file = "fonttools-4.55.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dd68c87a2bfe37c5b33bcda0fba39b65a353876d3b9006fde3adae31f97b3ef5"}, + {file = "fonttools-4.55.3-cp310-cp310-win32.whl", hash = "sha256:1bc7ad24ff98846282eef1cbeac05d013c2154f977a79886bb943015d2b1b261"}, + {file = "fonttools-4.55.3-cp310-cp310-win_amd64.whl", hash = "sha256:b54baf65c52952db65df39fcd4820668d0ef4766c0ccdf32879b77f7c804d5c5"}, + {file = "fonttools-4.55.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8c4491699bad88efe95772543cd49870cf756b019ad56294f6498982408ab03e"}, + {file = "fonttools-4.55.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5323a22eabddf4b24f66d26894f1229261021dacd9d29e89f7872dd8c63f0b8b"}, + {file = "fonttools-4.55.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5480673f599ad410695ca2ddef2dfefe9df779a9a5cda89503881e503c9c7d90"}, + {file = "fonttools-4.55.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da9da6d65cd7aa6b0f806556f4985bcbf603bf0c5c590e61b43aa3e5a0f822d0"}, + {file = "fonttools-4.55.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e894b5bd60d9f473bed7a8f506515549cc194de08064d829464088d23097331b"}, + {file = "fonttools-4.55.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:aee3b57643827e237ff6ec6d28d9ff9766bd8b21e08cd13bff479e13d4b14765"}, + {file = "fonttools-4.55.3-cp311-cp311-win32.whl", hash = "sha256:eb6ca911c4c17eb51853143624d8dc87cdcdf12a711fc38bf5bd21521e79715f"}, + {file = "fonttools-4.55.3-cp311-cp311-win_amd64.whl", hash = "sha256:6314bf82c54c53c71805318fcf6786d986461622dd926d92a465199ff54b1b72"}, + {file = "fonttools-4.55.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f9e736f60f4911061235603a6119e72053073a12c6d7904011df2d8fad2c0e35"}, + {file = "fonttools-4.55.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a8aa2c5e5b8b3bcb2e4538d929f6589a5c6bdb84fd16e2ed92649fb5454f11c"}, + {file = "fonttools-4.55.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07f8288aacf0a38d174445fc78377a97fb0b83cfe352a90c9d9c1400571963c7"}, + {file = "fonttools-4.55.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8d5e8916c0970fbc0f6f1bece0063363bb5857a7f170121a4493e31c3db3314"}, + {file = "fonttools-4.55.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ae3b6600565b2d80b7c05acb8e24d2b26ac407b27a3f2e078229721ba5698427"}, + {file = "fonttools-4.55.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:54153c49913f45065c8d9e6d0c101396725c5621c8aee744719300f79771d75a"}, + {file = "fonttools-4.55.3-cp312-cp312-win32.whl", hash = "sha256:827e95fdbbd3e51f8b459af5ea10ecb4e30af50221ca103bea68218e9615de07"}, + {file = "fonttools-4.55.3-cp312-cp312-win_amd64.whl", hash = "sha256:e6e8766eeeb2de759e862004aa11a9ea3d6f6d5ec710551a88b476192b64fd54"}, + {file = "fonttools-4.55.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a430178ad3e650e695167cb53242dae3477b35c95bef6525b074d87493c4bf29"}, + {file = "fonttools-4.55.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:529cef2ce91dc44f8e407cc567fae6e49a1786f2fefefa73a294704c415322a4"}, + {file = "fonttools-4.55.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e75f12c82127486fac2d8bfbf5bf058202f54bf4f158d367e41647b972342ca"}, + {file = "fonttools-4.55.3-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:859c358ebf41db18fb72342d3080bce67c02b39e86b9fbcf1610cca14984841b"}, + {file = "fonttools-4.55.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:546565028e244a701f73df6d8dd6be489d01617863ec0c6a42fa25bf45d43048"}, + {file = "fonttools-4.55.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:aca318b77f23523309eec4475d1fbbb00a6b133eb766a8bdc401faba91261abe"}, + {file = "fonttools-4.55.3-cp313-cp313-win32.whl", hash = "sha256:8c5ec45428edaa7022f1c949a632a6f298edc7b481312fc7dc258921e9399628"}, + {file = "fonttools-4.55.3-cp313-cp313-win_amd64.whl", hash = "sha256:11e5de1ee0d95af4ae23c1a138b184b7f06e0b6abacabf1d0db41c90b03d834b"}, + {file = "fonttools-4.55.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:caf8230f3e10f8f5d7593eb6d252a37caf58c480b19a17e250a63dad63834cf3"}, + {file = "fonttools-4.55.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b586ab5b15b6097f2fb71cafa3c98edfd0dba1ad8027229e7b1e204a58b0e09d"}, + {file = "fonttools-4.55.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8c2794ded89399cc2169c4d0bf7941247b8d5932b2659e09834adfbb01589aa"}, + {file = "fonttools-4.55.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf4fe7c124aa3f4e4c1940880156e13f2f4d98170d35c749e6b4f119a872551e"}, + {file = "fonttools-4.55.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:86721fbc389ef5cc1e2f477019e5069e8e4421e8d9576e9c26f840dbb04678de"}, + {file = "fonttools-4.55.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:89bdc5d88bdeec1b15af790810e267e8332d92561dce4f0748c2b95c9bdf3926"}, + {file = "fonttools-4.55.3-cp38-cp38-win32.whl", hash = "sha256:bc5dbb4685e51235ef487e4bd501ddfc49be5aede5e40f4cefcccabc6e60fb4b"}, + {file = "fonttools-4.55.3-cp38-cp38-win_amd64.whl", hash = "sha256:cd70de1a52a8ee2d1877b6293af8a2484ac82514f10b1c67c1c5762d38073e56"}, + {file = "fonttools-4.55.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bdcc9f04b36c6c20978d3f060e5323a43f6222accc4e7fcbef3f428e216d96af"}, + {file = "fonttools-4.55.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c3ca99e0d460eff46e033cd3992a969658c3169ffcd533e0a39c63a38beb6831"}, + {file = "fonttools-4.55.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22f38464daa6cdb7b6aebd14ab06609328fe1e9705bb0fcc7d1e69de7109ee02"}, + {file = "fonttools-4.55.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed63959d00b61959b035c7d47f9313c2c1ece090ff63afea702fe86de00dbed4"}, + {file = "fonttools-4.55.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5e8d657cd7326eeaba27de2740e847c6b39dde2f8d7cd7cc56f6aad404ddf0bd"}, + {file = "fonttools-4.55.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:fb594b5a99943042c702c550d5494bdd7577f6ef19b0bc73877c948a63184a32"}, + {file = "fonttools-4.55.3-cp39-cp39-win32.whl", hash = "sha256:dc5294a3d5c84226e3dbba1b6f61d7ad813a8c0238fceea4e09aa04848c3d851"}, + {file = "fonttools-4.55.3-cp39-cp39-win_amd64.whl", hash = "sha256:aedbeb1db64496d098e6be92b2e63b5fac4e53b1b92032dfc6988e1ea9134a4d"}, + {file = "fonttools-4.55.3-py3-none-any.whl", hash = "sha256:f412604ccbeee81b091b420272841e5ec5ef68967a9790e80bffd0e30b8e2977"}, + {file = "fonttools-4.55.3.tar.gz", hash = "sha256:3983313c2a04d6cc1fe9251f8fc647754cf49a61dac6cb1e7249ae67afaafc45"}, +] + +[package.extras] +all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "pycairo", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.1.0)", "xattr", "zopfli (>=0.1.4)"] +graphite = ["lz4 (>=1.7.4.2)"] +interpolatable = ["munkres", "pycairo", "scipy"] +lxml = ["lxml (>=4.0)"] +pathops = ["skia-pathops (>=0.5.0)"] +plot = ["matplotlib"] +repacker = ["uharfbuzz (>=0.23.0)"] +symfont = ["sympy"] +type1 = ["xattr"] +ufo = ["fs (>=2.2.0,<3)"] +unicode = ["unicodedata2 (>=15.1.0)"] +woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] + +[[package]] +name = "future" +version = "1.0.0" +description = "Clean single-source support for Python 3 and 2" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216"}, + {file = "future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05"}, +] + [[package]] name = "ghp-import" version = "2.1.0" @@ -1393,6 +1672,75 @@ files = [ astunparse = {version = ">=1.6", markers = "python_version < \"3.9\""} colorama = ">=0.4" +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "hier-config" +version = "2.2.3" +description = "A network configuration comparison tool, used to build remediation configurations." +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "hier_config-2.2.3-py3-none-any.whl", hash = "sha256:9adb860278afcf3813a49b75886649c9a21f7cc0c89f9d720f47ce8edcf021ca"}, + {file = "hier_config-2.2.3.tar.gz", hash = "sha256:6b0fb526c229b0f930f15a67be742d36230bf75a3041bf1d9d9487bbf9b01277"}, +] + +[package.dependencies] +PyYAML = ">=5.4" + +[[package]] +name = "httpcore" +version = "0.17.3" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.7" +files = [ + {file = "httpcore-0.17.3-py3-none-any.whl", hash = "sha256:c2789b767ddddfa2a5782e3199b2b7f6894540b17b16ec26b2c4d8e103510b87"}, + {file = "httpcore-0.17.3.tar.gz", hash = "sha256:a6f30213335e34c1ade7be6ec7c47f19f50c56db36abef1a9dfa3815b1cb3888"}, +] + +[package.dependencies] +anyio = ">=3.0,<5.0" +certifi = "*" +h11 = ">=0.13,<0.15" +sniffio = "==1.*" + +[package.extras] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] + +[[package]] +name = "httpx" +version = "0.24.1" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.7" +files = [ + {file = "httpx-0.24.1-py3-none-any.whl", hash = "sha256:06781eb9ac53cde990577af654bd990a4949de37a28bdb4a230d434f3a30b9bd"}, + {file = "httpx-0.24.1.tar.gz", hash = "sha256:5853a43053df830c20f8110c5e69fe44d035d850b2dfe795e196f00fdb774bdd"}, +] + +[package.dependencies] +certifi = "*" +httpcore = ">=0.15.0,<0.18.0" +idna = "*" +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] + [[package]] name = "idna" version = "3.10" @@ -1409,26 +1757,22 @@ all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2 [[package]] name = "importlib-metadata" -version = "8.5.0" +version = "4.13.0" description = "Read metadata from Python packages" optional = false -python-versions = ">=3.8" +python-versions = ">=3.7" files = [ - {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, - {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, + {file = "importlib_metadata-4.13.0-py3-none-any.whl", hash = "sha256:8a8a81bcf996e74fee46f0d16bd3eaa382a7eb20fd82445c3ad11f4090334116"}, + {file = "importlib_metadata-4.13.0.tar.gz", hash = "sha256:dd0173e8f150d6815e098fd354f6414b0f079af4644ddfe90c71e2fc6174346d"}, ] [package.dependencies] -zipp = ">=3.20" +zipp = ">=0.5" [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] perf = ["ipython"] -test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] -type = ["pytest-mypy"] +testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] [[package]] name = "importlib-resources" @@ -1619,6 +1963,152 @@ files = [ importlib-resources = {version = ">=1.4.0", markers = "python_version < \"3.9\""} referencing = ">=0.31.0" +[[package]] +name = "junos-eznc" +version = "2.7.1" +description = "Junos 'EZ' automation for non-programmers" +optional = false +python-versions = ">=3.8" +files = [ + {file = "junos-eznc-2.7.1.tar.gz", hash = "sha256:371f0298bf03e0cb4c017c43f6f4122263584eda0d690d0112e93f13daae41ac"}, + {file = "junos_eznc-2.7.1-py3-none-any.whl", hash = "sha256:8a7918faa8f0570341cac64c1210c1cd3e3542162d1e7449c3364f8d805716b2"}, +] + +[package.dependencies] +jinja2 = ">=2.7.1" +lxml = ">=3.2.4" +ncclient = ">=0.6.15" +pyparsing = "*" +pyserial = "*" +PyYAML = ">=5.1" +scp = ">=0.7.0" +six = "*" +transitions = "*" +yamlordereddictloader = "*" + +[[package]] +name = "kiwisolver" +version = "1.4.7" +description = "A fast implementation of the Cassowary constraint solver" +optional = false +python-versions = ">=3.8" +files = [ + {file = "kiwisolver-1.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8a9c83f75223d5e48b0bc9cb1bf2776cf01563e00ade8775ffe13b0b6e1af3a6"}, + {file = "kiwisolver-1.4.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:58370b1ffbd35407444d57057b57da5d6549d2d854fa30249771775c63b5fe17"}, + {file = "kiwisolver-1.4.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aa0abdf853e09aff551db11fce173e2177d00786c688203f52c87ad7fcd91ef9"}, + {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8d53103597a252fb3ab8b5845af04c7a26d5e7ea8122303dd7a021176a87e8b9"}, + {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:88f17c5ffa8e9462fb79f62746428dd57b46eb931698e42e990ad63103f35e6c"}, + {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a9ca9c710d598fd75ee5de59d5bda2684d9db36a9f50b6125eaea3969c2599"}, + {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f4d742cb7af1c28303a51b7a27aaee540e71bb8e24f68c736f6f2ffc82f2bf05"}, + {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e28c7fea2196bf4c2f8d46a0415c77a1c480cc0724722f23d7410ffe9842c407"}, + {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e968b84db54f9d42046cf154e02911e39c0435c9801681e3fc9ce8a3c4130278"}, + {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0c18ec74c0472de033e1bebb2911c3c310eef5649133dd0bedf2a169a1b269e5"}, + {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8f0ea6da6d393d8b2e187e6a5e3fb81f5862010a40c3945e2c6d12ae45cfb2ad"}, + {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:f106407dda69ae456dd1227966bf445b157ccc80ba0dff3802bb63f30b74e895"}, + {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:84ec80df401cfee1457063732d90022f93951944b5b58975d34ab56bb150dfb3"}, + {file = "kiwisolver-1.4.7-cp310-cp310-win32.whl", hash = "sha256:71bb308552200fb2c195e35ef05de12f0c878c07fc91c270eb3d6e41698c3bcc"}, + {file = "kiwisolver-1.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:44756f9fd339de0fb6ee4f8c1696cfd19b2422e0d70b4cefc1cc7f1f64045a8c"}, + {file = "kiwisolver-1.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:78a42513018c41c2ffd262eb676442315cbfe3c44eed82385c2ed043bc63210a"}, + {file = "kiwisolver-1.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d2b0e12a42fb4e72d509fc994713d099cbb15ebf1103545e8a45f14da2dfca54"}, + {file = "kiwisolver-1.4.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2a8781ac3edc42ea4b90bc23e7d37b665d89423818e26eb6df90698aa2287c95"}, + {file = "kiwisolver-1.4.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46707a10836894b559e04b0fd143e343945c97fd170d69a2d26d640b4e297935"}, + {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef97b8df011141c9b0f6caf23b29379f87dd13183c978a30a3c546d2c47314cb"}, + {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab58c12a2cd0fc769089e6d38466c46d7f76aced0a1f54c77652446733d2d02"}, + {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:803b8e1459341c1bb56d1c5c010406d5edec8a0713a0945851290a7930679b51"}, + {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9a9e8a507420fe35992ee9ecb302dab68550dedc0da9e2880dd88071c5fb052"}, + {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18077b53dc3bb490e330669a99920c5e6a496889ae8c63b58fbc57c3d7f33a18"}, + {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6af936f79086a89b3680a280c47ea90b4df7047b5bdf3aa5c524bbedddb9e545"}, + {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3abc5b19d24af4b77d1598a585b8a719beb8569a71568b66f4ebe1fb0449460b"}, + {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:933d4de052939d90afbe6e9d5273ae05fb836cc86c15b686edd4b3560cc0ee36"}, + {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:65e720d2ab2b53f1f72fb5da5fb477455905ce2c88aaa671ff0a447c2c80e8e3"}, + {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3bf1ed55088f214ba6427484c59553123fdd9b218a42bbc8c6496d6754b1e523"}, + {file = "kiwisolver-1.4.7-cp311-cp311-win32.whl", hash = "sha256:4c00336b9dd5ad96d0a558fd18a8b6f711b7449acce4c157e7343ba92dd0cf3d"}, + {file = "kiwisolver-1.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:929e294c1ac1e9f615c62a4e4313ca1823ba37326c164ec720a803287c4c499b"}, + {file = "kiwisolver-1.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:e33e8fbd440c917106b237ef1a2f1449dfbb9b6f6e1ce17c94cd6a1e0d438376"}, + {file = "kiwisolver-1.4.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:5360cc32706dab3931f738d3079652d20982511f7c0ac5711483e6eab08efff2"}, + {file = "kiwisolver-1.4.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942216596dc64ddb25adb215c3c783215b23626f8d84e8eff8d6d45c3f29f75a"}, + {file = "kiwisolver-1.4.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:48b571ecd8bae15702e4f22d3ff6a0f13e54d3d00cd25216d5e7f658242065ee"}, + {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad42ba922c67c5f219097b28fae965e10045ddf145d2928bfac2eb2e17673640"}, + {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:612a10bdae23404a72941a0fc8fa2660c6ea1217c4ce0dbcab8a8f6543ea9e7f"}, + {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e838bba3a3bac0fe06d849d29772eb1afb9745a59710762e4ba3f4cb8424483"}, + {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22f499f6157236c19f4bbbd472fa55b063db77a16cd74d49afe28992dff8c258"}, + {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693902d433cf585133699972b6d7c42a8b9f8f826ebcaf0132ff55200afc599e"}, + {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4e77f2126c3e0b0d055f44513ed349038ac180371ed9b52fe96a32aa071a5107"}, + {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:657a05857bda581c3656bfc3b20e353c232e9193eb167766ad2dc58b56504948"}, + {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4bfa75a048c056a411f9705856abfc872558e33c055d80af6a380e3658766038"}, + {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:34ea1de54beef1c104422d210c47c7d2a4999bdecf42c7b5718fbe59a4cac383"}, + {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:90da3b5f694b85231cf93586dad5e90e2d71b9428f9aad96952c99055582f520"}, + {file = "kiwisolver-1.4.7-cp312-cp312-win32.whl", hash = "sha256:18e0cca3e008e17fe9b164b55735a325140a5a35faad8de92dd80265cd5eb80b"}, + {file = "kiwisolver-1.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:58cb20602b18f86f83a5c87d3ee1c766a79c0d452f8def86d925e6c60fbf7bfb"}, + {file = "kiwisolver-1.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:f5a8b53bdc0b3961f8b6125e198617c40aeed638b387913bf1ce78afb1b0be2a"}, + {file = "kiwisolver-1.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2e6039dcbe79a8e0f044f1c39db1986a1b8071051efba3ee4d74f5b365f5226e"}, + {file = "kiwisolver-1.4.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a1ecf0ac1c518487d9d23b1cd7139a6a65bc460cd101ab01f1be82ecf09794b6"}, + {file = "kiwisolver-1.4.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ab9ccab2b5bd5702ab0803676a580fffa2aa178c2badc5557a84cc943fcf750"}, + {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f816dd2277f8d63d79f9c8473a79fe54047bc0467754962840782c575522224d"}, + {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf8bcc23ceb5a1b624572a1623b9f79d2c3b337c8c455405ef231933a10da379"}, + {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dea0bf229319828467d7fca8c7c189780aa9ff679c94539eed7532ebe33ed37c"}, + {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c06a4c7cf15ec739ce0e5971b26c93638730090add60e183530d70848ebdd34"}, + {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:913983ad2deb14e66d83c28b632fd35ba2b825031f2fa4ca29675e665dfecbe1"}, + {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5337ec7809bcd0f424c6b705ecf97941c46279cf5ed92311782c7c9c2026f07f"}, + {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4c26ed10c4f6fa6ddb329a5120ba3b6db349ca192ae211e882970bfc9d91420b"}, + {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c619b101e6de2222c1fcb0531e1b17bbffbe54294bfba43ea0d411d428618c27"}, + {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:073a36c8273647592ea332e816e75ef8da5c303236ec0167196793eb1e34657a"}, + {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3ce6b2b0231bda412463e152fc18335ba32faf4e8c23a754ad50ffa70e4091ee"}, + {file = "kiwisolver-1.4.7-cp313-cp313-win32.whl", hash = "sha256:f4c9aee212bc89d4e13f58be11a56cc8036cabad119259d12ace14b34476fd07"}, + {file = "kiwisolver-1.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:8a3ec5aa8e38fc4c8af308917ce12c536f1c88452ce554027e55b22cbbfbff76"}, + {file = "kiwisolver-1.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:76c8094ac20ec259471ac53e774623eb62e6e1f56cd8690c67ce6ce4fcb05650"}, + {file = "kiwisolver-1.4.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5d5abf8f8ec1f4e22882273c423e16cae834c36856cac348cfbfa68e01c40f3a"}, + {file = "kiwisolver-1.4.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:aeb3531b196ef6f11776c21674dba836aeea9d5bd1cf630f869e3d90b16cfade"}, + {file = "kiwisolver-1.4.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b7d755065e4e866a8086c9bdada157133ff466476a2ad7861828e17b6026e22c"}, + {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08471d4d86cbaec61f86b217dd938a83d85e03785f51121e791a6e6689a3be95"}, + {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7bbfcb7165ce3d54a3dfbe731e470f65739c4c1f85bb1018ee912bae139e263b"}, + {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d34eb8494bea691a1a450141ebb5385e4b69d38bb8403b5146ad279f4b30fa3"}, + {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9242795d174daa40105c1d86aba618e8eab7bf96ba8c3ee614da8302a9f95503"}, + {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a0f64a48bb81af7450e641e3fe0b0394d7381e342805479178b3d335d60ca7cf"}, + {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8e045731a5416357638d1700927529e2b8ab304811671f665b225f8bf8d8f933"}, + {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:4322872d5772cae7369f8351da1edf255a604ea7087fe295411397d0cfd9655e"}, + {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:e1631290ee9271dffe3062d2634c3ecac02c83890ada077d225e081aca8aab89"}, + {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:edcfc407e4eb17e037bca59be0e85a2031a2ac87e4fed26d3e9df88b4165f92d"}, + {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4d05d81ecb47d11e7f8932bd8b61b720bf0b41199358f3f5e36d38e28f0532c5"}, + {file = "kiwisolver-1.4.7-cp38-cp38-win32.whl", hash = "sha256:b38ac83d5f04b15e515fd86f312479d950d05ce2368d5413d46c088dda7de90a"}, + {file = "kiwisolver-1.4.7-cp38-cp38-win_amd64.whl", hash = "sha256:d83db7cde68459fc803052a55ace60bea2bae361fc3b7a6d5da07e11954e4b09"}, + {file = "kiwisolver-1.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f9362ecfca44c863569d3d3c033dbe8ba452ff8eed6f6b5806382741a1334bd"}, + {file = "kiwisolver-1.4.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e8df2eb9b2bac43ef8b082e06f750350fbbaf2887534a5be97f6cf07b19d9583"}, + {file = "kiwisolver-1.4.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f32d6edbc638cde7652bd690c3e728b25332acbadd7cad670cc4a02558d9c417"}, + {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e2e6c39bd7b9372b0be21456caab138e8e69cc0fc1190a9dfa92bd45a1e6e904"}, + {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dda56c24d869b1193fcc763f1284b9126550eaf84b88bbc7256e15028f19188a"}, + {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79849239c39b5e1fd906556c474d9b0439ea6792b637511f3fe3a41158d89ca8"}, + {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e3bc157fed2a4c02ec468de4ecd12a6e22818d4f09cde2c31ee3226ffbefab2"}, + {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3da53da805b71e41053dc670f9a820d1157aae77b6b944e08024d17bcd51ef88"}, + {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8705f17dfeb43139a692298cb6637ee2e59c0194538153e83e9ee0c75c2eddde"}, + {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:82a5c2f4b87c26bb1a0ef3d16b5c4753434633b83d365cc0ddf2770c93829e3c"}, + {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce8be0466f4c0d585cdb6c1e2ed07232221df101a4c6f28821d2aa754ca2d9e2"}, + {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:409afdfe1e2e90e6ee7fc896f3df9a7fec8e793e58bfa0d052c8a82f99c37abb"}, + {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5b9c3f4ee0b9a439d2415012bd1b1cc2df59e4d6a9939f4d669241d30b414327"}, + {file = "kiwisolver-1.4.7-cp39-cp39-win32.whl", hash = "sha256:a79ae34384df2b615eefca647a2873842ac3b596418032bef9a7283675962644"}, + {file = "kiwisolver-1.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:cf0438b42121a66a3a667de17e779330fc0f20b0d97d59d2f2121e182b0505e4"}, + {file = "kiwisolver-1.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:764202cc7e70f767dab49e8df52c7455e8de0df5d858fa801a11aa0d882ccf3f"}, + {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:94252291e3fe68001b1dd747b4c0b3be12582839b95ad4d1b641924d68fd4643"}, + {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b7dfa3b546da08a9f622bb6becdb14b3e24aaa30adba66749d38f3cc7ea9706"}, + {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd3de6481f4ed8b734da5df134cd5a6a64fe32124fe83dde1e5b5f29fe30b1e6"}, + {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a91b5f9f1205845d488c928e8570dcb62b893372f63b8b6e98b863ebd2368ff2"}, + {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40fa14dbd66b8b8f470d5fc79c089a66185619d31645f9b0773b88b19f7223c4"}, + {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:eb542fe7933aa09d8d8f9d9097ef37532a7df6497819d16efe4359890a2f417a"}, + {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bfa1acfa0c54932d5607e19a2c24646fb4c1ae2694437789129cf099789a3b00"}, + {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:eee3ea935c3d227d49b4eb85660ff631556841f6e567f0f7bda972df6c2c9935"}, + {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f3160309af4396e0ed04db259c3ccbfdc3621b5559b5453075e5de555e1f3a1b"}, + {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a17f6a29cf8935e587cc8a4dbfc8368c55edc645283db0ce9801016f83526c2d"}, + {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10849fb2c1ecbfae45a693c070e0320a91b35dd4bcf58172c023b994283a124d"}, + {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:ac542bf38a8a4be2dc6b15248d36315ccc65f0743f7b1a76688ffb6b5129a5c2"}, + {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8b01aac285f91ca889c800042c35ad3b239e704b150cfd3382adfc9dcc780e39"}, + {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:48be928f59a1f5c8207154f935334d374e79f2b5d212826307d072595ad76a2e"}, + {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f37cfe618a117e50d8c240555331160d73d0411422b59b5ee217843d7b693608"}, + {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:599b5c873c63a1f6ed7eead644a8a380cfbdf5db91dcb6f85707aaab213b1674"}, + {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:801fa7802e5cfabe3ab0c81a34c323a319b097dfb5004be950482d882f3d7225"}, + {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0c6c43471bc764fad4bc99c5c2d6d16a676b1abf844ca7c8702bdae92df01ee0"}, + {file = "kiwisolver-1.4.7.tar.gz", hash = "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60"}, +] + [[package]] name = "kombu" version = "5.4.2" @@ -1700,6 +2190,160 @@ files = [ {file = "lazy_object_proxy-1.10.0-pp310.pp311.pp312.pp38.pp39-none-any.whl", hash = "sha256:80fa48bd89c8f2f456fc0765c11c23bf5af827febacd2f523ca5bc1893fcc09d"}, ] +[[package]] +name = "lxml" +version = "5.3.0" +description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +optional = false +python-versions = ">=3.6" +files = [ + {file = "lxml-5.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:dd36439be765e2dde7660212b5275641edbc813e7b24668831a5c8ac91180656"}, + {file = "lxml-5.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ae5fe5c4b525aa82b8076c1a59d642c17b6e8739ecf852522c6321852178119d"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:501d0d7e26b4d261fca8132854d845e4988097611ba2531408ec91cf3fd9d20a"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb66442c2546446944437df74379e9cf9e9db353e61301d1a0e26482f43f0dd8"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e41506fec7a7f9405b14aa2d5c8abbb4dbbd09d88f9496958b6d00cb4d45330"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f7d4a670107d75dfe5ad080bed6c341d18c4442f9378c9f58e5851e86eb79965"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41ce1f1e2c7755abfc7e759dc34d7d05fd221723ff822947132dc934d122fe22"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:44264ecae91b30e5633013fb66f6ddd05c006d3e0e884f75ce0b4755b3e3847b"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:3c174dc350d3ec52deb77f2faf05c439331d6ed5e702fc247ccb4e6b62d884b7"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:2dfab5fa6a28a0b60a20638dc48e6343c02ea9933e3279ccb132f555a62323d8"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b1c8c20847b9f34e98080da785bb2336ea982e7f913eed5809e5a3c872900f32"}, + {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2c86bf781b12ba417f64f3422cfc302523ac9cd1d8ae8c0f92a1c66e56ef2e86"}, + {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c162b216070f280fa7da844531169be0baf9ccb17263cf5a8bf876fcd3117fa5"}, + {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:36aef61a1678cb778097b4a6eeae96a69875d51d1e8f4d4b491ab3cfb54b5a03"}, + {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f65e5120863c2b266dbcc927b306c5b78e502c71edf3295dfcb9501ec96e5fc7"}, + {file = "lxml-5.3.0-cp310-cp310-win32.whl", hash = "sha256:ef0c1fe22171dd7c7c27147f2e9c3e86f8bdf473fed75f16b0c2e84a5030ce80"}, + {file = "lxml-5.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:052d99051e77a4f3e8482c65014cf6372e61b0a6f4fe9edb98503bb5364cfee3"}, + {file = "lxml-5.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74bcb423462233bc5d6066e4e98b0264e7c1bed7541fff2f4e34fe6b21563c8b"}, + {file = "lxml-5.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a3d819eb6f9b8677f57f9664265d0a10dd6551d227afb4af2b9cd7bdc2ccbf18"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b8f5db71b28b8c404956ddf79575ea77aa8b1538e8b2ef9ec877945b3f46442"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c3406b63232fc7e9b8783ab0b765d7c59e7c59ff96759d8ef9632fca27c7ee4"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ecdd78ab768f844c7a1d4a03595038c166b609f6395e25af9b0f3f26ae1230f"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:168f2dfcfdedf611eb285efac1516c8454c8c99caf271dccda8943576b67552e"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa617107a410245b8660028a7483b68e7914304a6d4882b5ff3d2d3eb5948d8c"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:69959bd3167b993e6e710b99051265654133a98f20cec1d9b493b931942e9c16"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:bd96517ef76c8654446fc3db9242d019a1bb5fe8b751ba414765d59f99210b79"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ab6dd83b970dc97c2d10bc71aa925b84788c7c05de30241b9e96f9b6d9ea3080"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:eec1bb8cdbba2925bedc887bc0609a80e599c75b12d87ae42ac23fd199445654"}, + {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6a7095eeec6f89111d03dabfe5883a1fd54da319c94e0fb104ee8f23616b572d"}, + {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6f651ebd0b21ec65dfca93aa629610a0dbc13dbc13554f19b0113da2e61a4763"}, + {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f422a209d2455c56849442ae42f25dbaaba1c6c3f501d58761c619c7836642ec"}, + {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:62f7fdb0d1ed2065451f086519865b4c90aa19aed51081979ecd05a21eb4d1be"}, + {file = "lxml-5.3.0-cp311-cp311-win32.whl", hash = "sha256:c6379f35350b655fd817cd0d6cbeef7f265f3ae5fedb1caae2eb442bbeae9ab9"}, + {file = "lxml-5.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c52100e2c2dbb0649b90467935c4b0de5528833c76a35ea1a2691ec9f1ee7a1"}, + {file = "lxml-5.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e99f5507401436fdcc85036a2e7dc2e28d962550afe1cbfc07c40e454256a859"}, + {file = "lxml-5.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:384aacddf2e5813a36495233b64cb96b1949da72bef933918ba5c84e06af8f0e"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:874a216bf6afaf97c263b56371434e47e2c652d215788396f60477540298218f"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65ab5685d56914b9a2a34d67dd5488b83213d680b0c5d10b47f81da5a16b0b0e"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aac0bbd3e8dd2d9c45ceb82249e8bdd3ac99131a32b4d35c8af3cc9db1657179"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b369d3db3c22ed14c75ccd5af429086f166a19627e84a8fdade3f8f31426e52a"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24037349665434f375645fa9d1f5304800cec574d0310f618490c871fd902b3"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:62d172f358f33a26d6b41b28c170c63886742f5b6772a42b59b4f0fa10526cb1"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:c1f794c02903c2824fccce5b20c339a1a14b114e83b306ff11b597c5f71a1c8d"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:5d6a6972b93c426ace71e0be9a6f4b2cfae9b1baed2eed2006076a746692288c"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:3879cc6ce938ff4eb4900d901ed63555c778731a96365e53fadb36437a131a99"}, + {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:74068c601baff6ff021c70f0935b0c7bc528baa8ea210c202e03757c68c5a4ff"}, + {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ecd4ad8453ac17bc7ba3868371bffb46f628161ad0eefbd0a855d2c8c32dd81a"}, + {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7e2f58095acc211eb9d8b5771bf04df9ff37d6b87618d1cbf85f92399c98dae8"}, + {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e63601ad5cd8f860aa99d109889b5ac34de571c7ee902d6812d5d9ddcc77fa7d"}, + {file = "lxml-5.3.0-cp312-cp312-win32.whl", hash = "sha256:17e8d968d04a37c50ad9c456a286b525d78c4a1c15dd53aa46c1d8e06bf6fa30"}, + {file = "lxml-5.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:c1a69e58a6bb2de65902051d57fde951febad631a20a64572677a1052690482f"}, + {file = "lxml-5.3.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c72e9563347c7395910de6a3100a4840a75a6f60e05af5e58566868d5eb2d6a"}, + {file = "lxml-5.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e92ce66cd919d18d14b3856906a61d3f6b6a8500e0794142338da644260595cd"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d04f064bebdfef9240478f7a779e8c5dc32b8b7b0b2fc6a62e39b928d428e51"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c2fb570d7823c2bbaf8b419ba6e5662137f8166e364a8b2b91051a1fb40ab8b"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c120f43553ec759f8de1fee2f4794452b0946773299d44c36bfe18e83caf002"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:562e7494778a69086f0312ec9689f6b6ac1c6b65670ed7d0267e49f57ffa08c4"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:423b121f7e6fa514ba0c7918e56955a1d4470ed35faa03e3d9f0e3baa4c7e492"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c00f323cc00576df6165cc9d21a4c21285fa6b9989c5c39830c3903dc4303ef3"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:1fdc9fae8dd4c763e8a31e7630afef517eab9f5d5d31a278df087f307bf601f4"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:658f2aa69d31e09699705949b5fc4719cbecbd4a97f9656a232e7d6c7be1a367"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1473427aff3d66a3fa2199004c3e601e6c4500ab86696edffdbc84954c72d832"}, + {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a87de7dd873bf9a792bf1e58b1c3887b9264036629a5bf2d2e6579fe8e73edff"}, + {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0d7b36afa46c97875303a94e8f3ad932bf78bace9e18e603f2085b652422edcd"}, + {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cf120cce539453ae086eacc0130a324e7026113510efa83ab42ef3fcfccac7fb"}, + {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:df5c7333167b9674aa8ae1d4008fa4bc17a313cc490b2cca27838bbdcc6bb15b"}, + {file = "lxml-5.3.0-cp313-cp313-win32.whl", hash = "sha256:c802e1c2ed9f0c06a65bc4ed0189d000ada8049312cfeab6ca635e39c9608957"}, + {file = "lxml-5.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:406246b96d552e0503e17a1006fd27edac678b3fcc9f1be71a2f94b4ff61528d"}, + {file = "lxml-5.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8f0de2d390af441fe8b2c12626d103540b5d850d585b18fcada58d972b74a74e"}, + {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1afe0a8c353746e610bd9031a630a95bcfb1a720684c3f2b36c4710a0a96528f"}, + {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56b9861a71575f5795bde89256e7467ece3d339c9b43141dbdd54544566b3b94"}, + {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:9fb81d2824dff4f2e297a276297e9031f46d2682cafc484f49de182aa5e5df99"}, + {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2c226a06ecb8cdef28845ae976da407917542c5e6e75dcac7cc33eb04aaeb237"}, + {file = "lxml-5.3.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:7d3d1ca42870cdb6d0d29939630dbe48fa511c203724820fc0fd507b2fb46577"}, + {file = "lxml-5.3.0-cp36-cp36m-win32.whl", hash = "sha256:094cb601ba9f55296774c2d57ad68730daa0b13dc260e1f941b4d13678239e70"}, + {file = "lxml-5.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:eafa2c8658f4e560b098fe9fc54539f86528651f61849b22111a9b107d18910c"}, + {file = "lxml-5.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cb83f8a875b3d9b458cada4f880fa498646874ba4011dc974e071a0a84a1b033"}, + {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25f1b69d41656b05885aa185f5fdf822cb01a586d1b32739633679699f220391"}, + {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23e0553b8055600b3bf4a00b255ec5c92e1e4aebf8c2c09334f8368e8bd174d6"}, + {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ada35dd21dc6c039259596b358caab6b13f4db4d4a7f8665764d616daf9cc1d"}, + {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:81b4e48da4c69313192d8c8d4311e5d818b8be1afe68ee20f6385d0e96fc9512"}, + {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:2bc9fd5ca4729af796f9f59cd8ff160fe06a474da40aca03fcc79655ddee1a8b"}, + {file = "lxml-5.3.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:07da23d7ee08577760f0a71d67a861019103e4812c87e2fab26b039054594cc5"}, + {file = "lxml-5.3.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:ea2e2f6f801696ad7de8aec061044d6c8c0dd4037608c7cab38a9a4d316bfb11"}, + {file = "lxml-5.3.0-cp37-cp37m-win32.whl", hash = "sha256:5c54afdcbb0182d06836cc3d1be921e540be3ebdf8b8a51ee3ef987537455f84"}, + {file = "lxml-5.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:f2901429da1e645ce548bf9171784c0f74f0718c3f6150ce166be39e4dd66c3e"}, + {file = "lxml-5.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c56a1d43b2f9ee4786e4658c7903f05da35b923fb53c11025712562d5cc02753"}, + {file = "lxml-5.3.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ee8c39582d2652dcd516d1b879451500f8db3fe3607ce45d7c5957ab2596040"}, + {file = "lxml-5.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdf3a3059611f7585a78ee10399a15566356116a4288380921a4b598d807a22"}, + {file = "lxml-5.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:146173654d79eb1fc97498b4280c1d3e1e5d58c398fa530905c9ea50ea849b22"}, + {file = "lxml-5.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:0a7056921edbdd7560746f4221dca89bb7a3fe457d3d74267995253f46343f15"}, + {file = "lxml-5.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:9e4b47ac0f5e749cfc618efdf4726269441014ae1d5583e047b452a32e221920"}, + {file = "lxml-5.3.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f914c03e6a31deb632e2daa881fe198461f4d06e57ac3d0e05bbcab8eae01945"}, + {file = "lxml-5.3.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:213261f168c5e1d9b7535a67e68b1f59f92398dd17a56d934550837143f79c42"}, + {file = "lxml-5.3.0-cp38-cp38-win32.whl", hash = "sha256:218c1b2e17a710e363855594230f44060e2025b05c80d1f0661258142b2add2e"}, + {file = "lxml-5.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:315f9542011b2c4e1d280e4a20ddcca1761993dda3afc7a73b01235f8641e903"}, + {file = "lxml-5.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1ffc23010330c2ab67fac02781df60998ca8fe759e8efde6f8b756a20599c5de"}, + {file = "lxml-5.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2b3778cb38212f52fac9fe913017deea2fdf4eb1a4f8e4cfc6b009a13a6d3fcc"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b0c7a688944891086ba192e21c5229dea54382f4836a209ff8d0a660fac06be"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:747a3d3e98e24597981ca0be0fd922aebd471fa99d0043a3842d00cdcad7ad6a"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86a6b24b19eaebc448dc56b87c4865527855145d851f9fc3891673ff97950540"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b11a5d918a6216e521c715b02749240fb07ae5a1fefd4b7bf12f833bc8b4fe70"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68b87753c784d6acb8a25b05cb526c3406913c9d988d51f80adecc2b0775d6aa"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:109fa6fede314cc50eed29e6e56c540075e63d922455346f11e4d7a036d2b8cf"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:02ced472497b8362c8e902ade23e3300479f4f43e45f4105c85ef43b8db85229"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:6b038cc86b285e4f9fea2ba5ee76e89f21ed1ea898e287dc277a25884f3a7dfe"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:7437237c6a66b7ca341e868cda48be24b8701862757426852c9b3186de1da8a2"}, + {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7f41026c1d64043a36fda21d64c5026762d53a77043e73e94b71f0521939cc71"}, + {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:482c2f67761868f0108b1743098640fbb2a28a8e15bf3f47ada9fa59d9fe08c3"}, + {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:1483fd3358963cc5c1c9b122c80606a3a79ee0875bcac0204149fa09d6ff2727"}, + {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2dec2d1130a9cda5b904696cec33b2cfb451304ba9081eeda7f90f724097300a"}, + {file = "lxml-5.3.0-cp39-cp39-win32.whl", hash = "sha256:a0eabd0a81625049c5df745209dc7fcef6e2aea7793e5f003ba363610aa0a3ff"}, + {file = "lxml-5.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:89e043f1d9d341c52bf2af6d02e6adde62e0a46e6755d5eb60dc6e4f0b8aeca2"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7b1cd427cb0d5f7393c31b7496419da594fe600e6fdc4b105a54f82405e6626c"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51806cfe0279e06ed8500ce19479d757db42a30fd509940b1701be9c86a5ff9a"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee70d08fd60c9565ba8190f41a46a54096afa0eeb8f76bd66f2c25d3b1b83005"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8dc2c0395bea8254d8daebc76dcf8eb3a95ec2a46fa6fae5eaccee366bfe02ce"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6ba0d3dcac281aad8a0e5b14c7ed6f9fa89c8612b47939fc94f80b16e2e9bc83"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:6e91cf736959057f7aac7adfc83481e03615a8e8dd5758aa1d95ea69e8931dba"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:94d6c3782907b5e40e21cadf94b13b0842ac421192f26b84c45f13f3c9d5dc27"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c300306673aa0f3ed5ed9372b21867690a17dba38c68c44b287437c362ce486b"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d9b952e07aed35fe2e1a7ad26e929595412db48535921c5013edc8aa4a35ce"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:01220dca0d066d1349bd6a1726856a78f7929f3878f7e2ee83c296c69495309e"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2d9b8d9177afaef80c53c0a9e30fa252ff3036fb1c6494d427c066a4ce6a282f"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:20094fc3f21ea0a8669dc4c61ed7fa8263bd37d97d93b90f28fc613371e7a875"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ace2c2326a319a0bb8a8b0e5b570c764962e95818de9f259ce814ee666603f19"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92e67a0be1639c251d21e35fe74df6bcc40cba445c2cda7c4a967656733249e2"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd5350b55f9fecddc51385463a4f67a5da829bc741e38cf689f38ec9023f54ab"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c1fefd7e3d00921c44dc9ca80a775af49698bbfd92ea84498e56acffd4c5469"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:71a8dd38fbd2f2319136d4ae855a7078c69c9a38ae06e0c17c73fd70fc6caad8"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:97acf1e1fd66ab53dacd2c35b319d7e548380c2e9e8c54525c6e76d21b1ae3b1"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:68934b242c51eb02907c5b81d138cb977b2129a0a75a8f8b60b01cb8586c7b21"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b710bc2b8292966b23a6a0121f7a6c51d45d2347edcc75f016ac123b8054d3f2"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18feb4b93302091b1541221196a2155aa296c363fd233814fa11e181adebc52f"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3eb44520c4724c2e1a57c0af33a379eee41792595023f367ba3952a2d96c2aab"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:609251a0ca4770e5a8768ff902aa02bf636339c5a93f9349b48eb1f606f7f3e9"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:516f491c834eb320d6c843156440fe7fc0d50b33e44387fcec5b02f0bc118a4c"}, + {file = "lxml-5.3.0.tar.gz", hash = "sha256:4e109ca30d1edec1ac60cdbe341905dc3b8f55b16855e03a54aaf59e51ec8c6f"}, +] + +[package.extras] +cssselect = ["cssselect (>=0.7)"] +html-clean = ["lxml-html-clean"] +html5 = ["html5lib"] +htmlsoup = ["BeautifulSoup4"] +source = ["Cython (>=3.0.11)"] + [[package]] name = "markdown" version = "3.6" @@ -1801,6 +2445,74 @@ files = [ {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, ] +[[package]] +name = "matplotlib" +version = "3.7.5" +description = "Python plotting package" +optional = false +python-versions = ">=3.8" +files = [ + {file = "matplotlib-3.7.5-cp310-cp310-macosx_10_12_universal2.whl", hash = "sha256:4a87b69cb1cb20943010f63feb0b2901c17a3b435f75349fd9865713bfa63925"}, + {file = "matplotlib-3.7.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:d3ce45010fefb028359accebb852ca0c21bd77ec0f281952831d235228f15810"}, + {file = "matplotlib-3.7.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fbea1e762b28400393d71be1a02144aa16692a3c4c676ba0178ce83fc2928fdd"}, + {file = "matplotlib-3.7.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec0e1adc0ad70ba8227e957551e25a9d2995e319c29f94a97575bb90fa1d4469"}, + {file = "matplotlib-3.7.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6738c89a635ced486c8a20e20111d33f6398a9cbebce1ced59c211e12cd61455"}, + {file = "matplotlib-3.7.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1210b7919b4ed94b5573870f316bca26de3e3b07ffdb563e79327dc0e6bba515"}, + {file = "matplotlib-3.7.5-cp310-cp310-win32.whl", hash = "sha256:068ebcc59c072781d9dcdb82f0d3f1458271c2de7ca9c78f5bd672141091e9e1"}, + {file = "matplotlib-3.7.5-cp310-cp310-win_amd64.whl", hash = "sha256:f098ffbaab9df1e3ef04e5a5586a1e6b1791380698e84938d8640961c79b1fc0"}, + {file = "matplotlib-3.7.5-cp311-cp311-macosx_10_12_universal2.whl", hash = "sha256:f65342c147572673f02a4abec2d5a23ad9c3898167df9b47c149f32ce61ca078"}, + {file = "matplotlib-3.7.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4ddf7fc0e0dc553891a117aa083039088d8a07686d4c93fb8a810adca68810af"}, + {file = "matplotlib-3.7.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0ccb830fc29442360d91be48527809f23a5dcaee8da5f4d9b2d5b867c1b087b8"}, + {file = "matplotlib-3.7.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efc6bb28178e844d1f408dd4d6341ee8a2e906fc9e0fa3dae497da4e0cab775d"}, + {file = "matplotlib-3.7.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b15c4c2d374f249f324f46e883340d494c01768dd5287f8bc00b65b625ab56c"}, + {file = "matplotlib-3.7.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d028555421912307845e59e3de328260b26d055c5dac9b182cc9783854e98fb"}, + {file = "matplotlib-3.7.5-cp311-cp311-win32.whl", hash = "sha256:fe184b4625b4052fa88ef350b815559dd90cc6cc8e97b62f966e1ca84074aafa"}, + {file = "matplotlib-3.7.5-cp311-cp311-win_amd64.whl", hash = "sha256:084f1f0f2f1010868c6f1f50b4e1c6f2fb201c58475494f1e5b66fed66093647"}, + {file = "matplotlib-3.7.5-cp312-cp312-macosx_10_12_universal2.whl", hash = "sha256:34bceb9d8ddb142055ff27cd7135f539f2f01be2ce0bafbace4117abe58f8fe4"}, + {file = "matplotlib-3.7.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:c5a2134162273eb8cdfd320ae907bf84d171de948e62180fa372a3ca7cf0f433"}, + {file = "matplotlib-3.7.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:039ad54683a814002ff37bf7981aa1faa40b91f4ff84149beb53d1eb64617980"}, + {file = "matplotlib-3.7.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d742ccd1b09e863b4ca58291728db645b51dab343eebb08d5d4b31b308296ce"}, + {file = "matplotlib-3.7.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:743b1c488ca6a2bc7f56079d282e44d236bf375968bfd1b7ba701fd4d0fa32d6"}, + {file = "matplotlib-3.7.5-cp312-cp312-win_amd64.whl", hash = "sha256:fbf730fca3e1f23713bc1fae0a57db386e39dc81ea57dc305c67f628c1d7a342"}, + {file = "matplotlib-3.7.5-cp38-cp38-macosx_10_12_universal2.whl", hash = "sha256:cfff9b838531698ee40e40ea1a8a9dc2c01edb400b27d38de6ba44c1f9a8e3d2"}, + {file = "matplotlib-3.7.5-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:1dbcca4508bca7847fe2d64a05b237a3dcaec1f959aedb756d5b1c67b770c5ee"}, + {file = "matplotlib-3.7.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4cdf4ef46c2a1609a50411b66940b31778db1e4b73d4ecc2eaa40bd588979b13"}, + {file = "matplotlib-3.7.5-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:167200ccfefd1674b60e957186dfd9baf58b324562ad1a28e5d0a6b3bea77905"}, + {file = "matplotlib-3.7.5-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:53e64522934df6e1818b25fd48cf3b645b11740d78e6ef765fbb5fa5ce080d02"}, + {file = "matplotlib-3.7.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3e3bc79b2d7d615067bd010caff9243ead1fc95cf735c16e4b2583173f717eb"}, + {file = "matplotlib-3.7.5-cp38-cp38-win32.whl", hash = "sha256:6b641b48c6819726ed47c55835cdd330e53747d4efff574109fd79b2d8a13748"}, + {file = "matplotlib-3.7.5-cp38-cp38-win_amd64.whl", hash = "sha256:f0b60993ed3488b4532ec6b697059897891927cbfc2b8d458a891b60ec03d9d7"}, + {file = "matplotlib-3.7.5-cp39-cp39-macosx_10_12_universal2.whl", hash = "sha256:090964d0afaff9c90e4d8de7836757e72ecfb252fb02884016d809239f715651"}, + {file = "matplotlib-3.7.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:9fc6fcfbc55cd719bc0bfa60bde248eb68cf43876d4c22864603bdd23962ba25"}, + {file = "matplotlib-3.7.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e7cc3078b019bb863752b8b60e8b269423000f1603cb2299608231996bd9d54"}, + {file = "matplotlib-3.7.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e4e9a868e8163abaaa8259842d85f949a919e1ead17644fb77a60427c90473c"}, + {file = "matplotlib-3.7.5-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fa7ebc995a7d747dacf0a717d0eb3aa0f0c6a0e9ea88b0194d3a3cd241a1500f"}, + {file = "matplotlib-3.7.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3785bfd83b05fc0e0c2ae4c4a90034fe693ef96c679634756c50fe6efcc09856"}, + {file = "matplotlib-3.7.5-cp39-cp39-win32.whl", hash = "sha256:29b058738c104d0ca8806395f1c9089dfe4d4f0f78ea765c6c704469f3fffc81"}, + {file = "matplotlib-3.7.5-cp39-cp39-win_amd64.whl", hash = "sha256:fd4028d570fa4b31b7b165d4a685942ae9cdc669f33741e388c01857d9723eab"}, + {file = "matplotlib-3.7.5-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2a9a3f4d6a7f88a62a6a18c7e6a84aedcaf4faf0708b4ca46d87b19f1b526f88"}, + {file = "matplotlib-3.7.5-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9b3fd853d4a7f008a938df909b96db0b454225f935d3917520305b90680579c"}, + {file = "matplotlib-3.7.5-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0ad550da9f160737d7890217c5eeed4337d07e83ca1b2ca6535078f354e7675"}, + {file = "matplotlib-3.7.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:20da7924a08306a861b3f2d1da0d1aa9a6678e480cf8eacffe18b565af2813e7"}, + {file = "matplotlib-3.7.5-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b45c9798ea6bb920cb77eb7306409756a7fab9db9b463e462618e0559aecb30e"}, + {file = "matplotlib-3.7.5-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a99866267da1e561c7776fe12bf4442174b79aac1a47bd7e627c7e4d077ebd83"}, + {file = "matplotlib-3.7.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b6aa62adb6c268fc87d80f963aca39c64615c31830b02697743c95590ce3fbb"}, + {file = "matplotlib-3.7.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e530ab6a0afd082d2e9c17eb1eb064a63c5b09bb607b2b74fa41adbe3e162286"}, + {file = "matplotlib-3.7.5.tar.gz", hash = "sha256:1e5c971558ebc811aa07f54c7b7c677d78aa518ef4c390e14673a09e0860184a"}, +] + +[package.dependencies] +contourpy = ">=1.0.1" +cycler = ">=0.10" +fonttools = ">=4.22.0" +importlib-resources = {version = ">=3.2.0", markers = "python_version < \"3.10\""} +kiwisolver = ">=1.0.1" +numpy = ">=1.20,<2" +packaging = ">=20.0" +pillow = ">=6.2.0" +pyparsing = ">=2.3.1" +python-dateutil = ">=2.7" + [[package]] name = "matplotlib-inline" version = "0.1.7" @@ -1984,6 +2696,48 @@ files = [ griffe = ">=0.49" mkdocstrings = ">=0.25" +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "napalm" +version = "5.0.0" +description = "Network Automation and Programmability Abstraction Layer with Multivendor support" +optional = false +python-versions = "*" +files = [ + {file = "napalm-5.0.0-py2.py3-none-any.whl", hash = "sha256:458837932e527ca06a4bab7e600b0ca6e6bc3bb4b33fad9c9ef2befc7df6d2f5"}, + {file = "napalm-5.0.0.tar.gz", hash = "sha256:350ac3d74f2f10030dbae44d3395551d7e03ee25c65fa5eb8263a4e6f51f2c94"}, +] + +[package.dependencies] +cffi = ">=1.11.3" +jinja2 = "*" +junos-eznc = ">=2.7.0" +lxml = ">=4.3.0" +ncclient = "*" +netaddr = "*" +netmiko = ">=4.1.0" +netutils = ">=1.0.0" +paramiko = ">=2.6.0" +pyeapi = ">=1.0.2" +pyYAML = "*" +requests = ">=2.7.0" +scp = "*" +setuptools = ">=38.4.0" +textfsm = "*" +ttp = "*" +ttp-templates = "*" +typing-extensions = ">=4.3.0" + [[package]] name = "nautobot" version = "2.3.16" @@ -2013,8 +2767,8 @@ django-redis = ">=5.4.0,<5.5.0" django-silk = ">=5.1.0,<5.2.0" django-structlog = {version = ">=8.1.0,<9.0.0", extras = ["celery"]} django-tables2 = [ - {version = "2.7.0", markers = "python_version < \"3.9\""}, {version = ">=2.7.4,<2.8.0", markers = "python_version >= \"3.9\""}, + {version = "2.7.0", markers = "python_version < \"3.9\""}, ] django-taggit = ">=5.0.0,<5.1.0" django-timezone-field = ">=7.0,<7.1" @@ -2053,6 +2807,52 @@ napalm = ["napalm (>=4.1.0,<6.0.0)"] remote-storage = ["django-storages (==1.14.3)"] sso = ["social-auth-core[saml] (>=4.5.3,<4.6.0)"] +[[package]] +name = "nautobot-capacity-metrics" +version = "3.1.1" +description = "App to improve the instrumentation of Nautobot and expose additional metrics (Application Metrics, RQ Worker)." +optional = false +python-versions = "<3.13,>=3.8" +files = [ + {file = "nautobot_capacity_metrics-3.1.1-py3-none-any.whl", hash = "sha256:cba7108fc32473dd57e67e49e4c9de353837d0db63212e3dc9bed78ea6df57e6"}, + {file = "nautobot_capacity_metrics-3.1.1.tar.gz", hash = "sha256:3f54cbaca846fd89bd215829305e28877b596a4de081e785d22afd91f2ae90c2"}, +] + +[package.dependencies] +nautobot = ">=2.0.0,<3.0.0" + +[[package]] +name = "nautobot-plugin-nornir" +version = "2.1.0" +description = "Nautobot App that provides a shim layer to simplify using Nornir within other Nautobot Apps and Nautobot Jobs" +optional = false +python-versions = "<3.13,>=3.8" +files = [ + {file = "nautobot_plugin_nornir-2.1.0-py3-none-any.whl", hash = "sha256:aa50882b5fc729fb95e2d03383596a582f1b09419c8ec9c6db5f12cbb6f6ffa0"}, + {file = "nautobot_plugin_nornir-2.1.0.tar.gz", hash = "sha256:ea7ead4e52d27f349846d55bcdc00d6953f1bd03813e70a094f035a66bc863e7"}, +] + +[package.dependencies] +nautobot = ">=2.0.0,<3.0.0" +netutils = ">=1.6.0" +nornir-nautobot = ">=3.0.0,<4.0.0" + +[[package]] +name = "ncclient" +version = "0.6.16" +description = "Python library for NETCONF clients" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +files = [ + {file = "ncclient-0.6.16.tar.gz", hash = "sha256:a16a351d8c234e3bbf3495577b63c96ae4adfcdf67f2d84194313473ea65b805"}, +] + +[package.dependencies] +lxml = ">=3.3.0" +paramiko = ">=1.15.0" +setuptools = ">0.6" +six = "*" + [[package]] name = "netaddr" version = "1.3.0" @@ -2067,6 +2867,27 @@ files = [ [package.extras] nicer-shell = ["ipython"] +[[package]] +name = "netmiko" +version = "4.4.0" +description = "Multi-vendor library to simplify legacy CLI connections to network devices" +optional = false +python-versions = "<4.0,>=3.8" +files = [ + {file = "netmiko-4.4.0-py3-none-any.whl", hash = "sha256:2ff4683f013fac0f80715286c7d3250e89166aefc4421cb75d3ff483f2ebbbc0"}, + {file = "netmiko-4.4.0.tar.gz", hash = "sha256:25ff1237976aa3ff2cacf04949314638c899220a1675bd029e31b07ce20ce3b6"}, +] + +[package.dependencies] +cffi = ">=1.17.0rc1" +ntc-templates = ">=3.1.0" +paramiko = ">=2.9.5" +pyserial = ">=3.3" +pyyaml = ">=5.3" +scp = ">=0.13.6" +setuptools = ">=65.0.0" +textfsm = ">=1.1.3" + [[package]] name = "netutils" version = "1.11.0" @@ -2114,6 +2935,202 @@ files = [ {file = "nh3-0.2.20.tar.gz", hash = "sha256:9705c42d7ff88a0bea546c82d7fe5e59135e3d3f057e485394f491248a1f8ed5"}, ] +[[package]] +name = "nornir" +version = "3.4.1" +description = "Pluggable multi-threaded framework with inventory management to help operate collections of devices" +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "nornir-3.4.1-py3-none-any.whl", hash = "sha256:db079cb95e3baf855530f4f40cb6ee93f93e1bf3cb74ac08180546adb1b987b8"}, + {file = "nornir-3.4.1.tar.gz", hash = "sha256:82a90a3478a3890bef8ad51b256fa966e6e4ca326cbe20a230918ef907cf68c3"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=4,<5", markers = "python_version < \"3.10\""} +mypy_extensions = ">=1.0.0,<2.0.0" +"ruamel.yaml" = ">=0.17" + +[[package]] +name = "nornir-jinja2" +version = "0.2.0" +description = "Jinja2 plugins for nornir" +optional = false +python-versions = ">=3.6,<4.0" +files = [ + {file = "nornir_jinja2-0.2.0-py3-none-any.whl", hash = "sha256:0c446bec7a8492923d4eb9ca00fb327603b41bc35d5f0112843c048737b506b1"}, + {file = "nornir_jinja2-0.2.0.tar.gz", hash = "sha256:9ee5e725fe5543dcba4ec8b976804e9e88ecd356ea3b62bad97578cea0de1f75"}, +] + +[package.dependencies] +jinja2 = ">=2.11.2,<4" +nornir = ">=3,<4" + +[[package]] +name = "nornir-napalm" +version = "0.5.0" +description = "NAPALM's plugins for nornir" +optional = false +python-versions = "<4.0,>=3.8" +files = [ + {file = "nornir_napalm-0.5.0-py3-none-any.whl", hash = "sha256:1a418bf0f5e38ac65894d474f81b50787dafe0aa1965c4fbd1b86d34d4374418"}, + {file = "nornir_napalm-0.5.0.tar.gz", hash = "sha256:4c95979eebe2475e7b8516411ad8e3205d2ff30e410d1dbdce785a55033d1130"}, +] + +[package.dependencies] +napalm = ">=5,<6" +nornir = ">=3,<4" + +[[package]] +name = "nornir-nautobot" +version = "3.1.0" +description = "Nornir Nautobot" +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "nornir_nautobot-3.1.0-py3-none-any.whl", hash = "sha256:23197181c17fa6de503679490d04fdc7315133ec5ddc9b549eb0794af9da418f"}, + {file = "nornir_nautobot-3.1.0.tar.gz", hash = "sha256:5bc58d83650fb87aec456358205d455aaa5289345e2bc18f32d6bfa421eec63c"}, +] + +[package.dependencies] +httpx = ">=0.24.1,<0.25.0" +netutils = ">=1.6.0,<2.0.0" +nornir = ">=3.0.0,<4.0.0" +nornir-jinja2 = ">=0.2.0,<0.3.0" +nornir-napalm = ">=0.4.0,<1.0.0" +nornir-netmiko = ">=1,<2" +nornir-utils = ">=0,<1" +pynautobot = ">=2.0.0rc2" +requests = ">=2.25.1,<3.0.0" + +[package.extras] +mikrotik-driver = ["routeros-api (>=0.17.0,<0.18.0)"] + +[[package]] +name = "nornir-netmiko" +version = "1.0.1" +description = "Netmiko's plugins for Nornir" +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "nornir_netmiko-1.0.1-py3-none-any.whl", hash = "sha256:eaee2944ad386b40c0719e8ac393ac63d531f44fb9a07d660bae7de430f12834"}, + {file = "nornir_netmiko-1.0.1.tar.gz", hash = "sha256:498546df001e0e499f10c5646d1356e361ccbb165b1335b89cfe8f19765e24d7"}, +] + +[package.dependencies] +netmiko = ">=4.0.0,<5.0.0" + +[[package]] +name = "nornir-utils" +version = "0.2.0" +description = "Collection of plugins and functions for nornir that don't require external dependencies" +optional = false +python-versions = ">=3.6.2,<4.0.0" +files = [ + {file = "nornir_utils-0.2.0-py3-none-any.whl", hash = "sha256:b4c430793a74f03affd5ff2d90abc8c67a28c7ff325f48e3a01a9a44ec71b844"}, + {file = "nornir_utils-0.2.0.tar.gz", hash = "sha256:4de6aaa35e5c1a98e1c84db84a008b0b1e974dc65d88484f2dcea3e30c95fbc2"}, +] + +[package.dependencies] +colorama = ">=0.4.3,<0.5.0" +nornir = ">=3,<4" + +[[package]] +name = "ntc-templates" +version = "7.5.0" +description = "TextFSM Templates for Network Devices, and Python wrapper for TextFSM's CliTable." +optional = false +python-versions = "<4.0,>=3.8" +files = [ + {file = "ntc_templates-7.5.0-py3-none-any.whl", hash = "sha256:9d7fb6467ccaaedf8e93e12106e4c46b1610e88d1bcae396b8c2f6a786d9db1c"}, + {file = "ntc_templates-7.5.0.tar.gz", hash = "sha256:b4b1693cd79ef0da5be0c66d58e3c6285d8d264d46832545765c0d394afed0aa"}, +] + +[package.dependencies] +textfsm = ">=1.1.0,<2.0.0" + +[[package]] +name = "numpy" +version = "1.24.4" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "numpy-1.24.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0bfb52d2169d58c1cdb8cc1f16989101639b34c7d3ce60ed70b19c63eba0b64"}, + {file = "numpy-1.24.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed094d4f0c177b1b8e7aa9cba7d6ceed51c0e569a5318ac0ca9a090680a6a1b1"}, + {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79fc682a374c4a8ed08b331bef9c5f582585d1048fa6d80bc6c35bc384eee9b4"}, + {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ffe43c74893dbf38c2b0a1f5428760a1a9c98285553c89e12d70a96a7f3a4d6"}, + {file = "numpy-1.24.4-cp310-cp310-win32.whl", hash = "sha256:4c21decb6ea94057331e111a5bed9a79d335658c27ce2adb580fb4d54f2ad9bc"}, + {file = "numpy-1.24.4-cp310-cp310-win_amd64.whl", hash = "sha256:b4bea75e47d9586d31e892a7401f76e909712a0fd510f58f5337bea9572c571e"}, + {file = "numpy-1.24.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f136bab9c2cfd8da131132c2cf6cc27331dd6fae65f95f69dcd4ae3c3639c810"}, + {file = "numpy-1.24.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2926dac25b313635e4d6cf4dc4e51c8c0ebfed60b801c799ffc4c32bf3d1254"}, + {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:222e40d0e2548690405b0b3c7b21d1169117391c2e82c378467ef9ab4c8f0da7"}, + {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7215847ce88a85ce39baf9e89070cb860c98fdddacbaa6c0da3ffb31b3350bd5"}, + {file = "numpy-1.24.4-cp311-cp311-win32.whl", hash = "sha256:4979217d7de511a8d57f4b4b5b2b965f707768440c17cb70fbf254c4b225238d"}, + {file = "numpy-1.24.4-cp311-cp311-win_amd64.whl", hash = "sha256:b7b1fc9864d7d39e28f41d089bfd6353cb5f27ecd9905348c24187a768c79694"}, + {file = "numpy-1.24.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1452241c290f3e2a312c137a9999cdbf63f78864d63c79039bda65ee86943f61"}, + {file = "numpy-1.24.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:04640dab83f7c6c85abf9cd729c5b65f1ebd0ccf9de90b270cd61935eef0197f"}, + {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5425b114831d1e77e4b5d812b69d11d962e104095a5b9c3b641a218abcc050e"}, + {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd80e219fd4c71fc3699fc1dadac5dcf4fd882bfc6f7ec53d30fa197b8ee22dc"}, + {file = "numpy-1.24.4-cp38-cp38-win32.whl", hash = "sha256:4602244f345453db537be5314d3983dbf5834a9701b7723ec28923e2889e0bb2"}, + {file = "numpy-1.24.4-cp38-cp38-win_amd64.whl", hash = "sha256:692f2e0f55794943c5bfff12b3f56f99af76f902fc47487bdfe97856de51a706"}, + {file = "numpy-1.24.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2541312fbf09977f3b3ad449c4e5f4bb55d0dbf79226d7724211acc905049400"}, + {file = "numpy-1.24.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9667575fb6d13c95f1b36aca12c5ee3356bf001b714fc354eb5465ce1609e62f"}, + {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a86ed21e4f87050382c7bc96571755193c4c1392490744ac73d660e8f564a9"}, + {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d11efb4dbecbdf22508d55e48d9c8384db795e1b7b51ea735289ff96613ff74d"}, + {file = "numpy-1.24.4-cp39-cp39-win32.whl", hash = "sha256:6620c0acd41dbcb368610bb2f4d83145674040025e5536954782467100aa8835"}, + {file = "numpy-1.24.4-cp39-cp39-win_amd64.whl", hash = "sha256:befe2bf740fd8373cf56149a5c23a0f601e82869598d41f8e188a0e9869926f8"}, + {file = "numpy-1.24.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:31f13e25b4e304632a4619d0e0777662c2ffea99fcae2029556b17d8ff958aef"}, + {file = "numpy-1.24.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95f7ac6540e95bc440ad77f56e520da5bf877f87dca58bd095288dce8940532a"}, + {file = "numpy-1.24.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e98f220aa76ca2a977fe435f5b04d7b3470c0a2e6312907b37ba6068f26787f2"}, + {file = "numpy-1.24.4.tar.gz", hash = "sha256:80f5e3a4e498641401868df4208b74581206afbee7cf7b8329daae82676d9463"}, +] + +[[package]] +name = "numpy" +version = "1.26.4" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, + {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, + {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, + {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, + {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, + {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, + {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, + {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, + {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, + {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, + {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, +] + [[package]] name = "oauthlib" version = "3.2.2" @@ -2130,6 +3147,20 @@ rsa = ["cryptography (>=3.0.0)"] signals = ["blinker (>=1.4.0)"] signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] +[[package]] +name = "ordered-set" +version = "4.1.0" +description = "An OrderedSet is a custom MutableSet that remembers its order, so that every" +optional = false +python-versions = ">=3.7" +files = [ + {file = "ordered-set-4.1.0.tar.gz", hash = "sha256:694a8e44c87657c59292ede72891eb91d34131f6531463aab3009191c77364a8"}, + {file = "ordered_set-4.1.0-py3-none-any.whl", hash = "sha256:046e1132c71fcf3330438a539928932caf51ddbc582496833e23de611de14562"}, +] + +[package.extras] +dev = ["black", "mypy", "pytest"] + [[package]] name = "packaging" version = "24.2" @@ -2156,6 +3187,27 @@ files = [ dev = ["pytest", "tox"] lint = ["black"] +[[package]] +name = "paramiko" +version = "3.5.0" +description = "SSH2 protocol library" +optional = false +python-versions = ">=3.6" +files = [ + {file = "paramiko-3.5.0-py3-none-any.whl", hash = "sha256:1fedf06b085359051cd7d0d270cebe19e755a8a921cc2ddbfa647fb0cd7d68f9"}, + {file = "paramiko-3.5.0.tar.gz", hash = "sha256:ad11e540da4f55cedda52931f1a3f812a8238a7af7f62a60de538cd80bb28124"}, +] + +[package.dependencies] +bcrypt = ">=3.2" +cryptography = ">=3.3" +pynacl = ">=1.5" + +[package.extras] +all = ["gssapi (>=1.4.1)", "invoke (>=2.0)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8)"] +gssapi = ["gssapi (>=1.4.1)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8)"] +invoke = ["invoke (>=2.0)"] + [[package]] name = "parso" version = "0.8.4" @@ -2418,7 +3470,6 @@ files = [ {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909"}, {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1"}, {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567"}, - {file = "psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142"}, {file = "psycopg2_binary-2.9.10-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:eb09aa7f9cecb45027683bb55aebaaf45a0df8bf6de68801a6afdc7947bb09d4"}, {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b73d6d7f0ccdad7bc43e6d34273f70d587ef62f824d7261c4ae9b8b1b6af90e8"}, {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce5ab4bf46a211a8e924d307c1b1fcda82368586a19d0a24f8ae166f5c784864"}, @@ -2488,6 +3539,23 @@ files = [ {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] +[[package]] +name = "pyeapi" +version = "1.0.4" +description = "Python Client for eAPI" +optional = false +python-versions = "*" +files = [ + {file = "pyeapi-1.0.4.tar.gz", hash = "sha256:05920677246823cd3dddf7d4d0f831fbc86fd416f356706a03bc56a291d78f3d"}, +] + +[package.dependencies] +netaddr = "*" + +[package.extras] +dev = ["check-manifest", "pep8", "pyflakes", "twine"] +test = ["coverage"] + [[package]] name = "pygments" version = "2.19.1" @@ -2615,6 +3683,75 @@ pyyaml = "*" [package.extras] extra = ["pygments (>=2.19.1)"] +[[package]] +name = "pynacl" +version = "1.5.0" +description = "Python binding to the Networking and Cryptography (NaCl) library" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"}, + {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"}, + {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"}, + {file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"}, + {file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"}, + {file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"}, +] + +[package.dependencies] +cffi = ">=1.4.1" + +[package.extras] +docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] +tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] + +[[package]] +name = "pynautobot" +version = "2.0.1" +description = "Nautobot API client library" +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "pynautobot-2.0.1-py3-none-any.whl", hash = "sha256:14f9f05ef4c9f8918a56e4892c3badd3c25679aaf5cc6292adcebd7e1ba419c7"}, + {file = "pynautobot-2.0.1.tar.gz", hash = "sha256:de8bf725570baa5bee3a47e2a0de01605ab97e852e5f534b3d8e54a4ed6e2043"}, +] + +[package.dependencies] +requests = ">=2.30.0,<3.0.0" +urllib3 = ">=1.21.1,<1.27" + +[[package]] +name = "pyparsing" +version = "3.1.4" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +optional = false +python-versions = ">=3.6.8" +files = [ + {file = "pyparsing-3.1.4-py3-none-any.whl", hash = "sha256:a6a7ee4235a3f944aa1fa2249307708f893fe5717dc603503c6c7969c070fb7c"}, + {file = "pyparsing-3.1.4.tar.gz", hash = "sha256:f86ec8d1a83f11977c9a6ea7598e8c27fc5cddfa5b07ea2241edbbde1d7bc032"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "pyserial" +version = "3.5" +description = "Python Serial Port Extension" +optional = false +python-versions = "*" +files = [ + {file = "pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0"}, + {file = "pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb"}, +] + +[package.extras] +cp2110 = ["hidapi"] + [[package]] name = "python-crontab" version = "3.2.0" @@ -3134,6 +4271,83 @@ files = [ {file = "rpds_py-0.20.1.tar.gz", hash = "sha256:e1791c4aabd117653530dccd24108fa03cc6baf21f58b950d0a73c3b3b29a350"}, ] +[[package]] +name = "ruamel-yaml" +version = "0.18.10" +description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruamel.yaml-0.18.10-py3-none-any.whl", hash = "sha256:30f22513ab2301b3d2b577adc121c6471f28734d3d9728581245f1e76468b4f1"}, + {file = "ruamel.yaml-0.18.10.tar.gz", hash = "sha256:20c86ab29ac2153f80a428e1254a8adf686d3383df04490514ca3b79a362db58"}, +] + +[package.dependencies] +"ruamel.yaml.clib" = {version = ">=0.2.7", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.13\""} + +[package.extras] +docs = ["mercurial (>5.7)", "ryd"] +jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] + +[[package]] +name = "ruamel-yaml-clib" +version = "0.2.8" +description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" +optional = false +python-versions = ">=3.6" +files = [ + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b42169467c42b692c19cf539c38d4602069d8c1505e97b86387fcf7afb766e1d"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:07238db9cbdf8fc1e9de2489a4f68474e70dffcb32232db7c08fa61ca0c7c462"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fff3573c2db359f091e1589c3d7c5fc2f86f5bdb6f24252c2d8e539d4e45f412"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:aa2267c6a303eb483de8d02db2871afb5c5fc15618d894300b88958f729ad74f"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:840f0c7f194986a63d2c2465ca63af8ccbbc90ab1c6001b1978f05119b5e7334"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:024cfe1fc7c7f4e1aff4a81e718109e13409767e4f871443cbff3dba3578203d"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win32.whl", hash = "sha256:c69212f63169ec1cfc9bb44723bf2917cbbd8f6191a00ef3410f5a7fe300722d"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win_amd64.whl", hash = "sha256:cabddb8d8ead485e255fe80429f833172b4cadf99274db39abc080e068cbcc31"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bef08cd86169d9eafb3ccb0a39edb11d8e25f3dae2b28f5c52fd997521133069"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:b16420e621d26fdfa949a8b4b47ade8810c56002f5389970db4ddda51dbff248"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:25c515e350e5b739842fc3228d662413ef28f295791af5e5110b543cf0b57d9b"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_24_aarch64.whl", hash = "sha256:1707814f0d9791df063f8c19bb51b0d1278b8e9a2353abbb676c2f685dee6afe"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:46d378daaac94f454b3a0e3d8d78cafd78a026b1d71443f4966c696b48a6d899"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:09b055c05697b38ecacb7ac50bdab2240bfca1a0c4872b0fd309bb07dc9aa3a9"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win32.whl", hash = "sha256:53a300ed9cea38cf5a2a9b069058137c2ca1ce658a874b79baceb8f892f915a7"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win_amd64.whl", hash = "sha256:c2a72e9109ea74e511e29032f3b670835f8a59bbdc9ce692c5b4ed91ccf1eedb"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ebc06178e8821efc9692ea7544aa5644217358490145629914d8020042c24aa1"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:edaef1c1200c4b4cb914583150dcaa3bc30e592e907c01117c08b13a07255ec2"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d176b57452ab5b7028ac47e7b3cf644bcfdc8cacfecf7e71759f7f51a59e5c92"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_24_aarch64.whl", hash = "sha256:1dc67314e7e1086c9fdf2680b7b6c2be1c0d8e3a8279f2e993ca2a7545fecf62"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3213ece08ea033eb159ac52ae052a4899b56ecc124bb80020d9bbceeb50258e9"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aab7fd643f71d7946f2ee58cc88c9b7bfc97debd71dcc93e03e2d174628e7e2d"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-win32.whl", hash = "sha256:5c365d91c88390c8d0a8545df0b5857172824b1c604e867161e6b3d59a827eaa"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-win_amd64.whl", hash = "sha256:1758ce7d8e1a29d23de54a16ae867abd370f01b5a69e1a3ba75223eaa3ca1a1b"}, + {file = "ruamel.yaml.clib-0.2.8-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a5aa27bad2bb83670b71683aae140a1f52b0857a2deff56ad3f6c13a017a26ed"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c58ecd827313af6864893e7af0a3bb85fd529f862b6adbefe14643947cfe2942"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-macosx_12_0_arm64.whl", hash = "sha256:f481f16baec5290e45aebdc2a5168ebc6d35189ae6fea7a58787613a25f6e875"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:77159f5d5b5c14f7c34073862a6b7d34944075d9f93e681638f6d753606c6ce6"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7f67a1ee819dc4562d444bbafb135832b0b909f81cc90f7aa00260968c9ca1b3"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4ecbf9c3e19f9562c7fdd462e8d18dd902a47ca046a2e64dba80699f0b6c09b7"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:87ea5ff66d8064301a154b3933ae406b0863402a799b16e4a1d24d9fbbcbe0d3"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-win32.whl", hash = "sha256:75e1ed13e1f9de23c5607fe6bd1aeaae21e523b32d83bb33918245361e9cc51b"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-win_amd64.whl", hash = "sha256:3f215c5daf6a9d7bbed4a0a4f760f3113b10e82ff4c5c44bec20a68c8014f675"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1b617618914cb00bf5c34d4357c37aa15183fa229b24767259657746c9077615"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:a6a9ffd280b71ad062eae53ac1659ad86a17f59a0fdc7699fd9be40525153337"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:305889baa4043a09e5b76f8e2a51d4ffba44259f6b4c72dec8ca56207d9c6fe1"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:700e4ebb569e59e16a976857c8798aee258dceac7c7d6b50cab63e080058df91"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e2b4c44b60eadec492926a7270abb100ef9f72798e18743939bdbf037aab8c28"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e79e5db08739731b0ce4850bed599235d601701d5694c36570a99a0c5ca41a9d"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-win32.whl", hash = "sha256:955eae71ac26c1ab35924203fda6220f84dce57d6d7884f189743e2abe3a9fbe"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-win_amd64.whl", hash = "sha256:56f4252222c067b4ce51ae12cbac231bce32aee1d33fbfc9d17e5b8d6966c312"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:03d1162b6d1df1caa3a4bd27aa51ce17c9afc2046c31b0ad60a0a96ec22f8001"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:bba64af9fa9cebe325a62fa398760f5c7206b215201b0ec825005f1b18b9bccf"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:a1a45e0bb052edf6a1d3a93baef85319733a888363938e1fc9924cb00c8df24c"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:da09ad1c359a728e112d60116f626cc9f29730ff3e0e7db72b9a2dbc2e4beed5"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:184565012b60405d93838167f425713180b949e9d8dd0bbc7b49f074407c5a8b"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a75879bacf2c987c003368cf14bed0ffe99e8e85acfa6c0bfffc21a090f16880"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-win32.whl", hash = "sha256:84b554931e932c46f94ab306913ad7e11bba988104c5cff26d90d03f68258cd5"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-win_amd64.whl", hash = "sha256:25ac8c08322002b06fa1d49d1646181f0b2c72f5cbc15a85e80b4c30a544bb15"}, + {file = "ruamel.yaml.clib-0.2.8.tar.gz", hash = "sha256:beb2e0404003de9a4cab9753a8805a8fe9320ee6673136ed7f04255fe60bb512"}, +] + [[package]] name = "ruff" version = "0.5.5" @@ -3171,6 +4385,20 @@ files = [ {file = "Rx-1.6.3.tar.gz", hash = "sha256:ca71b65d0fc0603a3b5cfaa9e33f5ba81e4aae10a58491133595088d7734b2da"}, ] +[[package]] +name = "scp" +version = "0.15.0" +description = "scp module for paramiko" +optional = false +python-versions = "*" +files = [ + {file = "scp-0.15.0-py2.py3-none-any.whl", hash = "sha256:9e7f721e5ac563c33eb0831d0f949c6342f1c28c3bdc3b02f39d77b5ea20df7e"}, + {file = "scp-0.15.0.tar.gz", hash = "sha256:f1b22e9932123ccf17eebf19e0953c6e9148f589f93d91b872941a696305c83f"}, +] + +[package.dependencies] +paramiko = "*" + [[package]] name = "setuptools" version = "75.3.0" @@ -3228,6 +4456,17 @@ files = [ {file = "smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5"}, ] +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + [[package]] name = "social-auth-app-django" version = "5.4.2" @@ -3342,6 +4581,21 @@ files = [ {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, ] +[[package]] +name = "textfsm" +version = "1.1.3" +description = "Python module for parsing semi-structured text into python tables." +optional = false +python-versions = "*" +files = [ + {file = "textfsm-1.1.3-py2.py3-none-any.whl", hash = "sha256:dcbeebc6a6137bed561c71a56344d752e6dbc04ae5ea309252cb70fb97ccc9cd"}, + {file = "textfsm-1.1.3.tar.gz", hash = "sha256:577ef278a9237f5341ae9b682947cefa4a2c1b24dbe486f94f2c95addc6504b5"}, +] + +[package.dependencies] +future = "*" +six = "*" + [[package]] name = "to-json-schema" version = "1.0.1" @@ -3456,6 +4710,56 @@ files = [ docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"] +[[package]] +name = "transitions" +version = "0.9.2" +description = "A lightweight, object-oriented Python state machine implementation with many extensions." +optional = false +python-versions = "*" +files = [ + {file = "transitions-0.9.2-py2.py3-none-any.whl", hash = "sha256:f7b40c9b4a93869f36c4d1c33809aeb18cdeeb065fd1adba018ee39c3db216f3"}, + {file = "transitions-0.9.2.tar.gz", hash = "sha256:2f8490dbdbd419366cef1516032ab06d07ccb5839ef54905e842a472692d4204"}, +] + +[package.dependencies] +six = "*" + +[package.extras] +diagrams = ["pygraphviz"] +test = ["pytest"] + +[[package]] +name = "ttp" +version = "0.9.5" +description = "Template Text Parser" +optional = false +python-versions = ">=2.7,<4.0" +files = [ + {file = "ttp-0.9.5-py2.py3-none-any.whl", hash = "sha256:2c9fcf560b3f696e9fdd3554dc8e4622cbb10cac1d4fca13a7cf608c4a7fd137"}, + {file = "ttp-0.9.5.tar.gz", hash = "sha256:234414f4d3039d2d1cde09993f89f8db1b34d447f76c6a402555cefac2e59c4e"}, +] + +[package.extras] +docs = ["Sphinx (==4.3.0)", "readthedocs-sphinx-search (==0.1.1)", "sphinx_rtd_theme (==1.0.0)", "sphinxcontrib-applehelp (==1.0.1)", "sphinxcontrib-devhelp (==1.0.1)", "sphinxcontrib-htmlhelp (==2.0.0)", "sphinxcontrib-jsmath (==1.0.1)", "sphinxcontrib-napoleon (==0.7)", "sphinxcontrib-qthelp (==1.0.2)", "sphinxcontrib-serializinghtml (==1.1.5)", "sphinxcontrib-spelling (==7.2.1)"] +full = ["cerberus (>=1.3.0,<1.4.0)", "deepdiff (>=5.8.0,<5.9.0)", "jinja2 (>=3.0.0,<3.1.0)", "n2g (>=0.2.0,<0.3.0)", "openpyxl (>=3.0.0,<3.1.0)", "pyyaml (==6.0)", "tabulate (>=0.8.0,<0.9.0)", "ttp_templates (<1.0.0)", "yangson (>=1.4.0,<1.5.0)"] + +[[package]] +name = "ttp-templates" +version = "0.3.7" +description = "Template Text Parser Templates collections" +optional = false +python-versions = "<4.0,>=3.6" +files = [ + {file = "ttp_templates-0.3.7-py3-none-any.whl", hash = "sha256:2328304fb4c957ee60db6f301143e8a4556b22a12b3e2f30511e8ef97fc78f7e"}, + {file = "ttp_templates-0.3.7.tar.gz", hash = "sha256:f9103041a3683a0cb3811609ad990f679beadfc9a92c3e3fa05d6037414ad2bf"}, +] + +[package.dependencies] +ttp = ">=0.6.0" + +[package.extras] +docs = ["mkdocs (==1.2.4)", "mkdocs-material (==7.2.2)", "mkdocs-material-extensions (==1.0.1)", "mkdocstrings[python] (>=0.18.0,<0.19.0)", "pygments (==2.11)", "pymdown-extensions (==9.3)"] + [[package]] name = "typing-extensions" version = "4.12.2" @@ -3491,20 +4795,19 @@ files = [ [[package]] name = "urllib3" -version = "2.2.3" +version = "1.26.20" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=3.8" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ - {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, - {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, + {file = "urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e"}, + {file = "urllib3-1.26.20.tar.gz", hash = "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] +brotli = ["brotli (==1.0.9)", "brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "vine" @@ -3663,6 +4966,24 @@ files = [ {file = "wrapt-1.17.0.tar.gz", hash = "sha256:16187aa2317c731170a88ef35e8937ae0f533c402872c1ee5e6d079fcf320801"}, ] +[[package]] +name = "xmldiff" +version = "2.7.0" +description = "Creates diffs of XML files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "xmldiff-2.7.0-py3-none-any.whl", hash = "sha256:c8020e6aa4aa9fa13c72e5bf0eeafd0be998b0ab55d78b008abc75fbfebaca27"}, + {file = "xmldiff-2.7.0.tar.gz", hash = "sha256:c0910b1f800366dd7ec62923e5d06e8b06a1bd9120569a1c27f4f2446b9c68a2"}, +] + +[package.dependencies] +lxml = ">=3.1.0" +setuptools = "*" + +[package.extras] +devenv = ["black", "coverage", "flake8", "zest.releaser[recommended]"] + [[package]] name = "yamllint" version = "1.35.1" @@ -3681,6 +5002,20 @@ pyyaml = "*" [package.extras] dev = ["doc8", "flake8", "flake8-import-order", "rstcheck[sphinx]", "sphinx"] +[[package]] +name = "yamlordereddictloader" +version = "0.4.2" +description = "YAML loader and dumper for PyYAML allowing to keep keys order." +optional = false +python-versions = "*" +files = [ + {file = "yamlordereddictloader-0.4.2-py3-none-any.whl", hash = "sha256:dc048adb67026786cd24119bd71241f35bc8b0fd37d24b415c37bbc8049f9cd7"}, + {file = "yamlordereddictloader-0.4.2.tar.gz", hash = "sha256:36af2f6210fcff5da4fc4c12e1d815f973dceb41044e795e1f06115d634bca13"}, +] + +[package.dependencies] +pyyaml = "*" + [[package]] name = "zipp" version = "3.20.2" @@ -3706,4 +5041,4 @@ all = [] [metadata] lock-version = "2.0" python-versions = ">=3.8,<3.13" -content-hash = "f2041fa5a92502d80e47c6a6e762583dceabb072a9c36f52abed52a7ef2da478" +content-hash = "21528eed38f42921aa5bc3f5a9a8a1f07446965f212289876cbebd0d00e9033a"