diff --git a/.flake8 b/.flake8 index 9875fe18..6c56446a 100644 --- a/.flake8 +++ b/.flake8 @@ -1,7 +1,9 @@ [flake8] +# E501 - Line length is enforced by Black, so flake8 doesn't need to check it +# W503 - Black disagrees with this rule, as does PEP 8; Black wins ignore = - E501, # Line length is enforced by Black, so flake8 doesn't need to check it - W503 # Black disagrees with this rule, as does PEP 8; Black wins + E501, + W503 exclude = migrations, __pycache__, diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a6fe4bcd..2d07a667 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,6 @@ --- name: "CI" -concurrency: # Cancel any existing runs of this workflow for this same PR +concurrency: # Cancel any existing runs of this workflow for this same PR group: "${{ github.workflow }}-${{ github.ref }}" cancel-in-progress: true on: # yamllint disable-line rule:truthy rule:comments diff --git a/development/development.env b/development/development.env index c81fe526..e9690e6f 100644 --- a/development/development.env +++ b/development/development.env @@ -39,3 +39,6 @@ MYSQL_DATABASE=${NAUTOBOT_DB_NAME} MYSQL_ROOT_HOST=% DESIGN_BUILDER_ENABLE_BGP=True + +NAUTOBOT_CELERY_TASK_SOFT_TIME_LIMIT=86400 +NAUTOBOT_CELERY_TASK_TIME_LIMIT=86401 diff --git a/development/docker-compose.test-designs.yml b/development/docker-compose.test-designs.yml new file mode 100644 index 00000000..af1dd803 --- /dev/null +++ b/development/docker-compose.test-designs.yml @@ -0,0 +1,9 @@ +--- +version: "3.8" +services: + nautobot: + volumes: + - "../nautobot_design_builder/tests/designs:/opt/nautobot/jobs" + worker: + volumes: + - "../nautobot_design_builder/tests/designs:/opt/nautobot/jobs" diff --git a/docs/admin/release_notes/version_1.1.md b/docs/admin/release_notes/version_1.1.md new file mode 100644 index 00000000..d72302d0 --- /dev/null +++ b/docs/admin/release_notes/version_1.1.md @@ -0,0 +1,23 @@ +# v1.1.0 Release Notes + +## Release Overview + +The 1.1 release of Design Builder mostly includes performance improvements in the implementation system. There is a breaking change related to one-to-one relationships. Prior to the 1.1 release, one-to-one relationships were not saved until after the parent object was saved. The performance optimization work revealed this as a performance issue and now one-to-one relationships are treated as simple foreign keys. Since foreign key saves are not deferred by default, it may now be necessary to explicitly specify deferring the save operation. A new `deferred` attribute has been introduced that causes design builder to defer saving the foreign-key relationship until after the parent object has been saved. The one known case that is affected by this change is when setting a device primary IP when the IP itself is created as a member of an interface in the same device block. See unit tests and design examples for further explanation. + +Additionally, the `design.Builder` class has been renamed to `design.Environment` to better reflect what that class does. A `Builder` alias has been added to `Environment` for backwards compatibility with a deprecation warning. + +## [v1.1.0] - 2024-04 + +### Added + +- Added `deferred` attribute to allow deferral of field assignment. See notes in the Changed section. + +- Added `model_metadata` attribute to models. At the moment, this provides the ability to specify additional arguments passed to the `save` method of models being updated. The known use case for this is in creating Git repositories in Nautobot 1.x where `trigger_resync` must be `False`. In the future, additional fields will be added to `model_metadata` to provide new functionality. + +### Changed + +- Renamed `nautobot_design_builder.design.Builder` to `nautobot_design_builder.Environment` - aliased original name with deprecation warning. + +- Any designs that set `OneToOne` relationships (such as device `primary_ip4`) may now need a `deferred: true` statement in their design for those fields. Previously, `OneToOne` relationships were always deferred and this is usually unnecessary. Any deferrals must now be explicit. + +- Design reports are now saved to the file `report.md` for Nautobot 2.x installations. diff --git a/docs/dev/code_reference/fields.md b/docs/dev/code_reference/fields.md new file mode 100644 index 00000000..17e5c757 --- /dev/null +++ b/docs/dev/code_reference/fields.md @@ -0,0 +1 @@ +::: nautobot_design_builder.fields diff --git a/examples/custom_design/designs/l3vpn/context/__init__.py b/examples/custom_design/designs/l3vpn/context/__init__.py index 3489e7cf..4c32e96c 100644 --- a/examples/custom_design/designs/l3vpn/context/__init__.py +++ b/examples/custom_design/designs/l3vpn/context/__init__.py @@ -1,9 +1,8 @@ from django.core.exceptions import ObjectDoesNotExist import ipaddress -from functools import lru_cache -from nautobot.dcim.models import Device, Interface -from nautobot.ipam.models import VRF, Prefix +from nautobot.dcim.models import Device +from nautobot.ipam.models import VRF from nautobot_design_builder.context import Context, context_file @@ -19,20 +18,6 @@ class L3VPNContext(Context): def __hash__(self): return hash((self.pe.name, self.ce.name, self.customer_name)) - @lru_cache - def get_l3vpn_prefix(self, parent_prefix, prefix_length): - tag = self.design_instance_tag - if tag: - existing_prefix = Prefix.objects.filter(tags__in=[tag], prefix_length=30).first() - if existing_prefix: - return str(existing_prefix) - - for new_prefix in ipaddress.ip_network(parent_prefix).subnets(new_prefix=prefix_length): - try: - Prefix.objects.get(prefix=str(new_prefix)) - except ObjectDoesNotExist: - return new_prefix - def get_customer_id(self, customer_name, l3vpn_asn): try: vrf = VRF.objects.get(description=f"VRF for customer {customer_name}") @@ -44,16 +29,6 @@ def get_customer_id(self, customer_name, l3vpn_asn): new_id = int(last_vrf.name.split(":")[-1]) + 1 return str(new_id) - def get_interface_name(self, device): - root_interface_name = "GigabitEthernet" - interfaces = Interface.objects.filter(name__contains=root_interface_name, device=device) - tag = self.design_instance_tag - if tag: - existing_interface = interfaces.filter(tags__in=[tag]).first() - if existing_interface: - return existing_interface.name - return f"{root_interface_name}1/{len(interfaces) + 1}" - def get_ip_address(self, prefix, offset): net_prefix = ipaddress.ip_network(prefix) for count, host in enumerate(net_prefix): diff --git a/examples/custom_design/designs/l3vpn/designs/0001_ipam.yaml.j2 b/examples/custom_design/designs/l3vpn/designs/0001_ipam.yaml.j2 index 4d8ae1de..14b0dd94 100644 --- a/examples/custom_design/designs/l3vpn/designs/0001_ipam.yaml.j2 +++ b/examples/custom_design/designs/l3vpn/designs/0001_ipam.yaml.j2 @@ -1,14 +1,25 @@ --- -vrfs: - - "!create_or_update:name": "{{ l3vpn_asn }}:{{ get_customer_id(customer_name, l3vpn_asn) }}" - description: "VRF for customer {{ customer_name }}" - "!ref": "my_vrf" - +tags: + - "!create_or_update:name": "VRF Prefix" + "slug": "vrf_prefix" + - "!create_or_update:name": "VRF Interface" + "slug": "vrf_interface" prefixes: - "!create_or_update:prefix": "{{ l3vpn_prefix }}" status__name: "Reserved" - - "!create_or_update:prefix": "{{ get_l3vpn_prefix(l3vpn_prefix, l3vpn_prefix_length) }}" - status__name: "Reserved" - vrf: "!ref:my_vrf" + +vrfs: + - "!create_or_update:name": "{{ l3vpn_asn }}:{{ get_customer_id(customer_name, l3vpn_asn) }}" + description: "VRF for customer {{ customer_name }}" + prefixes: + - "!next_prefix": + identified_by: + tags__name: "VRF Prefix" + prefix: "{{ l3vpn_prefix }}" + length: 30 + status__name: "Reserved" + tags: + - {"!get:name": "VRF Prefix"} + "!ref": "l3vpn_p2p_prefix" diff --git a/examples/custom_design/designs/l3vpn/designs/0002_devices.yaml.j2 b/examples/custom_design/designs/l3vpn/designs/0002_devices.yaml.j2 index edc189e0..6687fa66 100644 --- a/examples/custom_design/designs/l3vpn/designs/0002_devices.yaml.j2 +++ b/examples/custom_design/designs/l3vpn/designs/0002_devices.yaml.j2 @@ -8,18 +8,22 @@ "mpls_router": true, } interfaces: - - "!create_or_update:name": "{{ get_interface_name(device) }}" + - "!next_interface": {} status__name: "Planned" type: "other" {% if offset == 2 %} "!connect_cable": status__name: "Planned" - to: - device__name: "{{ other_device.name }}" - name: "{{ get_interface_name(other_device) }}" + to: "!ref:other_interface" + {% else %} + "!ref": "other_interface" {% endif %} + tags: + - {"!get:name": "VRF Interface"} ip_addresses: - - "!create_or_update:address": "{{ get_ip_address(get_l3vpn_prefix(l3vpn_prefix, l3vpn_prefix_length), offset) }}" + - "!child_prefix:address": + parent: "!ref:l3vpn_p2p_prefix" + offset: "0.0.0.{{ offset }}/30" status__name: "Reserved" {% endmacro %} diff --git a/examples/custom_design/designs/l3vpn/jobs.py b/examples/custom_design/designs/l3vpn/jobs.py index acc126c6..6f699bf1 100644 --- a/examples/custom_design/designs/l3vpn/jobs.py +++ b/examples/custom_design/designs/l3vpn/jobs.py @@ -2,15 +2,50 @@ from django.core.exceptions import ValidationError -from nautobot.dcim.models import Device +from nautobot.dcim.models import Device, Interface from nautobot.extras.jobs import ObjectVar, StringVar from nautobot_design_builder.design_job import DesignJob +from nautobot_design_builder.design import ModelInstance +from nautobot_design_builder.ext import AttributeExtension from nautobot_design_builder.contrib import ext from .context import L3VPNContext +class NextInterfaceExtension(AttributeExtension): + """Attribute extension to calculate the next available interface name.""" + + tag = "next_interface" + + def attribute(self, *args, value, model_instance: ModelInstance) -> dict: + """Determine the next available interface name. + + Args: + *args: Any additional arguments following the tag name. These are `:` delimited. + value (Any): The value of the data structure at this key's point in the design YAML. This could be a scalar, a dict or a list. + model_instance (ModelInstance): Object is the ModelInstance that would ultimately contain the values. + + Returns: + dict: Dictionary with the new interface name `{"!create_or_update:name": new_interface_name} + """ + root_interface_name = "GigabitEthernet" + previous_interfaces = self.environment.design_instance.get_design_objects(Interface).values_list( + "id", flat=True + ) + interfaces = model_instance.relationship_manager.filter( + name__startswith="GigabitEthernet", + ) + existing_interface = interfaces.filter( + pk__in=previous_interfaces, + tags__name="VRF Interface", + ).first() + if existing_interface: + model_instance.instance = existing_interface + return {"!create_or_update:name": existing_interface.name} + return {"!create_or_update:name": f"{root_interface_name}1/{len(interfaces) + 1}"} + + class L3vpnDesign(DesignJob): """Create a l3vpn connection.""" @@ -38,7 +73,12 @@ class Meta: "designs/0002_devices.yaml.j2", ] context_class = L3VPNContext - extensions = [ext.CableConnectionExtension] + extensions = [ + ext.CableConnectionExtension, + ext.NextPrefixExtension, + NextInterfaceExtension, + ext.ChildPrefixExtension, + ] @staticmethod def validate_data_logic(data): diff --git a/mkdocs.yml b/mkdocs.yml index 8c0173d4..9e808cb8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -118,6 +118,7 @@ nav: - Release Notes: - "admin/release_notes/index.md" - v1.0: "admin/release_notes/version_1.0.md" + - v1.1: "admin/release_notes/version_1.1.md" - Developer Guide: - Extending the App: "dev/extending.md" - Contributing to the App: "dev/contributing.md" @@ -127,6 +128,7 @@ nav: - Package: "dev/code_reference/package.md" - Context: "dev/code_reference/context.md" - Design Builder: "dev/code_reference/design.md" + - Field Descriptors: "dev/code_reference/fields.md" - Design Job: "dev/code_reference/design_job.md" - Jinja Rendering: "dev/code_reference/jinja2.md" - Template Extensions: "dev/code_reference/ext.md" diff --git a/nautobot_design_builder/constants.py b/nautobot_design_builder/constants.py deleted file mode 100644 index a14c9871..00000000 --- a/nautobot_design_builder/constants.py +++ /dev/null @@ -1,4 +0,0 @@ -"""Constants used in Design Builder.""" - -NAUTOBOT_ID = "nautobot_identifier" -IDENTIFIER_KEYS = ["!create_or_update", "!create", "!update", "!get"] diff --git a/nautobot_design_builder/context.py b/nautobot_design_builder/context.py index 3b2b8673..0139f800 100644 --- a/nautobot_design_builder/context.py +++ b/nautobot_design_builder/context.py @@ -3,13 +3,12 @@ from functools import cached_property from collections import UserList, UserDict, UserString import inspect -from typing import Any, Union +from typing import Any import yaml from jinja2.nativetypes import NativeEnvironment from nautobot.extras.models import JobResult -from nautobot.extras.models import Tag from nautobot_design_builder.errors import DesignValidationError from nautobot_design_builder.jinja2 import new_template_environment @@ -371,14 +370,6 @@ def validate(self): if len(errors) > 0: raise DesignValidationError("\n".join(errors)) - @property - def design_instance_tag(self) -> Union[Tag, None]: - """Returns the `Tag` of the design instance if exists.""" - try: - return Tag.objects.get(name__contains=self._instance_name) - except Tag.DoesNotExist: - return None - @property def _instance_name(self): if nautobot_version < "2.0.0": diff --git a/nautobot_design_builder/contrib/ext.py b/nautobot_design_builder/contrib/ext.py index b9ebbc9a..a36d3d86 100644 --- a/nautobot_design_builder/contrib/ext.py +++ b/nautobot_design_builder/contrib/ext.py @@ -13,19 +13,17 @@ from nautobot.ipam.models import Prefix import netaddr -from nautobot_design_builder.design import Builder -from nautobot_design_builder.design import ModelInstance +from nautobot_design_builder.design import Environment, ModelInstance, ModelMetadata from nautobot_design_builder.errors import DesignImplementationError, MultipleObjectsReturnedError, DoesNotExistError from nautobot_design_builder.ext import AttributeExtension from nautobot_design_builder.jinja2 import network_offset -from nautobot_design_builder.constants import NAUTOBOT_ID class LookupMixin: """A helper mixin that provides a way to lookup objects.""" - builder: Builder + environment: Environment def lookup_by_content_type(self, app_label, model_name, query): """Perform a query on a model. @@ -67,7 +65,10 @@ def _flatten(query: dict, prefix="") -> Iterator[Tuple[str, Any]]: if isinstance(value, dict): yield from LookupMixin._flatten(value, f"{prefix}{key}__") else: - yield (f"{prefix}{key}", value) + if isinstance(value, ModelInstance): + yield (f"!get:{prefix}{key}", value.instance) + else: + yield (f"!get:{prefix}{key}", value) @staticmethod def flatten_query(query: dict) -> Dict[str, Any]: @@ -98,7 +99,7 @@ def flatten_query(query: dict) -> Dict[str, Any]: """ return dict(LookupMixin._flatten(query)) - def lookup(self, queryset, query, parent=None): + def lookup(self, queryset, query, parent: ModelInstance = None): """Perform a single object lookup from a queryset. Args: @@ -115,10 +116,19 @@ def lookup(self, queryset, query, parent=None): Returns: Any: The object matching the query. """ - query = self.builder.resolve_values(query, unwrap_model_instances=True) + query = self.environment.resolve_values(query) + # it's possible an extension actually returned the instance we need, in + # that case, no need to look it up. This is especially true for the + # !ref extension used as a value. + if isinstance(query, ModelInstance): + return query + query = self.flatten_query(query) try: - return queryset.get(**query) + model_class = self.environment.model_class_index[queryset.model] + if parent: + return parent.create_child(model_class, query, queryset) + return model_class(self.environment, query, queryset) except ObjectDoesNotExist: # pylint: disable=raise-missing-from raise DoesNotExistError(queryset.model, query_filter=query, parent=parent) @@ -139,6 +149,7 @@ def attribute(self, *args, value, model_instance) -> None: # pylint:disable=arg assign it to an attribute of another object. Args: + *args: Any additional arguments following the tag name. These are `:` delimited. value: A filter describing the object to get. Keys should map to lookup parameters equivalent to Django's `filter()` syntax for the given model. The special `type` parameter will override the relationship's model class @@ -226,10 +237,12 @@ def get_query_managers(endpoint_type): return query_managers - def attribute(self, value, model_instance) -> None: + def attribute(self, *args, value=None, model_instance: ModelInstance = None) -> None: """Connect a cable termination to another cable termination. Args: + *args: Any additional arguments following the tag name. These are `:` delimited. + value: Dictionary with details about the cable. At a minimum the dictionary must have a `to` key which includes a query dictionary that will return exactly one object to be added to the @@ -243,8 +256,7 @@ def attribute(self, value, model_instance) -> None: termination was found. Returns: - None: This tag does not return a value, as it adds a deferred object - representing the cable connection. + dict: All of the information needed to lookup or create a cable instance. Example: ```yaml @@ -265,9 +277,6 @@ def attribute(self, value, model_instance) -> None: name: "GigabitEthernet1" ``` """ - cable_id = value.pop(NAUTOBOT_ID, None) - connected_object_uuid = model_instance.attributes.get(NAUTOBOT_ID, None) - if "to" not in value: raise DesignImplementationError( f"`connect_cable` must have a `to` field indicating what to terminate to. {value}" @@ -288,48 +297,24 @@ def attribute(self, value, model_instance) -> None: cable_attributes.update( { "termination_a": model_instance, - "!create_or_update:termination_b_type": ContentType.objects.get_for_model(remote_instance), - "!create_or_update:termination_b_id": remote_instance.id, + "!create_or_update:termination_b_type_id": ContentType.objects.get_for_model( + remote_instance.instance + ).id, + "!create_or_update:termination_b_id": remote_instance.instance.id, } ) - # TODO: Some extensions may need to do some previous work to be able to be implemented - # For example, to set up this cable connection on an interface, we have to disconnect - # previously existing ones. And this is something that can be postponed for the cleanup phase - # We could change the paradigm of having attribute as an abstract method, and create a generic - # attribute method in the `AttributeExtension` that calls several hooks, one for setting - # (the current one), and one for pre-cleaning that would be custom for every case (and optional) - - # This is the custom implementation of the pre-clean up method for the connect_cable extension - if connected_object_uuid: - connected_object = model_instance.model_class.objects.get(id=connected_object_uuid) - - if cable_id: - existing_cable = dcim.Cable.objects.get(id=cable_id) - - if ( - connected_object_uuid - and connected_object.id == existing_cable.termination_a.id - and existing_cable.termination_b.id == remote_instance.id - ): - # If the cable is already connecting what needs to be connected, it passes - return - - model_instance.creator.decommission_object(cable_id, cable_id) - - elif connected_object_uuid and hasattr(connected_object, "cable") and connected_object.cable: - model_instance.creator.decommission_object(str(connected_object.cable.id), str(connected_object.cable)) - - model_instance.deferred.append("cable") - model_instance.deferred_attributes["cable"] = [ - model_instance.__class__( - self.builder, - model_class=dcim.Cable, - attributes=cable_attributes, - ext_tag=f"!{self.tag}", - ext_value=value, - ) - ] + def connect(): + existing_cable = dcim.Cable.objects.filter(termination_a_id=model_instance.instance.id).first() + if existing_cable: + if existing_cable.termination_b_id == remote_instance.instance.id: + return + self.environment.decommission_object(existing_cable.id, f"Cable {existing_cable.id}") + Cable = ModelInstance.factory(dcim.Cable) # pylint:disable=invalid-name + cable = Cable(self.environment, cable_attributes) + cable.save() + + model_instance.connect("POST_INSTANCE_SAVE", connect) class NextPrefixExtension(AttributeExtension): @@ -337,10 +322,12 @@ class NextPrefixExtension(AttributeExtension): tag = "next_prefix" - def attribute(self, value: dict, model_instance) -> None: + def attribute(self, *args, value: dict = None, model_instance: ModelInstance = None) -> None: """Provides the `!next_prefix` attribute that will calculate the next available prefix. Args: + *args: Any additional arguments following the tag name. These are `:` delimited. + value: A filter describing the parent prefix to provision from. If `prefix` is one of the query keys then the network and prefix length will be split and used as query arguments for the underlying Prefix object. The @@ -363,12 +350,20 @@ def attribute(self, value: dict, model_instance) -> None: - "10.0.0.0/23" - "10.0.2.0/23" length: 24 + identified_by: + tag__name: "some tag name" status__name: "Active" ``` """ if not isinstance(value, dict): raise DesignImplementationError("the next_prefix tag requires a dictionary of arguments") - + identified_by = value.pop("identified_by", None) + if identified_by: + try: + model_instance.instance = model_instance.relationship_manager.get(**identified_by) + return None + except ObjectDoesNotExist: + pass length = value.pop("length", None) if length is None: raise DesignImplementationError("the next_prefix tag requires a prefix length") @@ -398,7 +393,8 @@ def attribute(self, value: dict, model_instance) -> None: query = Q(**value) & reduce(operator.or_, prefix_q) prefixes = Prefix.objects.filter(query) - return "prefix", self._get_next(prefixes, length) + attr = args[0] if args else "prefix" + return attr, self._get_next(prefixes, length) @staticmethod def _get_next(prefixes, length) -> str: @@ -424,7 +420,7 @@ class ChildPrefixExtension(AttributeExtension): tag = "child_prefix" - def attribute(self, value: dict, model_instance) -> None: + def attribute(self, *args, value: dict = None, model_instance=None) -> None: """Provides the `!child_prefix` attribute. !child_prefix calculates a child prefix using a parent prefix @@ -433,6 +429,7 @@ def attribute(self, value: dict, model_instance) -> None: object. Args: + *args: Any additional arguments following the tag name. These are `:` delimited. value: a dictionary containing the `parent` prefix (string or `Prefix` instance) and the `offset` in the form of a CIDR string. The length of the child prefix will match the length @@ -480,8 +477,8 @@ def attribute(self, value: dict, model_instance) -> None: raise DesignImplementationError("the child_prefix tag requires an offset") if not isinstance(offset, str): raise DesignImplementationError("offset must be string") - - return "prefix", network_offset(parent, offset) + attr = args[0] if args else "prefix" + return attr, network_offset(parent, offset) class BGPPeeringExtension(AttributeExtension): @@ -489,7 +486,7 @@ class BGPPeeringExtension(AttributeExtension): tag = "bgp_peering" - def __init__(self, builder: Builder): + def __init__(self, environment: Environment): """Initialize the BGPPeeringExtension. This initializer will import the necessary BGP models. If the @@ -498,28 +495,19 @@ def __init__(self, builder: Builder): Raises: DesignImplementationError: Raised when the BGP Models App is not installed. """ - super().__init__(builder) + super().__init__(environment) try: from nautobot_bgp_models.models import PeerEndpoint, Peering # pylint:disable=import-outside-toplevel - self.PeerEndpoint = PeerEndpoint # pylint:disable=invalid-name - self.Peering = Peering # pylint:disable=invalid-name + self.PeerEndpoint = ModelInstance.factory(PeerEndpoint) # pylint:disable=invalid-name + self.Peering = ModelInstance.factory(Peering) # pylint:disable=invalid-name except ModuleNotFoundError: # pylint:disable=raise-missing-from raise DesignImplementationError( "the `bgp_peering` tag can only be used when the bgp models app is installed." ) - @staticmethod - def _post_save(sender, instance, **kwargs) -> None: # pylint:disable=unused-argument - peering_instance: ModelInstance = instance - endpoint_a = peering_instance.instance.endpoint_a - endpoint_z = peering_instance.instance.endpoint_z - endpoint_a.peer, endpoint_z.peer = endpoint_z, endpoint_a - endpoint_a.save() - endpoint_z.save() - - def attribute(self, value, model_instance) -> None: + def attribute(self, *args, value=None, model_instance: ModelInstance = None) -> None: """This attribute tag creates or updates a BGP peering for two endpoints. !bgp_peering will take an `endpoint_a` and `endpoint_z` argument to correctly @@ -527,7 +515,9 @@ def attribute(self, value, model_instance) -> None: Design Builder syntax. Args: - value (dict): dictionary containing the keys `entpoint_a` + *args: Any additional arguments following the tag name. These are `:` delimited. + + value (dict): dictionary containing the keys `endpoint_a` and `endpoint_z`. Both of these keys must be dictionaries specifying a way to either lookup or create the appropriate peer endpoints. @@ -566,14 +556,14 @@ def attribute(self, value, model_instance) -> None: # copy the value so it won't be modified in later # use retval = {**value} - endpoint_a = ModelInstance(self.builder, self.PeerEndpoint, retval.pop("endpoint_a")) - endpoint_z = ModelInstance(self.builder, self.PeerEndpoint, retval.pop("endpoint_z")) + endpoint_a = self.PeerEndpoint(self.environment, retval.pop("endpoint_a")) + endpoint_z = self.PeerEndpoint(self.environment, retval.pop("endpoint_z")) peering_a = None peering_z = None try: peering_a = endpoint_a.instance.peering peering_z = endpoint_z.instance.peering - except self.Peering.DoesNotExist: + except self.Peering.model_class.DoesNotExist: pass # try to prevent empty peerings @@ -587,8 +577,16 @@ def attribute(self, value, model_instance) -> None: peering_z.delete() retval["endpoints"] = [endpoint_a, endpoint_z] - endpoint_a.attributes["peering"] = model_instance - endpoint_z.attributes["peering"] = model_instance - - model_instance.connect(ModelInstance.POST_SAVE, BGPPeeringExtension._post_save) + endpoint_a.metadata.attributes["peering"] = model_instance + endpoint_z.metadata.attributes["peering"] = model_instance + + def post_save(): + peering_instance: ModelInstance = model_instance + endpoint_a = peering_instance.instance.endpoint_a + endpoint_z = peering_instance.instance.endpoint_z + endpoint_a.peer, endpoint_z.peer = endpoint_z, endpoint_a + endpoint_a.save() + endpoint_z.save() + + model_instance.connect(ModelMetadata.POST_SAVE, post_save) return retval diff --git a/nautobot_design_builder/contrib/tests/testdata/nautobot_v2/bgp_extension.yaml b/nautobot_design_builder/contrib/tests/testdata/nautobot_v2/bgp_extension.yaml index 627df53e..e0a7614d 100644 --- a/nautobot_design_builder/contrib/tests/testdata/nautobot_v2/bgp_extension.yaml +++ b/nautobot_design_builder/contrib/tests/testdata/nautobot_v2/bgp_extension.yaml @@ -17,8 +17,8 @@ designs: - "!get:app_label": "dcim" "!get:model": "device" locations: - name: "Site" - status__name: "Active" + - name: "Site" + status__name: "Active" manufacturers: - "!create_or_update:name": "test-manufacturer" diff --git a/nautobot_design_builder/debug.py b/nautobot_design_builder/debug.py new file mode 100644 index 00000000..b627edca --- /dev/null +++ b/nautobot_design_builder/debug.py @@ -0,0 +1,61 @@ +"""Utilities for debugging object assignment and saving.""" + +from django.db import models + +indent = "" # pylint:disable=invalid-name +DEBUG = False + + +class ObjDetails: # noqa:D101 # pylint:disable=too-few-public-methods,missing-class-docstring + def __init__(self, obj): # noqa:D107 # pylint:disable=missing-function-docstring + self.instance = obj + if hasattr(obj, "instance"): + self.instance = obj.instance + try: + description = str(obj) + if description.startswith(" str: + """Determine the action. + + This property will always return a value. If no action has been explicitly + set in a design object, then the default action is `CREATE`. If an action + has been determined (based on action tags) then that action is returned. + + Returns: + str: One of the valid values for action: `GET`, `CREATE`, `UPDATE`, `CREATE_OR_UPDATE` + """ + if self._action is None: + return self.CREATE + return self._action + + @action.setter + def action(self, action: str): + """Set the action for a given model instance. + + Args: + action (str): The indicated action (`GET`, `CREATE`, `UPDATE`, `CREATE_OR_UPDATE`) + + This setter confirms that exactly one action type is specified for a model instance. + The setter may be called multiple times with the same action type. However, if the + setter is called more than once with different action types then a `DesignImplementationError` + is raised. + + Raises: + errors.DesignImplementationError: If an unknown action has been specified or if the + specified action is different than what was previously set. + """ + if action not in self.ACTION_CHOICES: + raise errors.DesignImplementationError(f"Unknown action {action}", self.model_instance.model_class) + + if self._action is None or self._action == action: + self._action = action + return + + raise errors.DesignImplementationError( + f"Can perform only one action for a model, got both {self._action} and {action}", + self.model_instance.model_class, + ) + + @property + def attributes(self): + """Get any attributes that have been processed.""" + return self._attributes + + @attributes.setter + def attributes(self, attributes: Dict[str, Any]): + """Process and assign attributes for this metadata. + + Args: + attributes (Dict[str, Any]): The input attributes to be processed. + This should be a dictionary of key/value pairs where the keys + match the field names and properties of a given model type. The + attributes are processed sequentially. Any action tags are looked up + and executed in this step. + + Raises: + errors.DesignImplementationError: A `DesignImplementationError` can be raised + for a number of different error conditions if an extension cannot be found + or returns an unknown type. The error can also be raised if a dictionary + key cannot be mapped to a model field or property. + """ + self._attributes = {**attributes} + self._kwargs = {} + self._filter = {} + self._custom_fields = self._attributes.pop("custom_fields", {}) + + attribute_names = list(self._attributes.keys()) + while attribute_names: + key = attribute_names.pop(0) + self._attributes[key] = self.environment.resolve_values(self._attributes[key]) + if hasattr(self, key): + setattr(self, f"_{key}", self._attributes.pop(key)) + elif key.startswith("!"): + value = self._attributes.pop(key) + args = key.lstrip("!").split(":") + + extn: ext.AttributeExtension = self.environment.get_extension("attribute", args[0]) + if extn: + result = extn.attribute(*args[1:], value=value, model_instance=self.model_instance) + if isinstance(result, tuple): + self._attributes[result[0]] = result[1] + elif isinstance(result, dict): + self._attributes.update(result) + attribute_names.extend(result.keys()) + elif result is not None: + raise errors.DesignImplementationError(f"Cannot handle extension return type {type(result)}") + else: + self.action = args[0] + self._filter[args[1]] = value + elif "__" in key: + fieldname, search = key.split("__", 1) + if not hasattr(self.model_instance.model_class, fieldname): + raise errors.DesignImplementationError( + f"{fieldname} is not a property", self.model_instance.model_class + ) + self._attributes[fieldname] = {f"!get:{search}": self._attributes.pop(key)} + elif not hasattr(self.model_instance, key): + value = self._attributes.pop(key) + if isinstance(value, ModelInstance): + value = value.instance + self._kwargs[key] = value + + def connect(self, signal: str, handler: FunctionType): + """Connect a handler between this model instance (as sender) and signal. + + Args: + signal (str): Signal to listen for. + handler (FunctionType): Callback function + """ + self._signals[signal].append(handler) + + def send(self, signal: str): + """Send a signal to all associated listeners. + + Args: + signal (str): The signal to send + """ + for handler in self._signals[signal]: + handler() + self.model_instance.instance.refresh_from_db() + + @property + def custom_fields(self) -> Dict[str, Any]: + """`custom_fields` property. + + When attributes are processed, the `custom_fields` key is removed and assigned + to the `custom_fields` property. + + Returns: + Dict[str, Any]: A dictionary of custom fields/values. + """ + return self._custom_fields + + @property + def deferred(self) -> bool: + """Whether or not this model object's save should be deferred. + + Sometimes a model, specified as a child within a design, must be + saved after the parent. One good example of this is (in Nautobot 1.x) + a `Device.primary_ip4`. If the IP address itself is created within + the device's interface block, and that interface block is defined in the + same block as the `primary_ip4`, then the `primary_ip4` field cannot be + set until after the interface's IP has been created. Since the interface + cannot be created until after the device has been saved (since the interface + has a required foreign-key field to device) then the sequence must go like this: + + 1) Save the new device. + 2) Save the IP address that will be assigned to the interface + 3) Save the interface with foreign keys for device and IP address + 4) Set device's `primary_ip4` and re-save the device. + + The only way to tell design builder to do step 4 last is to set the value on + the field to `deferred`. This deferral can be specified as in the following example: + + ```yaml + # Note: the following example is for Nautobot 1.x + devices: + - name: "device_1" + site__name: "site_1" + status__name: "Active" + device_type__model: "model name" + device_role__name: "device role" + interfaces: + - name: "Ethernet1/1" + type: "virtual" + status__name: "Active" + description: "description for Ethernet1/1" + ip_addresses: + - address: "192.168.56.1/24" + status__name: "Active" + primary_ip4: {"!get:address": "192.168.56.1/24", "deferred": true} + ``` + + Returns: + bool: Whether or not the object's assignment should be deferred. + """ + return self._deferred + + @property + def filter(self): + """The processed query filter to find the object.""" + return self._filter + + @property + def kwargs(self): + """Any keyword arguments needed for the creation of the model object.""" + return self._kwargs + + @property + def query_filter_values(self): + """Returns a copy of the query-filter field/values.""" + return {**self._filter} + + @property + def query_filter(self) -> Dict[str, Any]: + """Calculate the query filter for the object. + + The `query_filter` property collects all of the lookups for an object + (set by `!create_or_update` and `!get` tags) and computes a dictionary + that can be used as keyword arguments to a model manager `.get` method. + + Returns: + Dict[str, Any]: The computed query filter. + """ + return _map_query_values(self._filter) + + +class ModelInstance: + """An individual object to be created or updated as Design Builder iterates through a rendered design YAML file. + + `ModelInstance` objects are essentially proxy objects between the design builder implementation process + and normal Django models. The `ModelInstance` intercepts value assignments to fields and properly + defers database saves so that `ForeignKey` and `ManyToMany` fields are set and saved in the correct order. + + This field proxying also provides a system to model relationships that are more complex than simple + database fields and relationships (such as Nautobot custom relationships). + """ + + name: str + model_class: Type[Model] + + @classmethod + def factory(cls, django_class: Type[Model]) -> "ModelInstance": + """`factory` takes a normal Django model class and creates a dynamic ModelInstance proxy class. + + Args: + django_class (Type[Model]): The Django model class to wrap in a proxy class. + + Returns: + type[ModelInstance]: The newly created proxy class. + """ + cls_attributes = { + "model_class": django_class, + "name": django_class.__name__, + } + + field: DjangoField + for field in django_class._meta.get_fields(): + cls_attributes[field.name] = field_factory(None, field) + model_class = type(django_class.__name__, (ModelInstance,), cls_attributes) + return model_class def __init__( self, - creator: "Builder", - model_class: Type[Model], + environment: "Environment", attributes: dict, relationship_manager=None, parent=None, - ext_tag=None, - ext_value=None, - ): # pylint:disable=too-many-arguments - """Constructor for a ModelInstance.""" - self.ext_tag = ext_tag - self.ext_value = ext_value - self.creator = creator - self.model_class = model_class - self.name = model_class.__name__ - self.instance: Model = None - # Make a copy of the attributes so the original - # design attributes are not overwritten - self.attributes = {**attributes} - self.parent = parent - self.deferred = [] - self.deferred_attributes = {} - self.signals = { - self.PRE_SAVE: Signal(), - self.POST_SAVE: Signal(), - } + ): + """Create a proxy instance for the model. - self.filter = {} - self.action = None - self.instance_fields = {} - self._kwargs = {} - for direction in Relationship.objects.get_for_model(model_class): - for relationship in direction: - self.instance_fields[relationship.slug] = field_factory(self, relationship) + This constructor will create a new `ModelInstance` object that wraps a Django + model instance. All assignments to this instance will be proxied to the underlying + object using the descriptors in the `fields` module. - field: DjangoField - for field in self.model_class._meta.get_fields(): - self.instance_fields[field.name] = field_factory(self, field) + Args: + environment (Environment): The build environment for the current design. + attributes (dict): The attributes dictionary for the current object. + relationship_manager (_type_, optional): The relationship manager to use for lookups. Defaults to None. + parent (_type_, optional): The parent this object belongs to in the design tree. Defaults to None. - self.created = False - self.nautobot_id = None - self._parse_attributes() + Raises: + errors.DoesNotExistError: If the object is being retrieved or updated (not created) and can't be found. + errors.MultipleObjectsReturnedError: If the object is being retrieved or updated (not created) + and more than one object matches the lookup criteria. + """ + self.environment = environment + self.instance: Model = None + self.metadata = ModelMetadata(self, **attributes.pop("model_metadata", {})) + + self._parent = parent + self.refresh_custom_relationships() self.relationship_manager = relationship_manager if self.relationship_manager is None: self.relationship_manager = self.model_class.objects + self.metadata.attributes = attributes + try: self._load_instance() except ObjectDoesNotExist as ex: raise errors.DoesNotExistError(self) from ex except MultipleObjectsReturned as ex: raise errors.MultipleObjectsReturnedError(self) from ex + self._update_fields() + + def refresh_custom_relationships(self): + """Look for any custom relationships for this model class and add any new fields.""" + for direction in Relationship.objects.get_for_model(self.model_class): + for relationship in direction: + field = field_factory(self, relationship) + + # make sure not to mask non-custom relationship fields that + # may have the same key name or field name + for attr_name in [field.key_name, field.field_name]: + if hasattr(self.__class__, attr_name): + # if there is already an attribute with the same name, + # delete it if it is a custom relationship, that way + # we reload the config from the database. + if isinstance(getattr(self.__class__, attr_name), CustomRelationshipField): + delattr(self.__class__, attr_name) + if not hasattr(self.__class__, attr_name): + setattr(self.__class__, attr_name, field) + + def __str__(self): + """Get the model class name.""" + return str(self.model_class) def get_changes(self, pre_change=None): """Determine the differences between the original instance and the current. @@ -287,13 +585,13 @@ def get_changes(self, pre_change=None): return calculate_changes( self.instance, initial_state=self._initial_state, - created=self.created, + created=self.metadata.created, pre_change=pre_change, ) def create_child( self, - model_class: Type[Model], + model_class: "ModelInstance", attributes: Dict, relationship_manager: Manager = None, ) -> "ModelInstance": @@ -307,93 +605,52 @@ def create_child( Returns: ModelInstance: Model instance that has its parent correctly set. """ - return ModelInstance( - self.creator, - model_class, - attributes, - relationship_manager, - parent=self, - ) - - def _parse_attributes(self): # pylint: disable=too-many-branches - self.custom_fields = self.attributes.pop("custom_fields", {}) - self.custom_relationships = self.attributes.pop("custom_relationships", {}) - attribute_names = list(self.attributes.keys()) - while attribute_names: - key = attribute_names.pop(0) - if key == NAUTOBOT_ID: - self.nautobot_id = self.attributes[key] - continue - - self.attributes[key] = self.creator.resolve_values(self.attributes[key]) - if key.startswith("!"): - value = self.attributes.pop(key) - args = key.lstrip("!").split(":") - - extn = self.creator.get_extension("attribute", args[0]) - if extn: - result = extn.attribute(*args[1:], value=self.creator.resolve_values(value), model_instance=self) - if isinstance(result, tuple): - self.attributes[result[0]] = result[1] - elif isinstance(result, dict): - self.attributes.update(result) - attribute_names.extend(result.keys()) - elif result is not None: - raise errors.DesignImplementationError(f"Cannot handle extension return type {type(result)}") - elif args[0] in [self.GET, self.UPDATE, self.CREATE_OR_UPDATE]: - self.action = args[0] - self.filter[args[1]] = value - - if self.action is None: - self.action = args[0] - elif self.action != args[0]: - raise errors.DesignImplementationError( - f"Can perform only one action for a model, got both {self.action} and {args[0]}", - self.model_class, - ) - else: - raise errors.DesignImplementationError(f"Unknown action {args[0]}", self.model_class) - elif "__" in key: - fieldname, search = key.split("__", 1) - if not hasattr(self.model_class, fieldname): - raise errors.DesignImplementationError(f"{fieldname} is not a property", self.model_class) - self.attributes[fieldname] = {search: self.attributes.pop(key)} - elif not hasattr(self.model_class, key) and key not in self.instance_fields: - value = self.creator.resolve_values(self.attributes.pop(key)) - if isinstance(value, ModelInstance): - value = value.instance - self._kwargs[key] = value - # raise errors.DesignImplementationError(f"{key} is not a property", self.model_class) - - if self.action is None: - self.action = self.CREATE - if self.action not in self.ACTION_CHOICES: - raise errors.DesignImplementationError(f"Unknown action {self.action}", self.model_class) - - def connect(self, signal: Signal, handler: FunctionType): + if not issubclass(model_class, ModelInstance): + model_class = self.environment.model_class_index[model_class] + try: + return model_class( + self.environment, + attributes, + relationship_manager, + parent=self, + ) + except MultipleObjectsReturned: + # pylint: disable=raise-missing-from + raise errors.DesignImplementationError( + f"Expected exactly 1 object for {model_class.__name__}({attributes}) but got more than one" + ) + except ObjectDoesNotExist: + query = ",".join([f'{k}="{v}"' for k, v in attributes.items()]) + # pylint: disable=raise-missing-from + raise errors.DesignImplementationError(f"Could not find {model_class.__name__}: {query}") + + def connect(self, signal: str, handler: FunctionType): """Connect a handler between this model instance (as sender) and signal. Args: signal (Signal): Signal to listen for. handler (FunctionType): Callback function """ - self.signals[signal].connect(handler, self) + self.metadata.connect(signal, handler) + + def _send(self, signal: str): + self.metadata.send(signal) def _load_instance(self): # pylint: disable=too-many-branches - # If the objects is already an existing Nautobot object, just get it. - if self.nautobot_id: - self.created = False - self.instance = self.model_class.objects.get(id=self.nautobot_id) + # Short circuit if the instance was loaded earlier in + # the initialization process + if self.instance is not None: self._initial_state = serialize_object_v2(self.instance) return - query_filter = _map_query_values(self.filter) - if self.action == self.GET: + query_filter = self.metadata.query_filter + field_values = self.metadata.query_filter_values + if self.metadata.action == ModelMetadata.GET: self.instance = self.model_class.objects.get(**query_filter) self._initial_state = serialize_object_v2(self.instance) return - if self.action in [self.UPDATE, self.CREATE_OR_UPDATE]: + if self.metadata.action in [ModelMetadata.UPDATE, ModelMetadata.CREATE_OR_UPDATE]: # perform nested lookups. First collect all the # query params for top-level relationships, then # perform the actual lookup @@ -406,117 +663,92 @@ def _load_instance(self): # pylint: disable=too-many-branches for query_param, value in query_filter.items(): if isinstance(value, Mapping): - rel = getattr(self.model_class, query_param) - queryset = rel.get_queryset() - model = self.create_child(queryset.model, value, relationship_manager=queryset) - if model.action != self.GET: - model.save(value) + rel: Manager = getattr(self.model_class, query_param) + queryset: QuerySet = rel.get_queryset() + + model = self.create_child( + self.environment.model_class_index[queryset.model], + value, + relationship_manager=queryset, + ) + if model.metadata.action != ModelMetadata.GET: + model.save() query_filter[query_param] = model.instance - + field_values[query_param] = model try: self.instance = self.relationship_manager.get(**query_filter) self._initial_state = serialize_object_v2(self.instance) return except ObjectDoesNotExist: - if self.action == "update": + if self.metadata.action == ModelMetadata.UPDATE: # pylint: disable=raise-missing-from raise errors.DesignImplementationError(f"No match with {query_filter}", self.model_class) - self.created = True + self.metadata.created = True # since the object was not found, we need to # put the search criteria back into the attributes # so that they will be set when the object is created - self.attributes.update(query_filter) - elif self.action != "create": - raise errors.DesignImplementationError(f"Unknown database action {self.action}", self.model_class) + self.metadata.attributes.update(field_values) + elif self.metadata.action != ModelMetadata.CREATE: + raise errors.DesignImplementationError(f"Unknown database action {self.metadata.action}", self.model_class) try: self._initial_state = {} - if not self.instance: - self.created = True - self.instance = self.model_class(**self._kwargs) + self.instance = self.model_class(**self.metadata.kwargs) + self.metadata.created = True except TypeError as ex: raise errors.DesignImplementationError(str(ex), self.model_class) - def _update_fields(self): # pylint: disable=too-many-branches - if self.action == self.GET and self.attributes: - raise ValueError("Cannot update fields when using the GET action") + def _update_fields(self): + if self.metadata.action == ModelMetadata.GET: + if self.metadata.attributes: + # TODO: Raise a DesignModelError from here. Currently the DesignModelError doesn't + # include a message. + raise errors.DesignImplementationError( + "Cannot update fields when using the GET action", self.model_class + ) + + for field_name, value in self.metadata.attributes.items(): + if hasattr(self.__class__, field_name): + setattr(self, field_name, value) + elif hasattr(self.instance, field_name): + setattr(self.instance, field_name, value) - for field_name, field in self.instance_fields.items(): - if field_name in self.attributes: - value = self.attributes.pop(field_name) - if field.deferrable: - self.deferred.append(field_name) - self.deferred_attributes[field_name] = self.creator.resolve_values(value) - else: - field.set_value(value) - elif ( - hasattr(self.relationship_manager, "field") - and (isinstance(field, (OneToOneField, ManyToOneField))) - and self.instance_fields[field_name].field == self.relationship_manager.field - ): - field.set_value(self.relationship_manager.instance) - - for key, value in self.attributes.items(): - if hasattr(self.instance, key): - setattr(self.instance, key, value) - - for key, value in self.custom_fields.items(): - self.set_custom_field(key, value) - - def save(self, output_dict): - """Save the model instance to the database.""" - # The reason we call _update_fields at this point is - # that some attributes passed into the constructor - # may not have been saved yet (thus have no ID). By - # deferring the update until just before save, we can - # ensure that parent instances have been saved and - # assigned a primary key - self._update_fields() - self.signals[ModelInstance.PRE_SAVE].send(sender=self, instance=self) + for key, value in self.metadata.custom_fields.items(): + self.set_custom_field(key, value) + + def save(self): + """Save the model instance to the database. + + This method will save the underlying model object to the database and + will send signals (`PRE_SAVE`, `POST_INSTANCE_SAVE` and `POST_SAVE`). The + design journal is updated in this step. + """ + if self.metadata.action == ModelMetadata.GET: + return + + self._send(ModelMetadata.PRE_SAVE) - msg = "Created" if self.instance._state.adding else "Updated" # pylint: disable=protected-access + msg = "Created" if self.metadata.created else "Updated" try: - if self.creator.journal.design_journal: + if self.environment.journal.design_journal: self.instance._current_design = ( # pylint: disable=protected-access - self.creator.journal.design_journal.design_instance + self.environment.journal.design_journal.design_instance ) self.instance.full_clean() - self.instance.save() - if self.parent is None: - self.creator.log_success(message=f"{msg} {self.name} {self.instance}", obj=self.instance) - self.creator.journal.log(self) + self.instance.save(**self.metadata.save_args) + self.environment.journal.log(self) + self.metadata.created = False + if self._parent is None: + self.environment.log_success( + message=f"{msg} {self.model_class.__name__} {self.instance}", obj=self.instance + ) + # Refresh from DB so that we update based on any + # post save signals that may have fired. self.instance.refresh_from_db() except ValidationError as validation_error: raise errors.DesignValidationError(self) from validation_error - for field_name in self.deferred: - items = self.deferred_attributes[field_name] - if isinstance(items, dict): - items = [items] - - for item in items: - field = self.instance_fields[field_name] - if isinstance(item, ModelInstance): - item_dict = output_dict - related_object = item - if item.ext_tag: - # If the item is a Design Builder extension, we get the ID - item_dict[item.ext_tag][NAUTOBOT_ID] = str(item.instance.id) - else: - item_dict = item - relationship_manager = None - if hasattr(self.instance, field_name): - relationship_manager = getattr(self.instance, field_name) - related_object = self.create_child(field.model, item, relationship_manager) - # The item_dict is recursively updated - related_object.save(item_dict) - # BEWARE - # DO NOT REMOVE THE FOLLOWING LINE, IT WILL BREAK THINGS - # THAT ARE UPDATED VIA SIGNALS, ESPECIALLY CABLES! - self.instance.refresh_from_db() - - field.set_value(related_object.instance) - self.signals[ModelInstance.POST_SAVE].send(sender=self, instance=self) - output_dict[NAUTOBOT_ID] = str(self.instance.id) + self._send(ModelMetadata.POST_INSTANCE_SAVE) + self._send(ModelMetadata.POST_SAVE) def set_custom_field(self, field, value): """Sets a value for a custom field.""" @@ -534,36 +766,55 @@ def set_custom_field(self, field, value): "auth", "taggit", "database", - "contenttypes", "sessions", "social_django", ] ) -class Builder(LoggingMixin): - """Iterates through a design and creates and updates the objects defined within.""" +class Environment(LoggingMixin): + """The design builder build environment. + + The build `Environment` contains all of the components needed to implement a design. + This includes custom action tag extensions and an optional `JobResult` for logging. The + build environment also is used by some extensions (such as the `ref` action tag) to store + information about the designs being implemented. + """ model_map: Dict[str, Type[Model]] + model_class_index: Dict[Type, "ModelInstance"] + design_instance: models.DesignInstance def __new__(cls, *args, **kwargs): - """Sets the model_map class attribute when the first Builder initialized.""" + """Sets the model_map class attribute when the first Builder is initialized.""" # only populate the model_map once if not hasattr(cls, "model_map"): cls.model_map = {} + cls.model_class_index = {} for model_class in apps.get_models(): if model_class._meta.app_label in _OBJECT_TYPES_APP_FILTER: continue plural_name = str_to_var_name(model_class._meta.verbose_name_plural) - cls.model_map[plural_name] = model_class + cls.model_map[plural_name] = ModelInstance.factory(model_class) + cls.model_class_index[model_class] = cls.model_map[plural_name] return object.__new__(cls) def __init__( self, job_result: JobResult = None, extensions: List[ext.Extension] = None, journal: models.Journal = None ): - """Constructor for Builder.""" - # builder_output is an auxiliary struct to store the output design with the corresponding Nautobot IDs - self.builder_output = {} + """Create a new build environment for implementing designs. + + Args: + job_result (JobResult, optional): If this environment is being used by + a `DesignJob` then it can log to the `JobResult` for the job. + Defaults to None. + extensions (List[ext.Extension], optional): Any custom extensions to use + when implementing designs. Defaults to None. + + Raises: + errors.DesignImplementationError: If a provided extension is not a subclass + of `ext.Extension`. + """ self.job_result = job_result self.logger = get_logger(__name__, self.job_result) @@ -591,16 +842,18 @@ def __init__( self.extensions["extensions"].append(extn) self.journal = Journal(design_journal=journal) + if journal: + self.design_instance = journal.design_instance def decommission_object(self, object_id, object_name): """This method decommissions an specific object_id from the design instance.""" - self.journal.design_journal.design_instance.decommission(local_logger=self.logger, object_id=object_id) + self.journal.design_journal.design_instance.decommission(object_id, local_logger=self.logger) self.log_success( message=f"Decommissioned {object_name} with ID {object_id} from design instance {self.journal.design_journal.design_instance}." ) def get_extension(self, ext_type: str, tag: str) -> ext.Extension: - """Looks up an extension based on its tag name and returns an instance of that Extension type. + """Look up an extension based on its tag name and return an instance of that Extension type. Args: ext_type (str): the type of the extension, i.e. 'attribute' or 'value' @@ -617,8 +870,7 @@ def get_extension(self, ext_type: str, tag: str) -> ext.Extension: extn["object"] = extn["class"](self) return extn["object"] - @transaction.atomic - def implement_design_changes(self, design: Dict, deprecated_design: Dict, design_file: str, commit: bool = False): + def implement_design(self, design: Dict, commit: bool = False): """Iterates through items in the design and creates them. This process is wrapped in a transaction. If either commit=False (default) or @@ -628,9 +880,7 @@ def implement_design_changes(self, design: Dict, deprecated_design: Dict, design Args: design (Dict): An iterable mapping of design changes. - deprecated_design (Dict): An iterable mapping of deprecated design changes. commit (bool): Whether or not to commit the transaction. Defaults to False. - design_file (str): Name of the design file. Raises: DesignImplementationError: if the model is not in the model map @@ -641,14 +891,16 @@ def implement_design_changes(self, design: Dict, deprecated_design: Dict, design try: for key, value in design.items(): if key in self.model_map and value: - self._create_objects(self.model_map[key], value, key, design_file) + self._create_objects(self.model_map[key], value) elif key not in self.model_map: raise errors.DesignImplementationError(f"Unknown model key {key} in design") - sorted_keys = sorted(deprecated_design, key=custom_delete_order) - for key in sorted_keys: - self._deprecate_objects(deprecated_design[key]) - + # TODO: The way this works now the commit happens on a per-design file + # basis. If a design job has multiple design files and the first + # one completes, but the second one fails, the first will still + # have had commit() called. I think this behavior would be considered + # unexpected. We need to consider removing the commit/rollback functionality + # from `implement_design` and move it to a higher layer, perhaps the `DesignJob` if commit: self.commit() else: @@ -657,8 +909,15 @@ def implement_design_changes(self, design: Dict, deprecated_design: Dict, design self.roll_back() raise ex - def resolve_value(self, value, unwrap_model_instance=False): - """Resolve a value using extensions, if needed.""" + def resolve_value(self, value): + """Resolve a single value using extensions, if needed. + + This method will examine a value to determine if it is an action + tag. If the value is an action tag, then the corresponding extension + is called and the result of the extension execution is returned. + + If the value is not an action tag then the original value is returned. + """ if isinstance(value, str) and value.startswith("!"): (action, arg) = value.lstrip("!").split(":", 1) extn = self.get_extension("value", action) @@ -666,13 +925,22 @@ def resolve_value(self, value, unwrap_model_instance=False): value = extn.value(arg) else: raise errors.DesignImplementationError(f"Unknown attribute extension {value}") - if unwrap_model_instance and isinstance(value, ModelInstance): - value = value.instance return value - def resolve_values(self, value: Union[list, dict, str], unwrap_model_instances: bool = False) -> Any: + def resolve_values(self, value: Union[list, dict, str]) -> Any: """Resolve a value, or values, using extensions. + This method is used to evaluate action tags and call their associated + extensions for a given value tree. The method will iterate the values + of a list or dictionary and determine if each value represents an + action tag. If so, the extension for that tag is called and the original + value is replaced with the result of the extension's execution. + + Lists and dictionaries are copied so that the original values remain un-altered. + + If the value is string and the string is an action tag, that tag is evaluated + and the result is returned. + Args: value (Union[list,dict,str]): The value to attempt to resolve. @@ -680,51 +948,37 @@ def resolve_values(self, value: Union[list, dict, str], unwrap_model_instances: Any: The resolved value. """ if isinstance(value, str): - value = self.resolve_value(value, unwrap_model_instances) + value = self.resolve_value(value) elif isinstance(value, list): # copy the list so we don't change the input value = list(value) for i, item in enumerate(value): - value[i] = self.resolve_value(item, unwrap_model_instances) + value[i] = self.resolve_value(item) elif isinstance(value, dict): # copy the dict so we don't change the input value = dict(value) for k, item in value.items(): - value[k] = self.resolve_value(item, unwrap_model_instances) + value[k] = self.resolve_value(item) return value - def _create_objects(self, model_cls, objects, key, design_file): + def _create_objects(self, model_class: Type[ModelInstance], objects: Union[List[Any], Dict[str, Any]]): if isinstance(objects, dict): - model = ModelInstance(self, model_cls, objects) - model.save(self.builder_output[design_file][key]) - # TODO: I feel this is not used at all - if model.deferred_attributes: - self.builder_output[design_file][key].update(model.deferred_attributes) + model = model_class(self, objects) + model.save() elif isinstance(objects, list): for model_instance in objects: - model_identifier = get_object_identifier(model_instance) - future_object = None - for obj in self.builder_output[design_file][key]: - obj_identifier = get_object_identifier(obj) - if obj_identifier == model_identifier: - future_object = obj - break - - if future_object: - # Recursive function to update the created Nautobot UUIDs into the final design for future reference - model = ModelInstance(self, model_cls, model_instance) - model.save(future_object) - - if model.deferred_attributes: - inject_nautobot_uuids(model.deferred_attributes, future_object) - - def _deprecate_objects(self, objects): - if isinstance(objects, list): - for obj in objects: - self.decommission_object(obj[0], obj[1]) + model = model_class(self, model_instance) + model.save() def commit(self): - """Method to commit all changes to the database.""" + """The `commit` method iterates all extensions and calls their `commit` methods. + + Some extensions need to perform an action after a design has been successfully + implemented. For instance, the config context extension waits until after the + design has been implemented before committing changes to a config context + repository. The `commit` method will find all extensions that include a `commit` + method and will call each of them in order. + """ for extn in self.extensions["extensions"]: if hasattr(extn["object"], "commit"): extn["object"].commit() @@ -738,3 +992,15 @@ def roll_back(self): for extn in self.extensions["extensions"]: if hasattr(extn["object"], "roll_back"): extn["object"].roll_back() + + +def Builder(*args, **kwargs): # pylint:disable=invalid-name + """`Builder` is an alias to the `Environment` class. + + This function is used to provide backwards compatible access to the `Builder` class, + which was renamed to `Environment`. This function will be removed in the future. + """ + from warnings import warn # pylint:disable=import-outside-toplevel + + warn("Builder is now named Environment. Please update your code.") + return Environment(*args, **kwargs) diff --git a/nautobot_design_builder/design_job.py b/nautobot_design_builder/design_job.py index 4b47e5a9..cbdae07f 100644 --- a/nautobot_design_builder/design_job.py +++ b/nautobot_design_builder/design_job.py @@ -1,30 +1,30 @@ """Base Design Job class definition.""" import sys -import copy import traceback from abc import ABC, abstractmethod from os import path -from datetime import datetime from typing import Dict import yaml + from django.db import transaction from django.contrib.contenttypes.models import ContentType +from django.core.files.base import ContentFile +from django.utils import timezone from jinja2 import TemplateError from nautobot.extras.models import Status from nautobot.extras.jobs import Job, StringVar - +from nautobot.extras.models import FileProxy from nautobot_design_builder.errors import DesignImplementationError, DesignModelError from nautobot_design_builder.jinja2 import new_template_environment from nautobot_design_builder.logging import LoggingMixin -from nautobot_design_builder.design import Builder +from nautobot_design_builder.design import Environment from nautobot_design_builder.context import Context from nautobot_design_builder import models from nautobot_design_builder import choices -from nautobot_design_builder.recursive import combine_designs from .util import nautobot_version @@ -52,10 +52,13 @@ def Meta(cls) -> Job.Meta: # pylint: disable=invalid-name def __init__(self, *args, **kwargs): """Initialize the design job.""" # rendered designs - self.builder: Builder = None + self.environment: Environment = None self.designs = {} + # TODO: Remove this when we no longer support Nautobot 1.x self.rendered = None + self.rendered_design = None self.failed = False + self.report = None super().__init__(*args, **kwargs) @@ -63,7 +66,7 @@ def design_model(self): """Get the related Job.""" return models.Design.objects.for_design_job(self.job_result.job_model) - def post_implementation(self, context: Context, builder: Builder): + def post_implementation(self, context: Context, environment: Environment): """Similar to Nautobot job's `post_run` method, but will be called after a design is implemented. Any design job that requires additional work to be completed after the design @@ -73,15 +76,18 @@ def post_implementation(self, context: Context, builder: Builder): Args: context (Context): The render context that was used for rendering the design files. - builder (Builder): The builder object that consumed the rendered design files. This is useful for accessing the design journal. + environment (Environment): The build environment that consumed the rendered design files. This is useful for accessing the design journal. """ def post_run(self): """Method that will run after the main Nautobot job has executed.""" + # TODO: This is not supported in Nautobot 2 and the entire method + # should be removed once we no longer support Nautobot 1. if self.rendered: self.job_result.data["output"] = self.rendered self.job_result.data["designs"] = self.designs + self.job_result.data["report"] = self.report def render(self, context: Context, filename: str) -> str: """High level function to render the Jinja design templates into YAML. @@ -126,12 +132,14 @@ def render_design(self, context, design_file): context (Context object): a tree of variables that can include templates for values design_file (str): Filename of the design file to render. """ + self.rendered_design = design_file self.rendered = self.render(context, design_file) design = yaml.safe_load(self.rendered) self.designs[design_file] = design # no need to save the rendered content if yaml loaded # it okay + self.rendered_design = None self.rendered = None return design @@ -160,47 +168,21 @@ def implement_design(self, context, design_file, commit): """ design = self.render_design(context, design_file) self.log_debug(f"New Design to be implemented: {design}") - deprecated_design = {} - - # The design to apply will take into account the previous journal that keeps track (in the builder_output) - # of the design used (i.e., the YAML) including the Nautobot IDs that will help to reference them - self.builder.builder_output[design_file] = copy.deepcopy(design) - last_journal = ( - self.builder.journal.design_journal.design_instance.journals.filter(active=True) - .exclude(id=self.builder.journal.design_journal.id) - .exclude(builder_output={}) - .order_by("-last_updated") - .first() - ) - if last_journal and last_journal.builder_output: - # The last design output is used as the reference to understand what needs to be changed - # The design output store the whole set of attributes, not only the ones taken into account - # in the implementation - previous_design = last_journal.builder_output[design_file] - self.log_debug(f"Design from previous Journal: {previous_design}") - - for key, new_value in design.items(): - old_value = previous_design[key] - future_value = self.builder.builder_output[design_file][key] - combine_designs(new_value, old_value, future_value, deprecated_design, key) - self.log_debug(f"Design to implement after reduction: {design}") - self.log_debug(f"Design to deprecate after reduction: {deprecated_design}") - - self.builder.implement_design_changes(design, deprecated_design, design_file, commit) + self.environment.implement_design(design, commit) def _setup_journal(self, instance_name: str): try: instance = models.DesignInstance.objects.get(name=instance_name, design=self.design_model()) self.log_info(message=f'Existing design instance of "{instance_name}" was found, re-running design job.') - instance.last_implemented = datetime.now() + instance.last_implemented = timezone.now() except models.DesignInstance.DoesNotExist: self.log_info(message=f'Implementing new design "{instance_name}".') content_type = ContentType.objects.get_for_model(models.DesignInstance) instance = models.DesignInstance( name=instance_name, design=self.design_model(), - last_implemented=datetime.now(), + last_implemented=timezone.now(), status=Status.objects.get(content_types=content_type, name=choices.DesignInstanceStatusChoices.ACTIVE), live_state=Status.objects.get( content_types=content_type, name=choices.DesignInstanceLiveStateChoices.PENDING @@ -208,21 +190,49 @@ def _setup_journal(self, instance_name: str): version=self.design_model().version, ) instance.validated_save() - + previous_journal = instance.journals.order_by("-last_updated").first() journal = models.Journal( design_instance=instance, job_result=self.job_result, ) journal.validated_save() - return journal + return (journal, previous_journal) @staticmethod def validate_data_logic(data): """Method to validate the input data logic that is already valid as a form by the `validate_data` method.""" + def run(self, **kwargs): # pylint: disable=arguments-differ + """Render the design and implement it within a build Environment object.""" + try: + return self._run_in_transaction(**kwargs) + finally: + if self.rendered: + rendered_design = path.basename(self.rendered_design) + rendered_design, _ = path.splitext(rendered_design) + if not rendered_design.endswith(".yaml") and not rendered_design.endswith(".yml"): + rendered_design = f"{rendered_design}.yaml" + self.save_design_file(rendered_design, self.rendered) + for design_file, design in self.designs.items(): + output_file = path.basename(design_file) + # this should remove the .j2 + output_file, _ = path.splitext(output_file) + if not output_file.endswith(".yaml") and not output_file.endswith(".yml"): + output_file = f"{output_file}.yaml" + self.save_design_file(output_file, yaml.safe_dump(design)) + @transaction.atomic - def run(self, **kwargs): # pylint: disable=arguments-differ,too-many-branches,too-many-statements - """Render the design and implement it with a Builder object.""" + def _run_in_transaction(self, **kwargs): # pylint: disable=too-many-branches, too-many-statements + """Render the design and implement it within a build Environment object. + + This version of `run` is wrapped in a transaction and will roll back database changes + on error. In general, this method should only be called by the `run` method. + """ + self.log_info(message=f"Building {getattr(self.Meta, 'name')}") + extensions = getattr(self.Meta, "extensions", []) + + design_files = None + if nautobot_version < "2.0.0": commit = kwargs["commit"] data = kwargs["data"] @@ -237,10 +247,10 @@ def run(self, **kwargs): # pylint: disable=arguments-differ,too-many-branches,t else: self.job_result.job_kwargs = self.serialize_data(data) - journal = self._setup_journal(data.pop("instance_name")) + journal, previous_journal = self._setup_journal(data.pop("instance_name")) self.log_info(message=f"Building {getattr(self.Meta, 'name')}") extensions = getattr(self.Meta, "extensions", []) - self.builder = Builder( + self.environment = Environment( job_result=self.job_result, extensions=extensions, journal=journal, @@ -268,12 +278,17 @@ def run(self, **kwargs): # pylint: disable=arguments-differ,too-many-branches,t try: for design_file in design_files: self.implement_design(context, design_file, commit) - if commit: - self.post_implementation(context, self.builder) + if previous_journal: + deleted_object_ids = previous_journal - journal + if deleted_object_ids: + self.log_info(f"Decommissioning {deleted_object_ids}") + journal.design_instance.decommission(*deleted_object_ids, local_logger=self.environment.logger) + + if commit: + self.post_implementation(context, self.environment) # The Journal stores the design (with Nautobot identifiers from post_implementation) # for future operations (e.g., updates) - journal.builder_output = self.builder.builder_output journal.design_instance.status = Status.objects.get( content_types=ContentType.objects.get_for_model(models.DesignInstance), name=choices.DesignInstanceStatusChoices.ACTIVE, @@ -285,8 +300,10 @@ def run(self, **kwargs): # pylint: disable=arguments-differ,too-many-branches,t "design_instance": journal.design_instance.pk, } if hasattr(self.Meta, "report"): - self.job_result.data["report"] = self.render_report(context, self.builder.journal) - self.log_success(message=self.job_result.data["report"]) + self.report = self.render_report(context, self.environment.journal) + self.log_success(message=self.report) + if nautobot_version >= "2.0": + self.save_design_file("report.md", self.report) else: transaction.savepoint_rollback(sid) self.log_info( @@ -303,3 +320,21 @@ def run(self, **kwargs): # pylint: disable=arguments-differ,too-many-branches,t transaction.savepoint_rollback(sid) self.failed = True raise ex + + def save_design_file(self, filename, content): + """Save some content to a job file. + + This is only supported on Nautobot 2.0 and greater. + + Args: + filename (str): The name of the file to save. + content (str): The content to save to the file. + """ + if nautobot_version < "2.0": + return + + FileProxy.objects.create( + name=filename, + job_result=self.job_result, + file=ContentFile(content.encode("utf-8"), name=filename), + ) diff --git a/nautobot_design_builder/errors.py b/nautobot_design_builder/errors.py index a4f91c5d..678324e0 100644 --- a/nautobot_design_builder/errors.py +++ b/nautobot_design_builder/errors.py @@ -80,7 +80,7 @@ def _model_str(model): # hasn't been set or something. Whatever it is commonly is # the cause of the original exception, we don't want to # cause *another* exception because of that. - instance_str = model.__class__.__name__ + instance_str = "unknown" model_str = model_class._meta.verbose_name.capitalize() if instance_str: model_str = f"{model_str} {instance_str}" @@ -112,8 +112,8 @@ def path_str(self): model = self.model while model is not None: path_msg.insert(0, DesignModelError._model_str(model)) - if not isclass(model) and hasattr(model, "parent"): - model = model.parent + if not isclass(model) and hasattr(model, "_parent"): + model = model._parent # pylint:disable=protected-access elif self.parent: model = self.parent self.parent = None @@ -193,7 +193,9 @@ def __str__(self) -> str: elif self.query_filter: msg.append(DesignModelError._object_to_markdown(self.query_filter, indentation=f"{indentation} ")) else: - msg.append(DesignModelError._object_to_markdown(self.model.filter, indentation=f"{indentation} ")) + msg.append( + DesignModelError._object_to_markdown(self.model.metadata.filter, indentation=f"{indentation} ") + ) return "\n".join(msg) diff --git a/nautobot_design_builder/ext.py b/nautobot_design_builder/ext.py index fcc790d9..5e06b51e 100644 --- a/nautobot_design_builder/ext.py +++ b/nautobot_design_builder/ext.py @@ -15,7 +15,7 @@ from nautobot_design_builder.git import GitRepo if TYPE_CHECKING: - from design import ModelInstance, Builder + from design import ModelInstance, Environment def is_extension(cls): @@ -65,27 +65,29 @@ class Extension(ABC): tag matching `tag_name` or `value_name` is encountered. Args: - builder (Builder): The object creator that is implementing the + environment (Environment): The object creator that is implementing the current design. """ + environment: "Environment" + @property @abstractmethod def tag(self): """All Extensions must specify their tag name. - The `tag` method indicates to the Builder what the + The `tag` method indicates to the Environment what the tag name is for this extensions. For instance, a `tag` of `ref` will match `!ref` in the design. """ - def __init__(self, builder: "Builder"): # noqa: D107 - self.builder = builder + def __init__(self, environment: "Environment"): # noqa: D107 + self.environment = environment def commit(self) -> None: """Optional method that is called once a design has been implemented and committed to the database. - Note: Commit is called once for each time Builder.implement_design is called. For a design job with + Note: Commit is called once for each time Environment.implement_design is called. For a design job with multiple design files, commit will be called once for each design file. It is up to the extension to track internal state so that multiple calls to `commit` don't introduce an inconsistency. """ @@ -98,12 +100,18 @@ class AttributeExtension(Extension, ABC): """An `AttributeExtension` will be evaluated when the design key matches the `tag`.""" @abstractmethod - def attribute(self, value: Any, model_instance: "ModelInstance") -> None: + def attribute(self, *args: List[Any], value: Any = None, model_instance: "ModelInstance" = None) -> None: """This method is called when the `attribute_tag` is encountered. + Note: The method signature must match the above for the extension to work. The + extension name is parsed by splitting on `:` symbols and the result is passed as the + varargs. For instance, if the attribute tag is `mytagg` and it is called with `!mytagg:arg1`: {} then + `*args` will be ['arg1'] and `value` will be the empty dictionary. + Args: + *args (List[Any]): Any additional arguments following the tag name. These are `:` delimited. value (Any): The value of the data structure at this key's point in the design YAML. This could be a scalar, a dict or a list. - model_instance (CreatorObject): Object is the CreatorObject that would ultimately contain the values. + model_instance (ModelInstance): Object is the ModelInstance that would ultimately contain the values. """ @@ -139,20 +147,22 @@ class ReferenceExtension(AttributeExtension, ValueExtension): stored creator object. Args: - builder (Builder): The object creator that is implementing the + environment (Environment): The object creator that is implementing the current design. """ tag = "ref" - def __init__(self, builder: "Builder"): # noqa: D107 - super().__init__(builder) + def __init__(self, environment: "Environment"): # noqa: D107 + super().__init__(environment) self._env = {} - def attribute(self, value, model_instance): + def attribute(self, *args: List[Any], value, model_instance): """This method is called when the `!ref` tag is encountered. Args: + *args (List[Any]): Any additional arguments following the tag name. These are `:` delimited. + value (Any): Value should be a string name (the reference) to refer to the object model_instance (CreatorObject): The object that will be later referenced @@ -196,6 +206,8 @@ def value(self, key) -> "ModelInstance": if model_instance.instance and not model_instance.instance._state.adding: # pylint: disable=protected-access model_instance.instance.refresh_from_db() if attribute: + # TODO: I think the result of the reduce operation needs to (potentially) + # be wrapped up in a ModelInstance object return reduce(getattr, [model_instance.instance, *attribute.split(".")]) return model_instance @@ -204,7 +216,7 @@ class GitContextExtension(AttributeExtension): """Provides the "!git_context" attribute extension that will save content to a git repo. Args: - builder (Builder): The object creator that is implementing the + environment (Environment): The object creator that is implementing the current design. Example: @@ -225,10 +237,10 @@ class GitContextExtension(AttributeExtension): tag = "git_context" - def __init__(self, builder: "Builder"): # noqa: D107 - super().__init__(builder) + def __init__(self, environment: "Environment"): # noqa: D107 + super().__init__(environment) slug = NautobotDesignBuilderConfig.context_repository - self.context_repo = GitRepo(slug, builder.job_result) + self.context_repo = GitRepo(slug, environment.job_result) self._env = {} self._reset() @@ -239,7 +251,7 @@ def _reset(self): "directories": [], } - def attribute(self, value, model_instance): + def attribute(self, *args, value=None, model_instance: "ModelInstance" = None): """Provide the attribute tag functionality for git_context. Args: diff --git a/nautobot_design_builder/fields.py b/nautobot_design_builder/fields.py index d44288bb..7ddfec74 100644 --- a/nautobot_design_builder/fields.py +++ b/nautobot_design_builder/fields.py @@ -1,227 +1,343 @@ -"""Model fields.""" +"""This module includes the proxy model field descriptors. + +The way design builder handles attribute assignment is with descriptors. This +allows assignment of values directly to the proxy model and the descriptors +handle any necessary deferrals. + +For instance, `ForeignKey` relationships require that foreign key object be +present in the database before the receiving object (object with the +`ForeignKey` field) can be saved. When design builder encounters this situation +there are some cases (for example, setting IP addresses on interfaces) where +assignment must be deferred in order to guarantee the parent object is present +in the database prior to save. + +Another example is the reverse side of foreign key relationships. Consider a `Device` which +has many `Interface` objects. The device foreign key is defined on the `Interface` object, +but we would typically model this in design builder as such: + +```yaml +devices: + # additional attributes such as device type, role, location, etc + # are not illustrated here. + - name: "My Device" + interfaces: + - name: "Ethernet1" + # type, status, etc + - name: "Ethernet2" + # type, status, etc +``` + +In order to save these objects to the database, the device must be saved first, and +then each interface's device foreign key is set and saved. + +Since design builder generally processes things in a depth first order, the natural sequence +is for the interfaces (in the above example) to be created first. Therefore, the +`ManyToOneRelField` will handle creating an instance of Interface but deferring database +save until after the device is saved. + +See also: https://docs.python.org/3/howto/descriptor.html +""" from abc import ABC, abstractmethod -from typing import Mapping, Type +from typing import Any, Mapping, Type, TYPE_CHECKING -from django.db.models import Model -from django.db.models.fields import Field as DjangoField +from django.db import models as django_models from django.contrib.contenttypes.models import ContentType -from django.contrib.contenttypes.fields import GenericRelation, GenericForeignKey -from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist +from django.contrib.contenttypes import fields as ct_fields from taggit.managers import TaggableManager -from nautobot.extras.choices import RelationshipTypeChoices +from nautobot.core.graphql.utils import str_to_var_name from nautobot.extras.models import Relationship, RelationshipAssociation from nautobot_design_builder.errors import DesignImplementationError +from nautobot_design_builder.debug import debug_set +from nautobot_design_builder.util import nautobot_version + +if TYPE_CHECKING: + from .design import ModelInstance + from django.db.models.manager import Manager class ModelField(ABC): - """This represents any type of field (attribute or relationship) on a Nautobot model.""" + """This represents any type of field (attribute or relationship) on a Nautobot model. + + The design builder fields are descriptors that are used to build the + correct relationship hierarchy of django models based on the design input. + The fields are used to sequence saves and value assignments in the correct + order. + """ + + field_name: str + + def __set_name__(self, owner, name): # noqa: D105 + self.field_name = name + + def __get__(self, obj, objtype=None) -> Any: + """Retrieve the field value. + + In the event `obj` is None (as in getting the attribute from the class) then + get the descriptor itself. + + Args: + obj (ModelInstance): The model to retrieve the field value + objtype (type, optional): The owning class of the descriptor. Defaults to None. + + Returns: + Any: Either the descriptor instance or the field value. + """ + if obj is None or obj.instance is None: + return self + return getattr(obj.instance, self.field_name) @abstractmethod - def set_value(self, value): + def __set__(self, obj: "ModelInstance", value): """Method used to set the value of the field. Args: + obj: (ModelInstance): The model to update. value (Any): Value that should be set on the model field. """ - @property - @abstractmethod - def deferrable(self): - """Determine whether the saving of this field should be deferred.""" +class BaseModelField(ModelField): # pylint:disable=too-few-public-methods + """`BaseModelField` is backed by django.db.models.fields.Field. -class BaseModelField(ModelField): - """BaseModelFields are backed by django.db.models.fields.Field.""" + `BaseModelField` is used as the base class for any design builder field + which proxies to a Django model field. Not all design builder fields are + for actual model database fields. For instance, custom relationships are + something design builder handles with a field descriptor, but they + are not backed by django `Field` descriptors. + """ - field: DjangoField - model: Type[object] + field: django_models.Field + related_model: Type[object] - def __init__(self, model_instance, field: DjangoField): - """Create a base model field. + def __init__(self, field: django_models.Field): + """Initialize a field proxy. Args: - model_instance (ModelInstance): Model instance which this field belongs to. - field (DjangoField): Database field to be managed. + field (django_models.Field): The field that should be proxied on the django model. """ - self.instance = model_instance self.field = field - self.model = field.related_model + self.field_name = field.name + self.related_model = field.related_model - @property - def deferrable(self): # noqa:D102 - return False +class SimpleField(BaseModelField): # pylint:disable=too-few-public-methods + """A field that accepts a scalar value. -class SimpleField(BaseModelField): - """A field that accepts a scalar value.""" + `SimpleField` will immediately set scalar values on the underlying field. This + includes assignment to fields such as `CharField` or `IntegerField`. When + this descriptor is called, the assigned value is immediately passed to the + underlying model object. + """ - def set_value(self, value): # noqa:D102 - setattr(self.instance.instance, self.field.name, value) + @debug_set + def __set__(self, obj: "ModelInstance", value): # noqa: D105 + setattr(obj.instance, self.field_name, value) -class RelationshipField(BaseModelField): - """Field that represents a relationship to another model.""" +class RelationshipFieldMixin: # pylint:disable=too-few-public-methods + """Field mixin for relationships to other models. - @property - def deferrable(self): # noqa:D102 - return True + `RelationshipField` instances represent fields that have some sort of relationship + to other objects. These include `ForeignKey` and `ManyToMany`. + Relationship fields also include the reverse side of fields or even custom relationships. + """ + def _get_instance(self, obj: "ModelInstance", value: Any, relationship_manager: "Manager" = None): + """Helper function to create a new child model from a value. -class OneToOneField(RelationshipField): - """One to one relationship field.""" + If the passed-in value is a dictionary, this method assumes that the dictionary + represents a new design builder object which will belong to a parent. In this + case a new child is created from the parent object. - def set_value(self, value): # noqa:D102 - setattr(self.instance.instance, self.field.name, value) - self.instance.instance.save() + If the value is not a dictionary, it is simply returned. + Args: + obj (ModelInstance): The parent object that the value will be ultimately assigned. + value (Any): The value being assigned to the parent object. + relationship_manager (Manager, optional): This argument can be used to restrict the + child object lookups to a subset. For instance, the `interfaces` manager on a `Device` + instance will restrict queries interfaces where their foreign key is set to the device. + Defaults to None. + + Returns: + ModelInstance: Either a newly created `ModelInstance` or the original value. + """ + if isinstance(value, Mapping): + value = obj.create_child(self.related_model, value, relationship_manager) + return value -class OneToManyField(RelationshipField): - """One to many relationship field.""" - def set_value(self, value): # noqa:D102 - getattr(self.instance.instance, self.field.name).add(value, bulk=False) - self.instance.instance.validated_save() - value.validated_save() +class ForeignKeyField(BaseModelField, RelationshipFieldMixin): # pylint:disable=too-few-public-methods + """`ForeignKey` relationship.""" + + @debug_set + def __set__(self, obj: "ModelInstance", value): # noqa: D105 + deferred = getattr(value, "deferred", False) or (isinstance(value, Mapping) and value.get("deferred", False)) + + def setter(): + model_instance = self._get_instance(obj, value) + if model_instance.metadata.created: + model_instance.save() + else: + model_instance.environment.journal.log(model_instance) + setattr(obj.instance, self.field_name, model_instance.instance) + if deferred: + obj.instance.save(update_fields=[self.field_name]) + + if deferred: + obj.connect("POST_INSTANCE_SAVE", setter) + else: + setter() -class ManyToManyField(RelationshipField): +class ManyToOneRelField(BaseModelField, RelationshipFieldMixin): # pylint:disable=too-few-public-methods + """The reverse side of a `ForeignKey` relationship.""" + + @debug_set + def __set__(self, obj: "ModelInstance", values): # noqa:D105 + if not isinstance(values, list): + raise DesignImplementationError("Many-to-one fields must be a list", obj) + + def setter(): + for value in values: + value = self._get_instance(obj, value, getattr(obj, self.field_name)) + setattr(value.instance, self.field.field.name, obj.instance) + value.save() + + obj.connect("POST_INSTANCE_SAVE", setter) + + +class ManyToManyField(BaseModelField, RelationshipFieldMixin): # pylint:disable=too-few-public-methods """Many to many relationship field.""" - def __init__(self, model_instance, field: DjangoField): # noqa:D102 - super().__init__(model_instance, field) + def __init__(self, field: django_models.Field): # noqa:D102 + super().__init__(field) if hasattr(field.remote_field, "through"): through = field.remote_field.through if not through._meta.auto_created: - self.model = through + self.related_model = through - def set_value(self, value): # noqa:D102 - getattr(self.instance.instance, self.field.name).add(value) + @debug_set + def __set__(self, obj: "ModelInstance", values): # noqa:D105 + def setter(): + items = [] + for value in values: + value = self._get_instance(obj, value, getattr(obj.instance, self.field_name)) + if value.metadata.created: + value.save() + else: + value.environment.journal.log(value) + items.append(value.instance) + getattr(obj.instance, self.field_name).add(*items) + obj.connect("POST_INSTANCE_SAVE", setter) -class GenericRelationField(RelationshipField): - """Generic relationship field.""" - - def set_value(self, value): # noqa:D102 - getattr(self.instance.instance, self.field.name).add(value) +class GenericRelationField(BaseModelField, RelationshipFieldMixin): # pylint:disable=too-few-public-methods + """Generic relationship field.""" -class GenericForeignKeyField(RelationshipField): + @debug_set + def __set__(self, obj: "ModelInstance", values): # noqa:D105 + if not isinstance(values, list): + values = [values] + items = [] + for value in values: + value = self._get_instance(obj, value) + if value.metadata.created: + value.save() + else: + value.environment.journal.log(value) + items.append(value.instance) + getattr(obj.instance, self.field_name).add(*items) + + +class GenericForeignKeyField(BaseModelField, RelationshipFieldMixin): # pylint:disable=too-few-public-methods """Generic foreign key field.""" - @property - def deferrable(self): # noqa:D102 - return False - - def set_value(self, value): # noqa:D102 + @debug_set + def __set__(self, obj: "ModelInstance", value): # noqa:D105 fk_field = self.field.fk_field ct_field = self.field.ct_field - if hasattr(value, "instance"): - value = value.instance - setattr(self.instance.instance, fk_field, value.pk) - setattr(self.instance.instance, ct_field, ContentType.objects.get_for_model(value)) + setattr(obj.instance, fk_field, value.instance.pk) + setattr(obj.instance, ct_field, ContentType.objects.get_for_model(value.instance)) -class TagField(ManyToManyField): +class TagField(ManyToManyField): # pylint:disable=too-few-public-methods """Taggit field.""" - def __init__(self, model_instance, field: DjangoField): # noqa:D102 - super().__init__(model_instance, field) - self.model = field.remote_field.model + def __init__(self, field: django_models.Field): # noqa:D102 + super().__init__(field) + self.related_model = field.remote_field.model -class ManyToOneField(RelationshipField): - """Many to one relationship field.""" +class GenericRelField(BaseModelField, RelationshipFieldMixin): # pylint:disable=too-few-public-methods + """Field used as part of content-types generic relation.""" - @property - def deferrable(self): # noqa:D102 - return False + @debug_set + def __set__(self, obj: "ModelInstance", value): # noqa:D105 + setattr(obj.instance, self.field.attname, self._get_instance(obj, value)) - def set_value(self, value): # noqa:D102 - if isinstance(value, Mapping): - try: - includes_action = False - for key in value.keys(): - if key.startswith("!"): - includes_action = True - if not includes_action: - value = {f"!get:{key}": value for key, value in value.items()} - value = self.instance.create_child(self.model, value) - if value.created: - # TODO: Here, we may need to store the uuid in the output? - # Not found yet the need for. - value.save({}) - value = value.instance.pk - except MultipleObjectsReturned: - # pylint: disable=raise-missing-from - raise DesignImplementationError( - f"Expected exactly 1 object for {self.model.__name__}({value}) but got more than one" - ) - except ObjectDoesNotExist: - query = ",".join([f'{k}="{v}"' for k, v in value.items()]) - # pylint: disable=raise-missing-from - raise DesignImplementationError(f"Could not find {self.model.__name__}: {query}") - elif hasattr(value, "instance"): - value = value.instance.pk - elif isinstance(value, Model): - value = value.pk - elif value is not None: - raise DesignImplementationError( - f"Expecting input field '{self.field.name}' to be a mapping or reference, got {type(value)}: {value}" - ) - setattr(self.instance.instance, self.field.attname, value) - - -class CustomRelationshipField(ModelField): # pylint: disable=too-few-public-methods + +class CustomRelationshipField(ModelField, RelationshipFieldMixin): # pylint: disable=too-few-public-methods """This class models a Nautobot custom relationship.""" - def __init__(self, model_instance: Model, relationship: Relationship): + def __init__(self, model_class, relationship: Relationship): """Create a new custom relationship field. Args: + model_class (Model): Model class for this relationship. relationship (Relationship): The Nautobot custom relationship backing this field. - model_class (Model): Model class for the remote end of this relationship. - model_instance (ModelInstance): Object being updated to include this field. """ self.relationship = relationship - self.instance = model_instance - self.one_to_one = relationship.type == RelationshipTypeChoices.TYPE_ONE_TO_ONE - self.many_to_one = relationship.type == RelationshipTypeChoices.TYPE_ONE_TO_MANY - self.one_to_many = self.many_to_one - self.many_to_many = relationship.type == RelationshipTypeChoices.TYPE_MANY_TO_MANY - if self.relationship.source_type == ContentType.objects.get_for_model(model_instance.model_class): - self.model = relationship.destination_type.model_class() + field_name = "" + if self.relationship.source_type == ContentType.objects.get_for_model(model_class.model_class): + self.related_model = relationship.destination_type.model_class() + field_name = str(self.relationship.get_label("source")) else: - self.model = relationship.source_type.model_class() - - @property - def deferrable(self): # noqa:D102 - return True + self.related_model = relationship.source_type.model_class() + field_name = str(self.relationship.get_label("destination")) + self.__set_name__(model_class, str_to_var_name(field_name)) + if nautobot_version < "2.0.0": + self.key_name = self.relationship.slug + else: + self.key_name = self.relationship.key - def set_value(self, value: Model): # noqa:D102 + @debug_set + def __set__(self, obj: "ModelInstance", values): # noqa:D105 """Add an association between the created object and the given value. Args: - value (Model): The related object to add. + values (Model): The related objects to add. """ - source = self.instance.instance - destination = value - if self.relationship.source_type == ContentType.objects.get_for_model(value): - source = value - destination = self.instance.instance - - source_type = ContentType.objects.get_for_model(source) - destination_type = ContentType.objects.get_for_model(destination) - RelationshipAssociation.objects.update_or_create( - relationship=self.relationship, - source_id=source.id, - source_type=source_type, - destination_id=destination.id, - destination_type=destination_type, - ) + + def setter(): + for value in values: + value = self._get_instance(obj, value) + if value.metadata.created: + value.save() + else: + value.environment.journal.log(value) + + source = obj.instance + destination = value.instance + if self.relationship.source_type == ContentType.objects.get_for_model(destination): + source, destination = destination, source + + source_type = ContentType.objects.get_for_model(source) + destination_type = ContentType.objects.get_for_model(destination) + RelationshipAssociation.objects.update_or_create( + relationship=self.relationship, + source_id=source.id, + source_type=source_type, + destination_id=destination.id, + destination_type=destination_type, + ) + + obj.connect("POST_INSTANCE_SAVE", setter) def field_factory(arg1, arg2) -> ModelField: @@ -231,21 +347,21 @@ def field_factory(arg1, arg2) -> ModelField: field = None if not arg2.is_relation: - field = SimpleField(arg1, arg2) - elif isinstance(arg2, GenericRelation): - field = GenericRelationField(arg1, arg2) - elif isinstance(arg2, GenericForeignKey): - field = GenericForeignKeyField(arg1, arg2) + field = SimpleField(arg2) + elif isinstance(arg2, ct_fields.GenericRelation): + field = GenericRelationField(arg2) + elif isinstance(arg2, ct_fields.GenericRel): + field = GenericRelField(arg2) + elif isinstance(arg2, ct_fields.GenericForeignKey): + field = GenericForeignKeyField(arg2) elif isinstance(arg2, TaggableManager): - field = TagField(arg1, arg2) - elif arg2.one_to_one: - field = OneToOneField(arg1, arg2) - elif arg2.one_to_many: - field = OneToManyField(arg1, arg2) - elif arg2.many_to_many: - field = ManyToManyField(arg1, arg2) - elif arg2.many_to_one: - field = ManyToOneField(arg1, arg2) + field = TagField(arg2) + elif isinstance(arg2, django_models.ForeignKey): + field = ForeignKeyField(arg2) + elif isinstance(arg2, django_models.ManyToOneRel): + field = ManyToOneRelField(arg2) + elif isinstance(arg2, (django_models.ManyToManyField, django_models.ManyToManyRel)): + field = ManyToManyField(arg2) else: raise DesignImplementationError(f"Cannot manufacture field for {type(arg2)}, {arg2} {arg2.is_relation}") return field diff --git a/nautobot_design_builder/management/commands/build_design.py b/nautobot_design_builder/management/commands/build_design.py index 296f07ca..23b8e62b 100644 --- a/nautobot_design_builder/management/commands/build_design.py +++ b/nautobot_design_builder/management/commands/build_design.py @@ -5,7 +5,7 @@ from django.core.management.base import BaseCommand, CommandError -from ...design import Builder +from ...design import Environment def _load_file(filename): @@ -29,8 +29,8 @@ def add_arguments(self, parser): def handle(self, *args, **options): """Handle the execution of the command.""" - builder = Builder() + builder = Environment() for filename in options["design_file"]: self.stdout.write(f"Building design from {filename}") design = _load_file(filename) - builder.implement_design_changes(design, {}, filename, commit=options["commit"]) + builder.implement_design(design, commit=options["commit"]) diff --git a/nautobot_design_builder/migrations/0006_alter_designinstance_status.py b/nautobot_design_builder/migrations/0006_alter_designinstance_status.py new file mode 100644 index 00000000..c766becb --- /dev/null +++ b/nautobot_design_builder/migrations/0006_alter_designinstance_status.py @@ -0,0 +1,26 @@ +# Generated by Django 3.2.20 on 2024-04-15 18:10 + +from django.db import migrations +import django.db.models.deletion +import nautobot.extras.models.statuses + + +class Migration(migrations.Migration): + + dependencies = [ + ("extras", "0058_jobresult_add_time_status_idxs"), + ("nautobot_design_builder", "0005_auto_20240415_0455"), + ] + + operations = [ + migrations.AlterField( + model_name="designinstance", + name="status", + field=nautobot.extras.models.statuses.StatusField( + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="design_instance_statuses", + to="extras.status", + ), + ), + ] diff --git a/nautobot_design_builder/migrations/0007_auto_20240430_1235.py b/nautobot_design_builder/migrations/0007_auto_20240430_1235.py new file mode 100644 index 00000000..341dfa72 --- /dev/null +++ b/nautobot_design_builder/migrations/0007_auto_20240430_1235.py @@ -0,0 +1,27 @@ +# Generated by Django 3.2.20 on 2024-04-30 12:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("nautobot_design_builder", "0006_alter_designinstance_status"), + ] + + operations = [ + migrations.AlterModelOptions( + name="journal", + options={"ordering": ["-last_updated"]}, + ), + migrations.RemoveField( + model_name="journal", + name="builder_output", + ), + migrations.AddField( + model_name="journalentry", + name="index", + field=models.IntegerField(default=0), + preserve_default=False, + ), + ] diff --git a/nautobot_design_builder/models.py b/nautobot_design_builder/models.py index 8ce2e6ea..ad84b146 100644 --- a/nautobot_design_builder/models.py +++ b/nautobot_design_builder/models.py @@ -11,10 +11,9 @@ from nautobot.apps.models import PrimaryModel, BaseModel from nautobot.core.celery import NautobotKombuJSONEncoder -from nautobot.extras.models import Job as JobModel, JobResult, Status, StatusModel, StatusField, Tag +from nautobot.extras.models import Job as JobModel, JobResult, Status, StatusField from nautobot.extras.utils import extras_features from nautobot.utilities.querysets import RestrictedQuerySet -from nautobot.utilities.choices import ColorChoices from .util import nautobot_version, get_created_and_last_updated_usernames_for_model from . import choices @@ -171,7 +170,7 @@ def get_by_natural_key(self, design_name, instance_name): @extras_features("statuses") -class DesignInstance(PrimaryModel, StatusModel): +class DesignInstance(PrimaryModel): """Design instance represents the result of executing a design. Design instance represents the collection of Nautobot objects @@ -184,6 +183,7 @@ class DesignInstance(PrimaryModel, StatusModel): post_decommission = Signal() + status = StatusField(blank=False, null=False, on_delete=models.PROTECT, related_name="design_instance_statuses") design = models.ForeignKey(to=Design, on_delete=models.PROTECT, editable=False, related_name="instances") name = models.CharField(max_length=DESIGN_NAME_MAX_LENGTH) first_implemented = models.DateTimeField(blank=True, null=True, auto_now_add=True) @@ -222,21 +222,21 @@ def __str__(self): """Stringify instance.""" return f"{self.design.name} - {self.name}" - def decommission(self, local_logger=logger, object_id=None): + def decommission(self, *object_ids, local_logger=logger): """Decommission a design instance. This will reverse the journal entries for the design instance and reset associated objects to their pre-design state. """ - if not object_id: + if not object_ids: local_logger.info("Decommissioning design", extra={"obj": self}) self.__class__.pre_decommission.send(self.__class__, design_instance=self) # Iterate the journals in reverse order (most recent first) and # revert each journal. for journal in self.journals.filter(active=True).order_by("-last_updated"): - journal.revert(local_logger=local_logger, object_id=object_id) + journal.revert(*object_ids, local_logger=local_logger) - if not object_id: + if not object_ids: content_type = ContentType.objects.get_for_model(DesignInstance) self.status = Status.objects.get( content_types=content_type, name=choices.DesignInstanceStatusChoices.DECOMMISSIONED @@ -253,9 +253,26 @@ def delete(self, *args, **kwargs): raise ValidationError("A Design Instance can only be delete if it's Decommissioned and not Deployed.") return super().delete(*args, **kwargs) + def get_design_objects(self, model): + """Get all of the design objects for this design instance that are of `model` type. + + For instance, do get all of the `dcim.Interface` objects for this design instance call + `design_instance.get_design_objects(Interface)`. + + Args: + model (type): The model type to match. + + Returns: + Queryset of matching objects. + """ + entries = JournalEntry.objects.filter_by_instance(self, model=model) + return model.objects.filter(pk__in=entries.values_list("pk", flat=True)) + @property def created_by(self): """Get the username of the user who created the object.""" + # TODO: if we just add a "created_by" and "last_updated_by" field, doesn't that + # reduce the complexity of code that we have in the util module? created_by, _ = get_created_and_last_updated_usernames_for_model(self) return created_by @@ -287,9 +304,13 @@ class Journal(PrimaryModel): related_name="journals", ) job_result = models.ForeignKey(to=JobResult, on_delete=models.PROTECT, editable=False) - builder_output = models.JSONField(encoder=NautobotKombuJSONEncoder, editable=False, null=True, blank=True) active = models.BooleanField(editable=False, default=True) + class Meta: + """Set the default query ordering.""" + + ordering = ["-last_updated"] + def get_absolute_url(self): """Return detail view for design instances.""" return reverse("plugins:nautobot_design_builder:journal", args=[self.pk]) @@ -310,6 +331,18 @@ def user_input(self): job = self.design_instance.design.job return job.job_class.deserialize_data(user_input) + def _next_index(self): + # The hokey getting/setting here is to make pylint happy + # and not complain about `no-member` + index = getattr(self, "_index", None) + if index is None: + index = self.entries.aggregate(index=models.Max("index"))["index"] + if index is None: + index = -1 + index += 1 + setattr(self, "_index", index) + return index + def log(self, model_instance): """Log changes to a model instance. @@ -325,21 +358,6 @@ def log(self, model_instance): instance = model_instance.instance content_type = ContentType.objects.get_for_model(instance) - if model_instance.created: - try: - tag_design_builder, _ = Tag.objects.get_or_create( - name=f"Managed by {self.design_instance}", - defaults={ - "description": f"Managed by Design Builder: {self.design_instance}", - "color": ColorChoices.COLOR_LIGHT_GREEN, - }, - ) - instance.tags.add(tag_design_builder) - instance.save() - except AttributeError: - # This happens when the instance doesn't support Tags, for example Region - pass - try: entry = self.entries.get( _design_object_type=content_type, @@ -354,11 +372,12 @@ def log(self, model_instance): _design_object_type=content_type, _design_object_id=instance.id, changes=model_instance.get_changes(), - full_control=model_instance.created, + full_control=model_instance.metadata.created, + index=self._next_index(), ) return entry - def revert(self, local_logger: logging.Logger = logger, object_id=None): + def revert(self, *object_ids, local_logger: logging.Logger = logger): """Revert the changes represented in this Journal. Raises: @@ -370,22 +389,49 @@ def revert(self, local_logger: logging.Logger = logger, object_id=None): # Without a design object we cannot have changes, right? I suppose if the # object has been deleted since the change was made then it wouldn't exist, # but I think we need to discuss the implications of this further. - if not object_id: + entries = self.entries.order_by("-index").exclude(_design_object_id=None).exclude(active=False) + if not object_ids: local_logger.info("Reverting journal", extra={"obj": self}) - for journal_entry in ( - self.entries.exclude(_design_object_id=None).exclude(active=False).order_by("-last_updated") - ): + else: + entries = entries.filter(_design_object_id__in=object_ids) + + for journal_entry in entries: try: - journal_entry.revert(local_logger=local_logger, object_id=object_id) + journal_entry.revert(local_logger=local_logger) except (ValidationError, DesignValidationError) as ex: local_logger.error(str(ex), extra={"obj": journal_entry.design_object}) raise ValueError from ex - if not object_id: + if not object_ids: # When the Journal is reverted, we mark is as not active anymore self.active = False self.save() + def __sub__(self, other: "Journal"): + """Calculate the difference between two journals. + + This method calculates the differences between the journal entries of two + journals. This is similar to Python's `set.difference` method. The result + is a queryset of JournalEntries from this journal that represent objects + that are are not in the `other` journal. + + Args: + other (Journal): The other Journal to subtract from this journal. + + Returns: + Queryset of journal entries + """ + if other is None: + return [] + + other_ids = other.entries.values_list("_design_object_id") + + return ( + self.entries.order_by("-index") + .exclude(_design_object_id__in=other_ids) + .values_list("_design_object_id", flat=True) + ) + class JournalEntryQuerySet(RestrictedQuerySet): """Queryset for `JournalEntry` objects.""" @@ -394,18 +440,37 @@ def exclude_decommissioned(self): """Returns JournalEntry which the related DesignInstance is not decommissioned.""" return self.exclude(journal__design_instance__status__name=choices.DesignInstanceStatusChoices.DECOMMISSIONED) - def filter_related(self, entry: "JournalEntry"): - """Returns JournalEntries which have the same object ID but excluding itself.""" - return self.filter(_design_object_id=entry._design_object_id).exclude( # pylint: disable=protected-access - id=entry.id - ) + def filter_related(self, entry): + """Returns other JournalEntries which have the same object ID but are in different designs. - def filter_same_parent_design_instance(self, entry: "JournalEntry"): - """Returns JournalEntries which have the same parent design instance.""" - return self.filter(_design_object_id=entry._design_object_id).exclude( # pylint: disable=protected-access - journal__design_instance__id=entry.journal.design_instance.id + Args: + entry (JournalEntry): The JournalEntry to use as reference. + + Returns: + QuerySet: The queryset that matches other journal entries with the same design object ID. This + excludes matching entries in the same design. + """ + return ( + self.filter(active=True) + .filter(_design_object_id=entry._design_object_id) # pylint:disable=protected-access + .exclude(journal__design_instance_id=entry.journal.design_instance_id) ) + def filter_by_instance(self, design_instance: "DesignInstance", model=None): + """Lookup all the entries for a design instance an optional model type. + + Args: + design_instance (DesignInstance): The design instance to retrieve all of the journal entries. + model (type, optional): An optional model type to filter by. Defaults to None. + + Returns: + Query set matching the options. + """ + queryset = self.filter(journal__design_instance=design_instance) + if model: + queryset.filter(_design_object_type=ContentType.objects.get_for_model(model)) + return queryset + class JournalEntry(BaseModel): """A single entry in the journal for exactly 1 object. @@ -430,6 +495,8 @@ class JournalEntry(BaseModel): related_name="entries", ) + index = models.IntegerField(null=False, blank=False) + _design_object_type = models.ForeignKey( to=ContentType, on_delete=models.PROTECT, @@ -465,7 +532,7 @@ def update_current_value_from_dict(current_value, added_value, removed_value): if key not in added_value: current_value[key] = removed_value[key] - def revert(self, local_logger: logging.Logger = logger, object_id=None): # pylint: disable=too-many-branches + def revert(self, local_logger: logging.Logger = logger): # pylint: disable=too-many-branches """Revert the changes that are represented in this journal entry. Raises: @@ -478,9 +545,6 @@ def revert(self, local_logger: logging.Logger = logger, object_id=None): # pyli # This is something that may happen when a design has been updated and object was deleted return - if object_id and str(self.design_object.id) != object_id: - return - # It is possible that the journal entry contains a stale copy of the # design object. Consider this example: A journal entry is create and # kept in memory. The object it represents is changed in another area @@ -493,16 +557,12 @@ def revert(self, local_logger: logging.Logger = logger, object_id=None): # pyli object_type = self.design_object._meta.verbose_name.title() object_str = str(self.design_object) - local_logger.info("Reverting journal entry for %s %s", object_type, object_str, extra={"obj": self}) + local_logger.info("Reverting journal entry", extra={"obj": self.design_object}) + # local_logger.info("Reverting journal entry for %s %s", object_type, object_str, extra={"obj": self}) if self.full_control: - related_entries = ( - JournalEntry.objects.filter(active=True) - .filter_related(self) - .filter_same_parent_design_instance(self) - .exclude_decommissioned() - ) + related_entries = list(JournalEntry.objects.filter_related(self).values_list("id", flat=True)) if related_entries: - active_journal_ids = ",".join([str(j.id) for j in related_entries]) + active_journal_ids = ",".join(map(str, related_entries)) raise DesignValidationError(f"This object is referenced by other active Journals: {active_journal_ids}") self.design_object._current_design = self.journal.design_instance # pylint: disable=protected-access @@ -521,10 +581,12 @@ def revert(self, local_logger: logging.Logger = logger, object_id=None): # pyli return differences = self.changes["differences"] - for attribute in differences.get("added", {}): added_value = differences["added"][attribute] - removed_value = differences["removed"][attribute] + if differences["removed"]: + removed_value = differences["removed"][attribute] + else: + removed_value = None if isinstance(added_value, dict) and isinstance(removed_value, dict): # If the value is a dictionary (e.g., config context), we only update the # keys changed, honouring the current value of the attribute diff --git a/nautobot_design_builder/recursive.py b/nautobot_design_builder/recursive.py deleted file mode 100644 index b4858b3b..00000000 --- a/nautobot_design_builder/recursive.py +++ /dev/null @@ -1,218 +0,0 @@ -"""Temporal file that includes the recursive functions used to manipulate designs.""" - -import itertools -from typing import Dict, Union -from nautobot_design_builder.errors import DesignImplementationError -from nautobot_design_builder.constants import NAUTOBOT_ID, IDENTIFIER_KEYS - - -def get_object_identifier(obj: Dict) -> Union[str, None]: - """Returns de object identifier value, if it exists. - - Args: - value (Union[list,dict,str]): The value to attempt to resolve. - - Returns: - Union[str, None]: the identifier value or None. - """ - for key in obj: - if any(identifier_key in key for identifier_key in IDENTIFIER_KEYS): - return obj[key] - return None - - -def inject_nautobot_uuids(initial_data, final_data, only_ext=False): # pylint: disable=too-many-branches - """This recursive function update the output design adding the Nautobot identifier.""" - if isinstance(initial_data, list): - for item1 in initial_data: - # If it's a ModelInstance - if not isinstance(item1, dict): - continue - - item1_identifier = get_object_identifier(item1) - if item1_identifier: - for item2 in final_data: - item2_identifier = get_object_identifier(item2) - if item2_identifier == item1_identifier: - inject_nautobot_uuids(item1, item2, only_ext) - break - elif isinstance(initial_data, dict): - new_data_identifier = get_object_identifier(final_data) - data_identifier = get_object_identifier(initial_data) - - for key in initial_data: - # We only recurse it for lists, not found a use case for dicts - if isinstance(initial_data[key], list) and key in final_data: - inject_nautobot_uuids(initial_data[key], final_data[key], only_ext) - - # Other special keys (extensions), not identifiers - elif "!" in key and not any(identifier_key in key for identifier_key in IDENTIFIER_KEYS): - inject_nautobot_uuids(initial_data[key], final_data[key], only_ext) - - if data_identifier == new_data_identifier and NAUTOBOT_ID in initial_data: - if not only_ext: - final_data[NAUTOBOT_ID] = initial_data[NAUTOBOT_ID] - else: - if data_identifier is None: - final_data[NAUTOBOT_ID] = initial_data[NAUTOBOT_ID] - - -# TODO: could we make it simpler? -def combine_designs( - new_value, old_value, future_value, decommissioned_objects, type_key -): # pylint: disable=too-many-locals,too-many-return-statements,too-many-branches,too-many-statements - """Recursive function to simplify the new design by comparing with a previous design. - - Args: - new_value: New design element. - old_value: Previous design element. - future_value: Final design element to be persisted for future reference. - decommissioned_objects: Elements that are no longer relevant and will be decommissioned. - type_key: Reference key in the design element. - - """ - if isinstance(new_value, list): - objects_to_decommission = [] - - for new_element, old_element, future_element in itertools.zip_longest( - new_value.copy(), old_value, future_value - ): - # It's assumed that the design will generated lists where the objects are on the same place - if new_element is None: - # This means that this is one element that was existing before, but it's no longer in the design - # Therefore, it must be decommissioned if it's a dictionary, that's a potential design object - if isinstance(old_element, dict): - objects_to_decommission.append((old_element.get(NAUTOBOT_ID), get_object_identifier(old_element))) - - elif old_element is None: - # If it is a new element in the design, we keep it as it is. - pass - - elif isinstance(new_element, dict) and isinstance(old_element, dict): - old_nautobot_identifier = old_element.get(NAUTOBOT_ID) - new_elem_identifier = get_object_identifier(new_element) - old_elem_identifier = get_object_identifier(old_element) - if new_elem_identifier != old_elem_identifier: - # If the objects in the same list position are not the same (based on the design identifier), - # the old element is added to the decommissioning list, and a recursive process to decommission - # all the related children objects is initiated - - objects_to_decommission.append((old_nautobot_identifier, old_elem_identifier)) - - # One possible situation is that a cable of a nested interface in the same object - # is added into the nested reduce design, but the nautobot identifier is lost to - # be taken into account to be decommissioned before. - inject_nautobot_uuids(old_element, new_element, only_ext=True) - - combine_designs({}, old_element, {}, decommissioned_objects, type_key) - - # When the elements have the same identifier, we progress on the recursive reduction analysis - elif combine_designs(new_element, old_element, future_element, decommissioned_objects, type_key): - # As we are iterating over the new_value list, we keep the elements that the `combine_designs` - # concludes that must be deleted as not longer relevant for the new design. - new_value.remove(new_element) - - else: - raise DesignImplementationError("Unexpected type of object.") - - if objects_to_decommission: - # All the elements marked for decommissioning are added to the mutable `decommissioned_objects` dictionary - # that will later revert the object changes done by this design. - if type_key not in decommissioned_objects: - decommissioned_objects[type_key] = [] - decommissioned_objects[type_key].extend(objects_to_decommission) - - # If the final result of the new_value list is empty (i.e., all the elements are no relevant), - # The function returns True to signal that the calling entity can be also reduced. - if new_value == []: - return True - - return False - - if isinstance(new_value, dict): - # Removing the old Nautobot identifier to simplify comparison - old_nautobot_identifier = old_value.pop(NAUTOBOT_ID, None) - - # When the objects are exactly the same (i.e., same values and no identifiers, including nested objects) - # The nautobot identifier must be persisted in the new design values, but the object may be reduced - # from the new design to implement (i.e., returning True) - if new_value == old_value: - if old_nautobot_identifier: - future_value[NAUTOBOT_ID] = old_nautobot_identifier - new_value[NAUTOBOT_ID] = old_nautobot_identifier - - # If the design object contains any reference to a another design object, it can't be - # reduced because maybe the referenced object is changing - for inner_key in new_value: - if isinstance(new_value[inner_key], str) and "!ref:" in new_value[inner_key]: - return False - - # If the design object is a reference for other design objects, it can't be reduced. - if "!ref" in new_value: - return False - - return True - - identifier_old_value = get_object_identifier(old_value) - - for inner_old_key in old_value: - if inner_old_key == NAUTOBOT_ID and "!" in inner_old_key: - continue - - # Resetting desired values for attributes not included in the new design implementation - # This makes them into account for decommissioning nested objects (e.g., interfaces, ip_addresses) - if inner_old_key not in new_value: - new_value[inner_old_key] = None - - identifier_new_value = get_object_identifier(new_value) - - for inner_key, inner_value in new_value.copy().items(): - if any(identifier_key in inner_key for identifier_key in IDENTIFIER_KEYS + ["!ref"]): - continue - - if ( - identifier_new_value - and identifier_new_value == identifier_old_value - and "!" not in inner_key - and inner_key in old_value - and new_value[inner_key] == old_value[inner_key] - ): - # If the values of the attribute in the design are the same, remove it for design reduction - del new_value[inner_key] - - elif not inner_value and isinstance(old_value[inner_key], list): - # If the old value was a list, and it doesn't exist in the new design object - # we append to the objects to decommission all the list objects, calling the recursive reduction - for obj in old_value[inner_key]: - if inner_key not in decommissioned_objects: - decommissioned_objects[inner_key] = [] - - decommissioned_objects[inner_key].append((obj[NAUTOBOT_ID], get_object_identifier(obj))) - combine_designs({}, obj, {}, decommissioned_objects, inner_key) - - elif isinstance(inner_value, (dict, list)) and inner_key in old_value: - # If an attribute is a dict or list, explore it recursively to reduce it - if combine_designs( - inner_value, - old_value[inner_key], - future_value[inner_key], - decommissioned_objects, - inner_key, - ): - del new_value[inner_key] - - # Reuse the Nautobot identifier for the future design in all cases - if old_nautobot_identifier and identifier_new_value == identifier_old_value: - future_value[NAUTOBOT_ID] = old_nautobot_identifier - - # If at this point we only have an identifier, remove the object, no need to take it into account - if len(new_value) <= 1: - return True - - # Reuse the Nautobot identifier for the current design only when there is need to keep it in the design - if old_nautobot_identifier and identifier_new_value == identifier_old_value: - new_value[NAUTOBOT_ID] = old_nautobot_identifier - - return False - - raise DesignImplementationError("The design reduction only works for dict or list objects.") diff --git a/nautobot_design_builder/signals.py b/nautobot_design_builder/signals.py index e98ae0f3..1c3ce072 100644 --- a/nautobot_design_builder/signals.py +++ b/nautobot_design_builder/signals.py @@ -5,14 +5,14 @@ from django.apps import apps from django.contrib.contenttypes.models import ContentType -from django.db.models.signals import post_save, post_delete +from django.db.models.signals import post_save from django.dispatch import receiver from django.conf import settings from django.db.models.signals import pre_delete from django.db.models import ProtectedError from nautobot.core.signals import nautobot_database_ready -from nautobot.extras.models import Job, Status, Tag +from nautobot.extras.models import Job, Status from nautobot.utilities.choices import ColorChoices from nautobot.extras.registry import registry from nautobot_design_builder.models import JournalEntry @@ -67,7 +67,6 @@ def create_design_model(sender, instance: Job, **kwargs): # pylint:disable=unus instance (Job): Job instance that has been created or updated. """ if instance.job_class and issubclass(instance.job_class, DesignJob): - _, created = Design.objects.get_or_create(job=instance) if created: _LOGGER.debug("Created design from %s", instance) @@ -106,9 +105,3 @@ def load_pre_delete_signals(): load_pre_delete_signals() - - -@receiver(signal=post_delete, sender=DesignInstance) -def handle_post_delete_design_instance(sender, instance, **kwargs): # pylint: disable=unused-argument - """Cleaning up the Tag created for a design instance.""" - Tag.objects.get(name=f"Managed by {instance}").delete() diff --git a/nautobot_design_builder/tests/__init__.py b/nautobot_design_builder/tests/__init__.py index 1368ffaa..2d0f3e5c 100644 --- a/nautobot_design_builder/tests/__init__.py +++ b/nautobot_design_builder/tests/__init__.py @@ -36,9 +36,11 @@ def get_mocked_job(self, design_class: Type[DesignJob]): job = design_class() job.job_result = mock.Mock() + job.save_design_file = lambda filename, content: None if nautobot_version < "2.0.0": job.request = mock.Mock() else: + # TODO: Remove this when we no longer support Nautobot 1.x job.job_result.data = {} old_run = job.run diff --git a/nautobot_design_builder/tests/designs/context.py b/nautobot_design_builder/tests/designs/context.py index 1375245e..cca382b7 100644 --- a/nautobot_design_builder/tests/designs/context.py +++ b/nautobot_design_builder/tests/designs/context.py @@ -1,12 +1,11 @@ """Base DesignContext for testing.""" import ipaddress -from functools import lru_cache from django.core.exceptions import ObjectDoesNotExist -from nautobot.dcim.models import Device, Interface -from nautobot.ipam.models import VRF, Prefix +from nautobot.dcim.models import Device +from nautobot.ipam.models import VRF from nautobot_design_builder.context import Context, context_file # pylint: disable=missing-function-docstring, inconsistent-return-statements @@ -28,20 +27,6 @@ class IntegrationTestContext(Context): def __hash__(self): return hash((self.pe.name, self.ce.name, self.customer_name)) - @lru_cache - def get_l3vpn_prefix(self, parent_prefix, prefix_length): - tag = self.design_instance_tag - if tag: - existing_prefix = Prefix.objects.filter(tags__in=[tag], prefix_length=30).first() - if existing_prefix: - return str(existing_prefix) - - for new_prefix in ipaddress.ip_network(parent_prefix).subnets(new_prefix=prefix_length): - try: - Prefix.objects.get(prefix=str(new_prefix)) - except ObjectDoesNotExist: - return new_prefix - def get_customer_id(self, customer_name, l3vpn_asn): try: vrf = VRF.objects.get(description=f"VRF for customer {customer_name}") @@ -53,16 +38,6 @@ def get_customer_id(self, customer_name, l3vpn_asn): new_id = int(last_vrf.name.split(":")[-1]) + 1 return str(new_id) - def get_interface_name(self, device): - root_interface_name = "GigabitEthernet" - interfaces = Interface.objects.filter(name__contains=root_interface_name, device=device) - tag = self.design_instance_tag - if tag: - existing_interface = interfaces.filter(tags__in=[tag]).first() - if existing_interface: - return existing_interface.name - return f"{root_interface_name}1/{len(interfaces) + 1}" - def get_ip_address(self, prefix, offset): net_prefix = ipaddress.ip_network(prefix) for count, host in enumerate(net_prefix): diff --git a/nautobot_design_builder/tests/designs/templates/integration_design_devices.yaml.j2 b/nautobot_design_builder/tests/designs/templates/integration_design_devices.yaml.j2 index edc189e0..6687fa66 100644 --- a/nautobot_design_builder/tests/designs/templates/integration_design_devices.yaml.j2 +++ b/nautobot_design_builder/tests/designs/templates/integration_design_devices.yaml.j2 @@ -8,18 +8,22 @@ "mpls_router": true, } interfaces: - - "!create_or_update:name": "{{ get_interface_name(device) }}" + - "!next_interface": {} status__name: "Planned" type: "other" {% if offset == 2 %} "!connect_cable": status__name: "Planned" - to: - device__name: "{{ other_device.name }}" - name: "{{ get_interface_name(other_device) }}" + to: "!ref:other_interface" + {% else %} + "!ref": "other_interface" {% endif %} + tags: + - {"!get:name": "VRF Interface"} ip_addresses: - - "!create_or_update:address": "{{ get_ip_address(get_l3vpn_prefix(l3vpn_prefix, l3vpn_prefix_length), offset) }}" + - "!child_prefix:address": + parent: "!ref:l3vpn_p2p_prefix" + offset: "0.0.0.{{ offset }}/30" status__name: "Reserved" {% endmacro %} diff --git a/nautobot_design_builder/tests/designs/templates/integration_design_ipam.yaml.j2 b/nautobot_design_builder/tests/designs/templates/integration_design_ipam.yaml.j2 index 4d8ae1de..14b0dd94 100644 --- a/nautobot_design_builder/tests/designs/templates/integration_design_ipam.yaml.j2 +++ b/nautobot_design_builder/tests/designs/templates/integration_design_ipam.yaml.j2 @@ -1,14 +1,25 @@ --- -vrfs: - - "!create_or_update:name": "{{ l3vpn_asn }}:{{ get_customer_id(customer_name, l3vpn_asn) }}" - description: "VRF for customer {{ customer_name }}" - "!ref": "my_vrf" - +tags: + - "!create_or_update:name": "VRF Prefix" + "slug": "vrf_prefix" + - "!create_or_update:name": "VRF Interface" + "slug": "vrf_interface" prefixes: - "!create_or_update:prefix": "{{ l3vpn_prefix }}" status__name: "Reserved" - - "!create_or_update:prefix": "{{ get_l3vpn_prefix(l3vpn_prefix, l3vpn_prefix_length) }}" - status__name: "Reserved" - vrf: "!ref:my_vrf" + +vrfs: + - "!create_or_update:name": "{{ l3vpn_asn }}:{{ get_customer_id(customer_name, l3vpn_asn) }}" + description: "VRF for customer {{ customer_name }}" + prefixes: + - "!next_prefix": + identified_by: + tags__name: "VRF Prefix" + prefix: "{{ l3vpn_prefix }}" + length: 30 + status__name: "Reserved" + tags: + - {"!get:name": "VRF Prefix"} + "!ref": "l3vpn_p2p_prefix" diff --git a/nautobot_design_builder/tests/designs/templates/simple_design_3.yaml.j2 b/nautobot_design_builder/tests/designs/templates/simple_design_3.yaml.j2 new file mode 100644 index 00000000..6f61f094 --- /dev/null +++ b/nautobot_design_builder/tests/designs/templates/simple_design_3.yaml.j2 @@ -0,0 +1,4 @@ +--- +manufacturers: + name: "Test Manufacturer 1" + name: "Test Manufacturer" diff --git a/nautobot_design_builder/tests/designs/test_designs.py b/nautobot_design_builder/tests/designs/test_designs.py index a3533b42..27a64612 100644 --- a/nautobot_design_builder/tests/designs/test_designs.py +++ b/nautobot_design_builder/tests/designs/test_designs.py @@ -1,10 +1,11 @@ """Design jobs used for unit testing.""" -from nautobot.dcim.models import Manufacturer, Device +from nautobot.dcim.models import Manufacturer, Device, Interface from nautobot.extras.jobs import StringVar, ObjectVar from nautobot_design_builder.design_job import DesignJob -from nautobot_design_builder.ext import Extension +from nautobot_design_builder.design import ModelInstance +from nautobot_design_builder.ext import Extension, AttributeExtension from nautobot_design_builder.contrib import ext from nautobot_design_builder.tests.designs.context import IntegrationTestContext @@ -20,15 +21,12 @@ class Meta: # pylint: disable=too-few-public-methods design_file = "templates/simple_design.yaml.j2" -class SimpleDesignWithInput(DesignJob): - """Simple design job with input.""" - - instance = StringVar() - secret = StringVar() +class SimpleDesign3(DesignJob): + """Simple design job with extra manufacturer.""" class Meta: # pylint: disable=too-few-public-methods - name = "Simple Design With Input" - design_file = "templates/simple_design_with_input.yaml.j2" + name = "Simple Design 3" + design_file = "templates/simple_design_3.yaml.j2" class SimpleDesignReport(DesignJob): @@ -44,7 +42,7 @@ class MultiDesignJob(DesignJob): """Design job that is implemented from multiple design files.""" class Meta: # pylint: disable=too-few-public-methods - name = "Multi Design" + name = "Multi File Design" design_files = [ "templates/simple_design.yaml.j2", "templates/simple_design_2.yaml.j2", @@ -55,7 +53,7 @@ class MultiDesignJobWithError(DesignJob): """Design job that includes an error (for unit testing).""" class Meta: # pylint: disable=too-few-public-methods - name = "Multi Design Job with Error" + name = "Multi File Design with Error" design_files = [ "templates/simple_design.yaml.j2", "templates/simple_design.yaml.j2", @@ -93,8 +91,41 @@ class Meta: # pylint: disable=too-few-public-methods design_file = "templates/design_with_validation_error.yaml.j2" +class NextInterfaceExtension(AttributeExtension): + """Attribute extension to calculate the next available interface name.""" + + tag = "next_interface" + + def attribute(self, *args, value, model_instance: ModelInstance) -> dict: + """Determine the next available interface name. + + Args: + *args: Any additional arguments following the tag name. These are `:` delimited. + value (Any): The value of the data structure at this key's point in the design YAML. This could be a scalar, a dict or a list. + model_instance (ModelInstance): Object is the ModelInstance that would ultimately contain the values. + + Returns: + dict: Dictionary with the new interface name `{"!create_or_update:name": new_interface_name} + """ + root_interface_name = "GigabitEthernet" + previous_interfaces = self.environment.design_instance.get_design_objects(Interface).values_list( + "id", flat=True + ) + interfaces = model_instance.relationship_manager.filter( + name__startswith="GigabitEthernet", + ) + existing_interface = interfaces.filter( + pk__in=previous_interfaces, + tags__name="VRF Interface", + ).first() + if existing_interface: + model_instance.instance = existing_interface + return {"!create_or_update:name": existing_interface.name} + return {"!create_or_update:name": f"{root_interface_name}1/{len(interfaces) + 1}"} + + class IntegrationDesign(DesignJob): - """Integration design job.""" + """Create a l3vpn connection.""" customer_name = StringVar() @@ -110,11 +141,19 @@ class IntegrationDesign(DesignJob): model=Device, ) - class Meta: # pylint: disable=too-few-public-methods - name = "Integration Design" - context_class = IntegrationTestContext - extensions = [ext.CableConnectionExtension] + class Meta: # pylint:disable=too-few-public-methods + """Metadata needed to implement the l3vpn design.""" + + name = "L3VPN Design" + commit_default = False design_files = [ "templates/integration_design_ipam.yaml.j2", "templates/integration_design_devices.yaml.j2", ] + context_class = IntegrationTestContext + extensions = [ + ext.CableConnectionExtension, + ext.NextPrefixExtension, + NextInterfaceExtension, + ext.ChildPrefixExtension, + ] diff --git a/nautobot_design_builder/tests/test_builder.py b/nautobot_design_builder/tests/test_builder.py index 8df196d9..12787d87 100644 --- a/nautobot_design_builder/tests/test_builder.py +++ b/nautobot_design_builder/tests/test_builder.py @@ -11,7 +11,7 @@ from nautobot.dcim.models import Cable -from nautobot_design_builder.design import Builder +from nautobot_design_builder.design import Environment from nautobot_design_builder.util import nautobot_version @@ -51,6 +51,15 @@ def check_equal(test, check, index): else: test.assertEqual(value0, value1, msg=f"Check {index}") + @staticmethod + def check_count_equal(test, check, index): + """Check that two values are equal.""" + value0 = _get_value(check[0]) + value1 = _get_value(check[1]) + if len(value0) == 1 and len(value1) == 1: + test.assertEqual(value0[0], value1[0], msg=f"Check {index}") + test.assertCountEqual(value0, value1, msg=f"Check {index}") + @staticmethod def check_model_exists(test, check, index): """Check that a model exists.""" @@ -114,7 +123,7 @@ def class_wrapper(test_class): # Create a new closure for testcase def test_wrapper(testcase): - @patch("nautobot_design_builder.design.Builder.roll_back") + @patch("nautobot_design_builder.design.Environment.roll_back") def test_runner(self, roll_back: Mock): if testcase.get("skip", False): self.skipTest("Skipping due to testcase skip=true") @@ -122,16 +131,13 @@ def test_runner(self, roll_back: Mock): for extension in testcase.get("extensions", []): extensions.append(_load_class(extension)) - for design in testcase["designs"]: - builder = Builder(extensions=extensions) - commit = design.pop("commit", True) - fake_file_name = "whatever" - builder.builder_output[fake_file_name] = design.copy() - builder.implement_design_changes( - design=design, deprecated_design={}, design_file=fake_file_name, commit=commit - ) - if not commit: - roll_back.assert_called() + with self.captureOnCommitCallbacks(execute=True): + for design in testcase["designs"]: + environment = Environment(extensions=extensions) + commit = design.pop("commit", True) + environment.implement_design(design=design, commit=commit) + if not commit: + roll_back.assert_called() for index, check in enumerate(testcase.get("checks", [])): for check_name, args in check.items(): diff --git a/nautobot_design_builder/tests/test_data_protection.py b/nautobot_design_builder/tests/test_data_protection.py index ff843ec4..e5526565 100644 --- a/nautobot_design_builder/tests/test_data_protection.py +++ b/nautobot_design_builder/tests/test_data_protection.py @@ -50,6 +50,7 @@ def setUp(self): full_control=True, changes=calculate_changes(self.manufacturer_from_design), journal=self.journal, + index=self.journal._next_index(), # pylint:disable=protected-access ) self.client = Client() diff --git a/nautobot_design_builder/tests/test_decommissioning_job.py b/nautobot_design_builder/tests/test_decommissioning_job.py index 77453392..c07e3306 100644 --- a/nautobot_design_builder/tests/test_decommissioning_job.py +++ b/nautobot_design_builder/tests/test_decommissioning_job.py @@ -125,7 +125,10 @@ def test_basic_decommission_run_with_full_control(self): self.assertEqual(1, Secret.objects.count()) journal_entry = models.JournalEntry.objects.create( - journal=self.journal1, design_object=self.secret, full_control=True + journal=self.journal1, + design_object=self.secret, + full_control=True, + index=self.journal1._next_index(), # pylint:disable=protected-access ) journal_entry.validated_save() @@ -137,13 +140,22 @@ def test_decommission_run_with_dependencies(self): self.assertEqual(1, Secret.objects.count()) journal_entry_1 = models.JournalEntry.objects.create( - journal=self.journal1, design_object=self.secret, full_control=True + journal=self.journal1, + design_object=self.secret, + full_control=True, + index=self.journal1._next_index(), # pylint:disable=protected-access ) journal_entry_1.validated_save() journal_entry_2 = models.JournalEntry.objects.create( - journal=self.journal2, design_object=self.secret, full_control=False, changes={"differences": {}} + journal=self.journal2, + design_object=self.secret, + full_control=False, + changes={ + "differences": {}, + }, + index=self.journal2._next_index(), # pylint:disable=protected-access ) journal_entry_2.validated_save() @@ -160,20 +172,24 @@ def test_decommission_run_with_dependencies_but_decommissioned(self): self.assertEqual(1, Secret.objects.count()) journal_entry_1 = models.JournalEntry.objects.create( - journal=self.journal1, design_object=self.secret, full_control=True + journal=self.journal1, + design_object=self.secret, + full_control=True, + index=self.journal1._next_index(), # pylint:disable=protected-access ) journal_entry_1.validated_save() journal_entry_2 = models.JournalEntry.objects.create( - journal=self.journal2, design_object=self.secret, full_control=False, changes={"differences": {}} + journal=self.journal2, + design_object=self.secret, + full_control=False, + changes={"differences": {}}, + index=self.journal2._next_index(), # pylint:disable=protected-access ) journal_entry_2.validated_save() - self.design_instance_2.status = Status.objects.get( - content_types=self.content_type, name=choices.DesignInstanceStatusChoices.DECOMMISSIONED - ) - self.design_instance_2.validated_save() + self.design_instance_2.decommission() self.job.run(data={"design_instances": [self.design_instance]}, commit=True) @@ -183,7 +199,11 @@ def test_basic_decommission_run_without_full_control(self): self.assertEqual(1, Secret.objects.count()) journal_entry_1 = models.JournalEntry.objects.create( - journal=self.journal1, design_object=self.secret, full_control=False, changes={"differences": {}} + journal=self.journal1, + design_object=self.secret, + full_control=False, + changes={"differences": {}}, + index=self.journal1._next_index(), # pylint:disable=protected-access ) journal_entry_1.validated_save() @@ -205,6 +225,7 @@ def test_decommission_run_without_full_control_string_value(self): "removed": {"description": "previous description"}, } }, + index=self.journal1._next_index(), # pylint:disable=protected-access ) journal_entry.validated_save() @@ -224,6 +245,7 @@ def test_decommission_run_without_full_control_dict_value_with_overlap(self): "removed": {"parameters": self.initial_params}, } }, + index=self.journal1._next_index(), # pylint:disable=protected-access ) journal_entry.validated_save() @@ -245,6 +267,7 @@ def test_decommission_run_without_full_control_dict_value_without_overlap(self): "removed": {"parameters": self.initial_params}, } }, + index=self.journal1._next_index(), # pylint:disable=protected-access ) journal_entry.validated_save() @@ -270,6 +293,7 @@ def test_decommission_run_without_full_control_dict_value_with_new_values_and_ol "removed": {"parameters": self.initial_params}, } }, + index=self.journal1._next_index(), # pylint:disable=protected-access ) journal_entry.validated_save() @@ -282,7 +306,10 @@ def test_decommission_run_with_pre_hook_pass(self): self.assertEqual(1, Secret.objects.count()) journal_entry_1 = models.JournalEntry.objects.create( - journal=self.journal1, design_object=self.secret, full_control=True + journal=self.journal1, + design_object=self.secret, + full_control=True, + index=self.journal1._next_index(), # pylint:disable=protected-access ) journal_entry_1.validated_save() @@ -295,7 +322,10 @@ def test_decommission_run_with_pre_hook_fail(self): models.DesignInstance.pre_decommission.connect(fake_ko) self.assertEqual(1, Secret.objects.count()) journal_entry_1 = models.JournalEntry.objects.create( - journal=self.journal1, design_object=self.secret, full_control=True + journal=self.journal1, + design_object=self.secret, + full_control=True, + index=self.journal1._next_index(), # pylint:disable=protected-access ) journal_entry_1.validated_save() @@ -311,7 +341,10 @@ def test_decommission_run_with_pre_hook_fail(self): def test_decommission_run_multiple_design_instance(self): journal_entry = models.JournalEntry.objects.create( - journal=self.journal1, design_object=self.secret, full_control=True + journal=self.journal1, + design_object=self.secret, + full_control=True, + index=self.journal1._next_index(), # pylint:disable=protected-access ) journal_entry.validated_save() @@ -323,7 +356,10 @@ def test_decommission_run_multiple_design_instance(self): secret_2.validated_save() journal_entry_2 = models.JournalEntry.objects.create( - journal=self.journal2, design_object=secret_2, full_control=True + journal=self.journal2, + design_object=secret_2, + full_control=True, + index=self.journal2._next_index(), # pylint:disable=protected-access ) journal_entry_2.validated_save() diff --git a/nautobot_design_builder/tests/test_design_job.py b/nautobot_design_builder/tests/test_design_job.py index 5c4ee43f..4e669dbd 100644 --- a/nautobot_design_builder/tests/test_design_job.py +++ b/nautobot_design_builder/tests/test_design_job.py @@ -26,31 +26,44 @@ class TestDesignJob(DesignTestCase): @patch("nautobot_design_builder.models.Journal") @patch("nautobot_design_builder.models.DesignInstance.objects.get") @patch("nautobot_design_builder.design_job.DesignJob.design_model") - @patch("nautobot_design_builder.design_job.Builder") - def test_simple_design_commit(self, object_creator: Mock, design_model_mock, design_instance_mock, journal_mock): + @patch("nautobot_design_builder.design_job.Environment") + def test_simple_design_commit(self, environment: Mock, *_): job = self.get_mocked_job(test_designs.SimpleDesign) job.run(data=self.data, commit=True) self.assertIsNotNone(job.job_result) - object_creator.assert_called() + environment.assert_called() self.assertDictEqual( {"manufacturers": {"name": "Test Manufacturer"}}, job.designs[test_designs.SimpleDesign.Meta.design_file], ) - object_creator.return_value.roll_back.assert_not_called() + environment.return_value.roll_back.assert_not_called() @patch("nautobot_design_builder.models.Journal") @patch("nautobot_design_builder.models.DesignInstance.objects.get") @patch("nautobot_design_builder.design_job.DesignJob.design_model") - def test_simple_design_report(self, design_model_mock, design_instance_mock, journal_mock): + def test_simple_design_rollback(self, *_): + self.assertEqual(0, Manufacturer.objects.all().count()) + job = self.get_mocked_job(test_designs.MultiDesignJobWithError) + if nautobot_version < "2": + job.run(data=self.data, commit=True) + else: + self.assertRaises(DesignValidationError, job.run, data={}, commit=True) + + self.assertEqual(0, Manufacturer.objects.all().count()) + + @patch("nautobot_design_builder.models.Journal") + @patch("nautobot_design_builder.models.DesignInstance.objects.get") + @patch("nautobot_design_builder.design_job.DesignJob.design_model") + def test_simple_design_report(self, *_): job = self.get_mocked_job(test_designs.SimpleDesignReport) job.run(data=self.data, commit=True) self.assertJobSuccess(job) - self.assertEqual("Report output", job.job_result.data["report"]) # pylint: disable=unsubscriptable-object + self.assertEqual("Report output", job.report) @patch("nautobot_design_builder.models.Journal") @patch("nautobot_design_builder.models.DesignInstance.objects.get") @patch("nautobot_design_builder.design_job.DesignJob.design_model") - def test_multiple_design_files(self, design_model_mock, design_instance_mock, journal_mock): + def test_multiple_design_files(self, *_): job = self.get_mocked_job(test_designs.MultiDesignJob) job.run(data=self.data, commit=True) self.assertDictEqual( @@ -65,7 +78,7 @@ def test_multiple_design_files(self, design_model_mock, design_instance_mock, jo @patch("nautobot_design_builder.models.Journal") @patch("nautobot_design_builder.models.DesignInstance.objects.get") @patch("nautobot_design_builder.design_job.DesignJob.design_model") - def test_multiple_design_files_with_roll_back(self, design_model_mock, design_instance_mock, journal_mock): + def test_multiple_design_files_with_roll_back(self, *_): self.assertEqual(0, Manufacturer.objects.all().count()) job = self.get_mocked_job(test_designs.MultiDesignJobWithError) if nautobot_version < "2": @@ -75,14 +88,14 @@ def test_multiple_design_files_with_roll_back(self, design_model_mock, design_in self.assertEqual(0, Manufacturer.objects.all().count()) - @patch("nautobot_design_builder.models.Journal") @patch("nautobot_design_builder.models.DesignInstance.objects.get") @patch("nautobot_design_builder.design_job.DesignJob.design_model") - @patch("nautobot_design_builder.design_job.Builder") - def test_custom_extensions(self, builder_patch: Mock, design_model_mock, design_instance_mock, journal_mock): + @patch("nautobot_design_builder.models.Journal") + @patch("nautobot_design_builder.design_job.Environment") + def test_custom_extensions(self, environment: Mock, journal_mock, *_): job = self.get_mocked_job(test_designs.DesignJobWithExtensions) job.run(data=self.data, commit=True) - builder_patch.assert_called_once_with( + environment.assert_called_once_with( job_result=job.job_result, extensions=test_designs.DesignJobWithExtensions.Meta.extensions, journal=journal_mock(), @@ -95,11 +108,9 @@ class TestDesignJobLogging(DesignTestCase): @patch("nautobot_design_builder.models.Journal") @patch("nautobot_design_builder.models.DesignInstance.objects.get") @patch("nautobot_design_builder.design_job.DesignJob.design_model") - @patch("nautobot_design_builder.design_job.Builder") - def test_simple_design_implementation_error( - self, object_creator: Mock, design_model_mock, design_instance_mock, journal_mock - ): - object_creator.return_value.implement_design_changes.side_effect = DesignImplementationError("Broken") + @patch("nautobot_design_builder.design_job.Environment") + def test_simple_design_implementation_error(self, environment: Mock, *_): + environment.return_value.implement_design.side_effect = DesignImplementationError("Broken") job = self.get_mocked_job(test_designs.SimpleDesign) if nautobot_version < "2": job.run(data=self.data, commit=True) @@ -112,7 +123,7 @@ def test_simple_design_implementation_error( @patch("nautobot_design_builder.models.Journal") @patch("nautobot_design_builder.models.DesignInstance.objects.get") @patch("nautobot_design_builder.design_job.DesignJob.design_model") - def test_invalid_ref(self, design_model_mock, design_instance_mock, journal_mock): + def test_invalid_ref(self, *_): job = self.get_mocked_job(test_designs.DesignWithRefError) if nautobot_version < "2": job.run(data=self.data, commit=True) @@ -124,7 +135,7 @@ def test_invalid_ref(self, design_model_mock, design_instance_mock, journal_mock @patch("nautobot_design_builder.models.Journal") @patch("nautobot_design_builder.models.DesignInstance.objects.get") @patch("nautobot_design_builder.design_job.DesignJob.design_model") - def test_failed_validation(self, design_model_mock, design_instance_mock, journal_mock): + def test_failed_validation(self, *_): job = self.get_mocked_job(test_designs.DesignWithValidationError) if nautobot_version < "2": job.run(data=self.data, commit=True) diff --git a/nautobot_design_builder/tests/test_errors.py b/nautobot_design_builder/tests/test_errors.py index 5d01ea85..9df582e5 100644 --- a/nautobot_design_builder/tests/test_errors.py +++ b/nautobot_design_builder/tests/test_errors.py @@ -19,7 +19,7 @@ def __init__(self, title="", parent=None): self.model_class = self self._meta = self self.verbose_name = "verbose name" - self.parent = parent + self._parent = parent def __str__(self): return self.title diff --git a/nautobot_design_builder/tests/test_ext.py b/nautobot_design_builder/tests/test_ext.py index 8e7d89b1..af288d26 100644 --- a/nautobot_design_builder/tests/test_ext.py +++ b/nautobot_design_builder/tests/test_ext.py @@ -1,11 +1,10 @@ """Unit tests related to template extensions.""" import sys -import copy from django.test import TestCase from nautobot_design_builder import ext -from nautobot_design_builder.design import Builder +from nautobot_design_builder.design import Environment from nautobot_design_builder.ext import DesignImplementationError @@ -14,7 +13,7 @@ class Extension(ext.AttributeExtension): tag = "custom_extension" - def attribute(self, value, model_instance) -> None: + def attribute(self, *args, value=None, model_instance=None) -> None: pass @@ -45,14 +44,14 @@ class TestCustomExtensions(TestCase): """Test that custom extensions are loaded correctly.""" def test_builder_called_with_custom_extensions(self): - builder = Builder(extensions=[Extension]) + environment = Environment(extensions=[Extension]) self.assertEqual( - builder.extensions["attribute"]["custom_extension"]["class"], + environment.extensions["attribute"]["custom_extension"]["class"], Extension, ) - def test_builder_called_with_invalid_extensions(self): - self.assertRaises(DesignImplementationError, Builder, extensions=[NotExtension]) + def test_environment_called_with_invalid_extensions(self): + self.assertRaises(DesignImplementationError, Environment, extensions=[NotExtension]) class TestExtensionCommitRollback(TestCase): @@ -69,7 +68,7 @@ class CommitExtension(ext.AttributeExtension): tag = "extension" - def attribute(self, value, model_instance) -> None: + def attribute(self, *args, value=None, model_instance=None) -> None: pass def commit(self) -> None: @@ -80,10 +79,9 @@ def roll_back(self) -> None: nonlocal rolled_back rolled_back = True - builder = Builder(extensions=[CommitExtension]) - builder.builder_output["whatever"] = copy.deepcopy(design) + environment = Environment(extensions=[CommitExtension]) try: - builder.implement_design_changes(design, {}, design_file="whatever", commit=commit) + environment.implement_design(design, commit=commit) except DesignImplementationError: pass return committed, rolled_back diff --git a/nautobot_design_builder/tests/test_inject_uuids.py b/nautobot_design_builder/tests/test_inject_uuids.py deleted file mode 100644 index 398a5bc7..00000000 --- a/nautobot_design_builder/tests/test_inject_uuids.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Unit tests related to the recursive functions for updating designs with UUIDs.""" - -import os -import json -import unittest -from parameterized import parameterized - -from nautobot_design_builder.recursive import inject_nautobot_uuids - - -# pylint: disable=missing-class-docstring - - -class TestInjectUUIDs(unittest.TestCase): - def setUp(self): - self.maxDiff = None # pylint: disable=invalid-name - - @parameterized.expand( - [ - [ - "test1", - ], - [ - "test2", - ], - ] - ) - def test_inject_uuids(self, folder_name): - folder_path = os.path.join(os.path.dirname(__file__), "testdata_inject_uuids") - deferred_data_filename = os.path.join(folder_path, folder_name, "deferred_data.json") - goal_data_filename = os.path.join(folder_path, folder_name, "goal_data.json") - future_data_filename = os.path.join(folder_path, folder_name, "future_data.json") - with open(deferred_data_filename, encoding="utf-8") as deferred_file, open( - goal_data_filename, encoding="utf-8" - ) as goal_data_file, open(future_data_filename, encoding="utf-8") as future_data_file: - deferred_data = json.load(deferred_file) - future_data = json.load(future_data_file) - goal_data = json.load(goal_data_file) - - inject_nautobot_uuids(deferred_data, future_data) - self.assertEqual(future_data, goal_data) diff --git a/nautobot_design_builder/tests/test_model_journal_entry.py b/nautobot_design_builder/tests/test_model_journal_entry.py index 8ad083af..3793c862 100644 --- a/nautobot_design_builder/tests/test_model_journal_entry.py +++ b/nautobot_design_builder/tests/test_model_journal_entry.py @@ -40,6 +40,7 @@ def setUp(self) -> None: full_control=True, changes=calculate_changes(self.secret), journal=self.journal, + index=0, ) # Used to test Property attributes and ForeignKeys @@ -54,6 +55,7 @@ def setUp(self) -> None: full_control=True, changes=calculate_changes(self.device_type), journal=self.journal, + index=1, ) def get_entry(self, updated_object, design_object=None, initial_state=None): @@ -72,32 +74,23 @@ def get_entry(self, updated_object, design_object=None, initial_state=None): ), full_control=False, journal=self.journal, + index=self.journal._next_index(), # pylint:disable=protected-access ) @patch("nautobot_design_builder.models.JournalEntry.objects") def test_revert_full_control(self, objects: Mock): - objects.filter.side_effect = lambda active: objects - objects.filter_related.side_effect = lambda _: objects - objects.filter_same_parent_design_instance.side_effect = lambda _: objects - objects.exclude_decommissioned.return_value = [] + objects.filter_related.side_effect = lambda *args, **kwargs: objects + objects.values_list.side_effect = lambda *args, **kwargs: [] self.assertEqual(1, Secret.objects.count()) self.initial_entry.revert() - objects.filter.assert_called() - objects.filter_related.assert_called() - objects.filter_same_parent_design_instance.assert_called() - objects.exclude_decommissioned.assert_called() self.assertEqual(0, Secret.objects.count()) @patch("nautobot_design_builder.models.JournalEntry.objects") def test_revert_with_dependencies(self, objects: Mock): - objects.filter.side_effect = lambda active: objects - objects.filter_related.side_effect = lambda _: objects - objects.filter_same_parent_design_instance.side_effect = lambda _: objects + objects.filter_related.side_effect = lambda *args, **kwargs: objects + objects.values_list.side_effect = lambda *args, **kwargs: [12345] self.assertEqual(1, Secret.objects.count()) - entry2 = JournalEntry() - objects.exclude_decommissioned.return_value = [entry2] self.assertRaises(DesignValidationError, self.initial_entry.revert) - objects.exclude_decommissioned.assert_called() def test_updated_scalar(self): updated_secret = Secret.objects.get(id=self.secret.id) diff --git a/nautobot_design_builder/tests/test_reduce.py b/nautobot_design_builder/tests/test_reduce.py deleted file mode 100644 index a719936f..00000000 --- a/nautobot_design_builder/tests/test_reduce.py +++ /dev/null @@ -1,76 +0,0 @@ -"""Unit tests related to the recursive functions for reducing and updating designs.""" - -import copy -import unittest -import os -import json -from parameterized import parameterized - -from nautobot_design_builder.recursive import combine_designs - - -# pylint: disable=missing-class-docstring - - -class TestReduce(unittest.TestCase): - def setUp(self): - self.maxDiff = None # pylint: disable=invalid-name - - @parameterized.expand( - [ - [ - "test1", - ], - [ - "test2", - ], - [ - "test3", - ], - [ - "test4", - ], - [ - "test5", - ], - ] - ) - def test_combine_designs(self, folder_name): # pylint: disable=too-many-locals - folder_path = os.path.join(os.path.dirname(__file__), "testdata_reduce") - design_filename = os.path.join(folder_path, folder_name, "design.json") - previous_design_filename = os.path.join(folder_path, folder_name, "previous_design.json") - goal_design_filename = os.path.join(folder_path, folder_name, "goal_design.json") - goal_elements_to_be_decommissioned_filename = os.path.join( - folder_path, folder_name, "goal_elements_to_be_decommissioned.json" - ) - - with open(design_filename, encoding="utf-8") as design_file, open( - previous_design_filename, encoding="utf-8" - ) as previous_design_file, open(goal_design_filename, encoding="utf-8") as goal_design_file, open( - goal_elements_to_be_decommissioned_filename, encoding="utf-8" - ) as goal_elements_to_be_decommissioned_file: - design = json.load(design_file) - previous_design = json.load(previous_design_file) - goal_design = json.load(goal_design_file) - goal_elements_to_be_decommissioned = json.load(goal_elements_to_be_decommissioned_file) - - elements_to_be_decommissioned = {} - future_design = copy.deepcopy(design) - ext_keys_to_be_simplified = [] - for key, new_value in design.items(): - old_value = previous_design[key] - future_value = future_design[key] - to_delete = combine_designs(new_value, old_value, future_value, elements_to_be_decommissioned, key) - if to_delete: - ext_keys_to_be_simplified.append(key) - - for key, value in goal_design.items(): - self.assertEqual(value, design[key]) - - for key, value in goal_elements_to_be_decommissioned.items(): - for item1, item2 in zip(value, elements_to_be_decommissioned[key]): - self.assertEqual(tuple(item1), item2) - - -if __name__ == "__main__": - unittest.main() diff --git a/nautobot_design_builder/tests/testdata/nautobot_v1/custom_relationship_by_label.yaml b/nautobot_design_builder/tests/testdata/nautobot_v1/custom_relationship_by_label.yaml new file mode 100644 index 00000000..34c1d209 --- /dev/null +++ b/nautobot_design_builder/tests/testdata/nautobot_v1/custom_relationship_by_label.yaml @@ -0,0 +1,55 @@ +--- +extensions: + - "nautobot_design_builder.contrib.ext.LookupExtension" +designs: + - relationships: + - name: "Device to VLANS" + slug: "device-to-vlans" + type: "many-to-many" + "!lookup:source_type": + app_label: "dcim" + model: "device" + "!lookup:destination_type": + app_label: "ipam" + model: "vlan" + manufacturers: + - name: "manufacturer1" + + device_types: + - manufacturer__name: "manufacturer1" + model: "model name" + u_height: 1 + + device_roles: + - name: "device role" + + sites: + - name: "site_1" + status__name: "Active" + + vlans: + - "!create_or_update:vid": 42 + name: "The Answer" + status__name: "Active" + + devices: + - name: "device_1" + site__name: "site_1" + status__name: "Active" + device_type__model: "model name" + device_role__name: "device role" + vlans: + - "!get:vid": 42 + - vid: "43" + name: "Better Answer" + status__name: "Active" +checks: + - model_exists: + model: "nautobot.ipam.models.VLAN" + query: {vid: "43"} + + - equal: + - model: "nautobot.extras.models.RelationshipAssociation" + query: {relationship__name: "Device to VLANS"} + attribute: "destination" + - model: "nautobot.ipam.models.VLAN" diff --git a/nautobot_design_builder/tests/testdata/nautobot_v1/custom_relationship.yaml b/nautobot_design_builder/tests/testdata/nautobot_v1/custom_relationship_by_slug.yaml similarity index 100% rename from nautobot_design_builder/tests/testdata/nautobot_v1/custom_relationship.yaml rename to nautobot_design_builder/tests/testdata/nautobot_v1/custom_relationship_by_slug.yaml diff --git a/nautobot_design_builder/tests/testdata/nautobot_v1/device_primary_ip.yaml b/nautobot_design_builder/tests/testdata/nautobot_v1/device_primary_ip.yaml index 364b3fdd..d2beba83 100644 --- a/nautobot_design_builder/tests/testdata/nautobot_v1/device_primary_ip.yaml +++ b/nautobot_design_builder/tests/testdata/nautobot_v1/device_primary_ip.yaml @@ -29,7 +29,7 @@ designs: ip_addresses: - address: "192.168.56.1/24" status__name: "Active" - primary_ip4: {"!get:address": "192.168.56.1/24"} + primary_ip4: {"!get:address": "192.168.56.1/24", "deferred": true} checks: - equal: - model: "nautobot.dcim.models.Device" diff --git a/nautobot_design_builder/tests/testdata/nautobot_v1/git_repo.yaml b/nautobot_design_builder/tests/testdata/nautobot_v1/git_repo.yaml new file mode 100644 index 00000000..4ad8cbf2 --- /dev/null +++ b/nautobot_design_builder/tests/testdata/nautobot_v1/git_repo.yaml @@ -0,0 +1,14 @@ +--- +designs: + - git_repositories: + - model_metadata: + save_args: + trigger_resync: false + name: "backups" + remote_url: "https://github.com/nautobot/demo-gc-backups" + branch: "main" + +checks: + - model_exists: + model: "nautobot.extras.models.GitRepository" + query: {name: "backups"} diff --git a/nautobot_design_builder/tests/testdata/nautobot_v2/custom_relationship_by_key.yaml b/nautobot_design_builder/tests/testdata/nautobot_v2/custom_relationship_by_key.yaml new file mode 100644 index 00000000..88a9bc11 --- /dev/null +++ b/nautobot_design_builder/tests/testdata/nautobot_v2/custom_relationship_by_key.yaml @@ -0,0 +1,64 @@ +--- +extensions: + - "nautobot_design_builder.contrib.ext.LookupExtension" +designs: + - relationships: + - label: "Device to VLANS" + key: "device_to_vlans" + type: "many-to-many" + "!lookup:source_type": + app_label: "dcim" + model: "device" + "!lookup:destination_type": + app_label: "ipam" + model: "vlan" + + manufacturers: + - name: "manufacturer1" + + device_types: + - manufacturer__name: "manufacturer1" + model: "model name" + u_height: 1 + + roles: + - name: "device role" + content_types: + - "!get:app_label": "dcim" + "!get:model": "device" + + location_types: + - name: "Site" + content_types: + - "!get:app_label": "dcim" + "!get:model": "device" + locations: + - name: "site_1" + status__name: "Active" + + vlans: + - "!create_or_update:vid": 42 + name: "The Answer" + status__name: "Active" + + devices: + - name: "device_1" + location__name: "site_1" + status__name: "Active" + device_type__model: "model name" + role__name: "device role" + device_to_vlans: + - "!get:vid": 42 + - vid: "43" + name: "Better Answer" + status__name: "Active" +checks: + - model_exists: + model: "nautobot.ipam.models.VLAN" + query: {vid: "43"} + + - count_equal: + - model: "nautobot.extras.models.RelationshipAssociation" + query: {relationship__label: "Device to VLANS"} + attribute: "destination" + - model: "nautobot.ipam.models.VLAN" diff --git a/nautobot_design_builder/tests/testdata/nautobot_v2/custom_relationship_by_label.yaml b/nautobot_design_builder/tests/testdata/nautobot_v2/custom_relationship_by_label.yaml new file mode 100644 index 00000000..c63782bc --- /dev/null +++ b/nautobot_design_builder/tests/testdata/nautobot_v2/custom_relationship_by_label.yaml @@ -0,0 +1,64 @@ +--- +extensions: + - "nautobot_design_builder.contrib.ext.LookupExtension" +designs: + - relationships: + - label: "Device to VLANS" + key: "device_to_vlans" + type: "many-to-many" + "!lookup:source_type": + app_label: "dcim" + model: "device" + "!lookup:destination_type": + app_label: "ipam" + model: "vlan" + + manufacturers: + - name: "manufacturer1" + + device_types: + - manufacturer__name: "manufacturer1" + model: "model name" + u_height: 1 + + roles: + - name: "device role" + content_types: + - "!get:app_label": "dcim" + "!get:model": "device" + + location_types: + - name: "Site" + content_types: + - "!get:app_label": "dcim" + "!get:model": "device" + locations: + - name: "site_1" + status__name: "Active" + + vlans: + - "!create_or_update:vid": 42 + name: "The Answer" + status__name: "Active" + + devices: + - name: "device_1" + location__name: "site_1" + status__name: "Active" + device_type__model: "model name" + role__name: "device role" + vlans: + - "!get:vid": 42 + - vid: "43" + name: "Better Answer" + status__name: "Active" +checks: + - model_exists: + model: "nautobot.ipam.models.VLAN" + query: {vid: "43"} + + - count_equal: + - model: "nautobot.extras.models.RelationshipAssociation" + query: {relationship__label: "Device to VLANS"} + attribute: "destination" + - model: "nautobot.ipam.models.VLAN" diff --git a/nautobot_design_builder/tests/testdata/nautobot_v2/git_repo.yaml b/nautobot_design_builder/tests/testdata/nautobot_v2/git_repo.yaml new file mode 100644 index 00000000..9a06aa0c --- /dev/null +++ b/nautobot_design_builder/tests/testdata/nautobot_v2/git_repo.yaml @@ -0,0 +1,11 @@ +--- +designs: + - git_repositories: + - name: "backups" + remote_url: "https://github.com/nautobot/demo-gc-backups" + branch: "main" + +checks: + - model_exists: + model: "nautobot.extras.models.GitRepository" + query: {name: "backups"} diff --git a/nautobot_design_builder/tests/testdata/nautobot_v2/interface_addresses.yaml b/nautobot_design_builder/tests/testdata/nautobot_v2/interface_addresses.yaml index bc13583d..5f18e090 100644 --- a/nautobot_design_builder/tests/testdata/nautobot_v2/interface_addresses.yaml +++ b/nautobot_design_builder/tests/testdata/nautobot_v2/interface_addresses.yaml @@ -20,8 +20,8 @@ designs: - "!get:app_label": "dcim" "!get:model": "device" locations: - name: "site_1" - status__name: "Active" + - name: "site_1" + status__name: "Active" prefixes: - prefix: "192.168.56.0/24" diff --git a/nautobot_design_builder/tests/testdata/nautobot_v2/nested_create.yaml b/nautobot_design_builder/tests/testdata/nautobot_v2/nested_create.yaml index afd5886b..9122a160 100644 --- a/nautobot_design_builder/tests/testdata/nautobot_v2/nested_create.yaml +++ b/nautobot_design_builder/tests/testdata/nautobot_v2/nested_create.yaml @@ -20,8 +20,8 @@ designs: - "!get:app_label": "dcim" "!get:model": "device" locations: - name: "site_1" - status__name: "Active" + - name: "site_1" + status__name: "Active" devices: - name: "device_1" diff --git a/nautobot_design_builder/tests/testdata/nautobot_v2/prefixes.yaml b/nautobot_design_builder/tests/testdata/nautobot_v2/prefixes.yaml index 9d3469cb..d6d18c19 100644 --- a/nautobot_design_builder/tests/testdata/nautobot_v2/prefixes.yaml +++ b/nautobot_design_builder/tests/testdata/nautobot_v2/prefixes.yaml @@ -6,20 +6,23 @@ designs: - "!get:app_label": "ipam" "!get:model": "prefix" locations: - name: "site_1" - status__name: "Active" + - name: "site_1" + status__name: "Active" + "!ref": "site_1" prefixes: - - location__name: "site_1" + - locations: + - "!ref:site_1" status__name: "Active" prefix: "192.168.0.0/24" - - "!create_or_update:location__name": "site_1" - "!create_or_update:prefix": "192.168.56.0/24" + - "!create_or_update:prefix": "192.168.56.0/24" + locations: + - "!ref:site_1" status__name: "Active" checks: - equal: - model: "nautobot.ipam.models.Prefix" - query: {location__name: "site_1"} + query: {locations__name: "site_1"} attribute: "__str__" - value: ["192.168.0.0/24", "192.168.56.0/24"] diff --git a/nautobot_design_builder/tests/testdata_inject_uuids/test1/deferred_data.json b/nautobot_design_builder/tests/testdata_inject_uuids/test1/deferred_data.json deleted file mode 100644 index 78fb48d6..00000000 --- a/nautobot_design_builder/tests/testdata_inject_uuids/test1/deferred_data.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "interfaces": [ - { - "!create_or_update:name": "Vlan1", - "ip_addresses": [ - { - "!create_or_update:address": "10.250.0.6/30", - "status__name": "Reserved", - "nautobot_identifier": "0bd5ff9d-1457-4935-8b85-78f2a6defee4" - } - ], - "nautobot_identifier": "dc0cf235-305a-4553-afb9-1f0d0e6eba93" - } - ] -} diff --git a/nautobot_design_builder/tests/testdata_inject_uuids/test1/future_data.json b/nautobot_design_builder/tests/testdata_inject_uuids/test1/future_data.json deleted file mode 100644 index d5a1ee72..00000000 --- a/nautobot_design_builder/tests/testdata_inject_uuids/test1/future_data.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "!update:name": "Device 1", - "site__name": "Site 1", - "location__name": "Location 1", - "device_role__slug": "ces", - "status__name": "Planned", - "interfaces": [ - { - "!update:name": "GigabitEthernet1/0/1", - "!connect_cable": { - "status__name": "Planned", - "to": {"device__name": "Device 2", "name": "GigabitEthernet0/0/0"}, - "nautobot_identifier": "dab03f25-58be-4185-9daf-0deff326543f" - }, - "nautobot_identifier": "ed0de1c0-2d99-4b83-ac5f-8fe4c03cac14" - }, - { - "!update:name": "GigabitEthernet1/0/14", - "!connect_cable": { - "status__name": "Planned", - "to": {"device__name": "Device 4", "name": "GigabitEthernet0/0/0"}, - "nautobot_identifier": "44198dd4-5e71-4f75-b4f6-c756b16c30bc" - }, - "nautobot_identifier": "b8321d58-1266-4ed3-a55d-92c25a1adb88" - }, - { - "!create_or_update:name": "Vlan1", - "status__name": "Planned", - "type": "virtual", - "ip_addresses": [{"!create_or_update:address": "10.250.0.6/30", "status__name": "Reserved"}], - "nautobot_identifier": "dc0cf235-305a-4553-afb9-1f0d0e6eba93" - } - ], - "nautobot_identifier": "d93ca54a-6123-4792-b7d9-d730a6fddaa4" -} diff --git a/nautobot_design_builder/tests/testdata_inject_uuids/test1/goal_data.json b/nautobot_design_builder/tests/testdata_inject_uuids/test1/goal_data.json deleted file mode 100644 index 3013e1bb..00000000 --- a/nautobot_design_builder/tests/testdata_inject_uuids/test1/goal_data.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "!update:name": "Device 1", - "site__name": "Site 1", - "location__name": "Location 1", - "device_role__slug": "ces", - "status__name": "Planned", - "interfaces": [ - { - "!update:name": "GigabitEthernet1/0/1", - "!connect_cable": { - "status__name": "Planned", - "to": {"device__name": "Device 2", "name": "GigabitEthernet0/0/0"}, - "nautobot_identifier": "dab03f25-58be-4185-9daf-0deff326543f" - }, - "nautobot_identifier": "ed0de1c0-2d99-4b83-ac5f-8fe4c03cac14" - }, - { - "!update:name": "GigabitEthernet1/0/14", - "!connect_cable": { - "status__name": "Planned", - "to": {"device__name": "Device 4", "name": "GigabitEthernet0/0/0"}, - "nautobot_identifier": "44198dd4-5e71-4f75-b4f6-c756b16c30bc" - }, - "nautobot_identifier": "b8321d58-1266-4ed3-a55d-92c25a1adb88" - }, - { - "!create_or_update:name": "Vlan1", - "status__name": "Planned", - "type": "virtual", - "ip_addresses": [ - { - "!create_or_update:address": "10.250.0.6/30", - "status__name": "Reserved", - "nautobot_identifier": "0bd5ff9d-1457-4935-8b85-78f2a6defee4" - } - ], - "nautobot_identifier": "dc0cf235-305a-4553-afb9-1f0d0e6eba93" - } - ], - "nautobot_identifier": "d93ca54a-6123-4792-b7d9-d730a6fddaa4" -} diff --git a/nautobot_design_builder/tests/testdata_inject_uuids/test2/deferred_data.json b/nautobot_design_builder/tests/testdata_inject_uuids/test2/deferred_data.json deleted file mode 100644 index 265c9e6a..00000000 --- a/nautobot_design_builder/tests/testdata_inject_uuids/test2/deferred_data.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "interfaces": [ - { - "!update:name": "GigabitEthernet1/0/1", - "!connect_cable": { - "status__name": "Planned", - "to": {"device__name": "Device 2", "name": "GigabitEthernet0/0/0"}, - "nautobot_identifier": "8322e248-a872-4b54-930e-e8fe5a1ad4d0" - }, - "nautobot_identifier": "ed0de1c0-2d99-4b83-ac5f-8fe4c03cac14" - }, - { - "!update:name": "GigabitEthernet1/0/14", - "!connect_cable": { - "status__name": "Planned", - "to": {"device__name": "Device 4", "name": "GigabitEthernet0/0/0"}, - "nautobot_identifier": "c514cdf9-754e-4c1c-b1ff-eddb17d396d4" - }, - "nautobot_identifier": "b8321d58-1266-4ed3-a55d-92c25a1adb88" - }, - { - "!create_or_update:name": "Vlan1", - "status__name": "Planned", - "type": "virtual", - "ip_addresses": [ - { - "!create_or_update:address": "10.250.0.2/30", - "status__name": "Reserved", - "nautobot_identifier": "8f910a91-395f-4c00-adfc-303121dc5d69" - } - ], - "nautobot_identifier": "acca93cf-813f-4cd5-a15b-90847d5fe118" - } - ] -} diff --git a/nautobot_design_builder/tests/testdata_inject_uuids/test2/future_data.json b/nautobot_design_builder/tests/testdata_inject_uuids/test2/future_data.json deleted file mode 100644 index c810c6c7..00000000 --- a/nautobot_design_builder/tests/testdata_inject_uuids/test2/future_data.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "!update:name": "Device 1", - "site__name": "Site 1", - "location__name": "Location 1", - "device_role__slug": "ces", - "status__name": "Planned", - "interfaces": [ - { - "!update:name": "GigabitEthernet1/0/1", - "!connect_cable": { - "status__name": "Planned", - "to": {"device__name": "Device 2", "name": "GigabitEthernet0/0/0"} - } - }, - { - "!update:name": "GigabitEthernet1/0/14", - "!connect_cable": { - "status__name": "Planned", - "to": {"device__name": "Device 4", "name": "GigabitEthernet0/0/0"} - } - }, - { - "!create_or_update:name": "Vlan1", - "status__name": "Planned", - "type": "virtual", - "ip_addresses": [{"!create_or_update:address": "10.250.0.2/30", "status__name": "Reserved"}] - } - ], - "nautobot_identifier": "d93ca54a-6123-4792-b7d9-d730a6fddaa4" -} diff --git a/nautobot_design_builder/tests/testdata_inject_uuids/test2/goal_data.json b/nautobot_design_builder/tests/testdata_inject_uuids/test2/goal_data.json deleted file mode 100644 index ca4ed934..00000000 --- a/nautobot_design_builder/tests/testdata_inject_uuids/test2/goal_data.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "!update:name": "Device 1", - "site__name": "Site 1", - "location__name": "Location 1", - "device_role__slug": "ces", - "status__name": "Planned", - "interfaces": [ - { - "!update:name": "GigabitEthernet1/0/1", - "!connect_cable": { - "status__name": "Planned", - "to": {"device__name": "Device 2", "name": "GigabitEthernet0/0/0"}, - "nautobot_identifier": "8322e248-a872-4b54-930e-e8fe5a1ad4d0" - }, - "nautobot_identifier": "ed0de1c0-2d99-4b83-ac5f-8fe4c03cac14" - }, - { - "!update:name": "GigabitEthernet1/0/14", - "!connect_cable": { - "status__name": "Planned", - "to": {"device__name": "Device 4", "name": "GigabitEthernet0/0/0"}, - "nautobot_identifier": "c514cdf9-754e-4c1c-b1ff-eddb17d396d4" - }, - "nautobot_identifier": "b8321d58-1266-4ed3-a55d-92c25a1adb88" - }, - { - "!create_or_update:name": "Vlan1", - "status__name": "Planned", - "type": "virtual", - "ip_addresses": [ - { - "!create_or_update:address": "10.250.0.2/30", - "status__name": "Reserved", - "nautobot_identifier": "8f910a91-395f-4c00-adfc-303121dc5d69" - } - ], - "nautobot_identifier": "acca93cf-813f-4cd5-a15b-90847d5fe118" - } - ], - "nautobot_identifier": "d93ca54a-6123-4792-b7d9-d730a6fddaa4" -} diff --git a/nautobot_design_builder/tests/testdata_reduce/test1/design.json b/nautobot_design_builder/tests/testdata_reduce/test1/design.json deleted file mode 100644 index 17f22a0e..00000000 --- a/nautobot_design_builder/tests/testdata_reduce/test1/design.json +++ /dev/null @@ -1,107 +0,0 @@ -{ - "prefixes": [ - { - "!create_or_update:prefix": "10.255.0.0/32", - "status__name": "Active", - "description": "co-intraprefix-01 Instance:a" - }, - { - "!create_or_update:prefix": "10.255.0.1/32", - "status__name": "Active", - "description": "ce01-intraprefix Instance:a" - }, - { - "!create_or_update:prefix": "10.250.0.4/30", - "status__name": "Active", - "description": "ce-ces Mgmt Instance:a" - }, - { - "!create_or_update:prefix": "10.250.100.4/30", - "status__name": "Active", - "description": "co-cer Mgmt Instance:a" - } - ], - "devices": [ - { - "!update:name": "Device 1", - "site__name": "Site 1", - "location__name": "Location 1", - "device_role__slug": "ces", - "status__name": "Planned", - "interfaces": [ - { - "!update:name": "GigabitEthernet1/0/1", - "!connect_cable": { - "status__name": "Planned", - "to": {"device__name": "Device 2", "name": "GigabitEthernet0/0/0"} - } - }, - { - "!update:name": "GigabitEthernet1/0/14", - "!connect_cable": { - "status__name": "Planned", - "to": {"device__name": "Device 4", "name": "GigabitEthernet0/0/0"} - } - }, - { - "!create_or_update:name": "Vlan1", - "status__name": "Planned", - "type": "virtual", - "ip_addresses": [ - {"!create_or_update:address": "10.250.0.6/30", "status__name": "Reserved"} - ] - } - ] - }, - { - "!update:name": "Device 2", - "site__name": "Site 1", - "location__name": "Location 1", - "device_role__slug": "ce", - "status__name": "Planned", - "interfaces": [ - { - "!update:name": "Ethernet0/2/0", - "!connect_cable": { - "status__name": "Planned", - "to": {"device__name": "Device 3", "name": "Ethernet0/2/0"} - }, - "ip_addresses": [ - {"!create_or_update:address": "10.250.100.5/30", "status__name": "Reserved"} - ] - }, - { - "!create_or_update:name": "lo10", - "status__name": "Planned", - "type": "virtual", - "ip_addresses": [ - {"!create_or_update:address": "10.255.0.0/32", "status__name": "Reserved"} - ] - } - ] - }, - { - "!update:name": "Device 3", - "site__name": "Site 2", - "location__name": "Location 2", - "device_role__slug": "cer", - "status__name": "Planned", - "interfaces": [ - { - "!update:name": "Ethernet0/2/0", - "ip_addresses": [ - {"!create_or_update:address": "10.250.100.6/30", "status__name": "Reserved"} - ] - }, - { - "!create_or_update:name": "lo10", - "status__name": "Planned", - "type": "virtual", - "ip_addresses": [ - {"!create_or_update:address": "10.255.0.1/32", "status__name": "Reserved"} - ] - } - ] - } - ] -} diff --git a/nautobot_design_builder/tests/testdata_reduce/test1/goal_design.json b/nautobot_design_builder/tests/testdata_reduce/test1/goal_design.json deleted file mode 100644 index f213b187..00000000 --- a/nautobot_design_builder/tests/testdata_reduce/test1/goal_design.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "prefixes": [ - { - "!create_or_update:prefix": "10.250.0.4/30", - "description": "ce-ces Mgmt Instance:a", - "status__name": "Active" - }, - { - "!create_or_update:prefix": "10.250.100.4/30", - "description": "co-cer Mgmt Instance:a", - "status__name": "Active" - } - ], - "devices": [ - { - "!update:name": "Device 1", - "interfaces": [ - { - "!create_or_update:name": "Vlan1", - "ip_addresses": [ - {"!create_or_update:address": "10.250.0.6/30", "status__name": "Reserved"} - ], - "nautobot_identifier": "ed91b2fc-cc4a-4726-82fc-07facbb429bb" - } - ], - "nautobot_identifier": "a6165def-a1a7-4c0d-8f96-aa6f7e3b83d2" - }, - { - "!update:name": "Device 2", - "interfaces": [ - { - "!update:name": "Ethernet0/2/0", - "ip_addresses": [ - {"!create_or_update:address": "10.250.100.5/30", "status__name": "Reserved"} - ], - "nautobot_identifier": "259a7a35-5336-4a45-aa74-27be778358a2" - } - ], - "nautobot_identifier": "1cc796cd-4c2c-47c4-af60-3c56f69965f8" - }, - { - "!update:name": "Device 3", - "interfaces": [ - { - "!update:name": "Ethernet0/2/0", - "ip_addresses": [ - {"!create_or_update:address": "10.250.100.6/30", "status__name": "Reserved"} - ], - "nautobot_identifier": "c9ae176d-ea86-4844-a5e7-cd331b9c9491" - } - ], - "nautobot_identifier": "2509af45-70e0-4708-87ca-8203b8570819" - } - ] -} diff --git a/nautobot_design_builder/tests/testdata_reduce/test1/goal_elements_to_be_decommissioned.json b/nautobot_design_builder/tests/testdata_reduce/test1/goal_elements_to_be_decommissioned.json deleted file mode 100644 index f3be7183..00000000 --- a/nautobot_design_builder/tests/testdata_reduce/test1/goal_elements_to_be_decommissioned.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "prefixes": [ - ["0804b67b-ec96-4f79-96c0-e7750fd42b5a", "10.250.0.0/30"], - ["9806c31b-a01d-4537-bf08-ba2db697052e", "10.250.100.0/30"] - ], - "ip_addresses": [ - ["c844e64d-b8e1-4226-80ef-c938f8177793", "10.250.0.2/30"], - ["33943833-8ab4-473c-a64d-5b45d54d1d46", "10.250.100.1/30"], - ["d50d3b01-e59d-431f-b91d-46c5a933afe8", "10.250.100.2/30"] - ] -} diff --git a/nautobot_design_builder/tests/testdata_reduce/test1/previous_design.json b/nautobot_design_builder/tests/testdata_reduce/test1/previous_design.json deleted file mode 100644 index 62cf30ab..00000000 --- a/nautobot_design_builder/tests/testdata_reduce/test1/previous_design.json +++ /dev/null @@ -1,144 +0,0 @@ -{ - "devices": [ - { - "interfaces": [ - { - "!update:name": "GigabitEthernet1/0/1", - "!connect_cable": { - "to": {"name": "GigabitEthernet0/0/0", "device__name": "Device 2"}, - "status__name": "Planned", - "nautobot_identifier": "0fd83863-6bf6-4a32-b056-1c14970307e1" - }, - "nautobot_identifier": "91772985-9564-4176-9232-94b12d30c0c3" - }, - { - "!update:name": "GigabitEthernet1/0/14", - "!connect_cable": { - "to": {"name": "GigabitEthernet0/0/0", "device__name": "Device 4"}, - "status__name": "Planned", - "nautobot_identifier": "5e2cc3a6-b47e-4070-8ca2-5df738e29774" - }, - "nautobot_identifier": "b783c298-c398-4498-9ecc-50ffcb9d0364" - }, - { - "type": "virtual", - "ip_addresses": [ - { - "status__name": "Reserved", - "nautobot_identifier": "c844e64d-b8e1-4226-80ef-c938f8177793", - "!create_or_update:address": "10.250.0.2/30" - } - ], - "status__name": "Planned", - "nautobot_identifier": "ed91b2fc-cc4a-4726-82fc-07facbb429bb", - "!create_or_update:name": "Vlan1" - } - ], - "site__name": "Site 1", - "!update:name": "Device 1", - "status__name": "Planned", - "location__name": "Location 1", - "device_role__slug": "ces", - "nautobot_identifier": "a6165def-a1a7-4c0d-8f96-aa6f7e3b83d2" - }, - { - "interfaces": [ - { - "!update:name": "Ethernet0/2/0", - "ip_addresses": [ - { - "status__name": "Reserved", - "nautobot_identifier": "33943833-8ab4-473c-a64d-5b45d54d1d46", - "!create_or_update:address": "10.250.100.1/30" - } - ], - "!connect_cable": { - "to": {"name": "Ethernet0/2/0", "device__name": "Device 3"}, - "status__name": "Planned", - "nautobot_identifier": "f321b2b4-421f-481a-9955-1f4347e14f6c" - }, - "nautobot_identifier": "259a7a35-5336-4a45-aa74-27be778358a2" - }, - { - "type": "virtual", - "ip_addresses": [ - { - "status__name": "Reserved", - "nautobot_identifier": "6a4e36f2-9231-4618-b091-9f5fbebfb387", - "!create_or_update:address": "10.255.0.0/32" - } - ], - "status__name": "Planned", - "nautobot_identifier": "65832777-e48e-4d5d-984c-e9fa32e3f7df", - "!create_or_update:name": "lo10" - } - ], - "site__name": "Site 1", - "!update:name": "Device 2", - "status__name": "Planned", - "location__name": "Location 1", - "device_role__slug": "ce", - "nautobot_identifier": "1cc796cd-4c2c-47c4-af60-3c56f69965f8" - }, - { - "interfaces": [ - { - "!update:name": "Ethernet0/2/0", - "ip_addresses": [ - { - "status__name": "Reserved", - "nautobot_identifier": "d50d3b01-e59d-431f-b91d-46c5a933afe8", - "!create_or_update:address": "10.250.100.2/30" - } - ], - "nautobot_identifier": "c9ae176d-ea86-4844-a5e7-cd331b9c9491" - }, - { - "type": "virtual", - "ip_addresses": [ - { - "status__name": "Reserved", - "nautobot_identifier": "be9b9a70-78ee-407c-93cf-55231718e5c7", - "!create_or_update:address": "10.255.0.1/32" - } - ], - "status__name": "Planned", - "nautobot_identifier": "2e4bc2ec-a837-4fc0-87b7-5fa6b9847532", - "!create_or_update:name": "lo10" - } - ], - "site__name": "Site 2", - "!update:name": "Device 3", - "status__name": "Planned", - "location__name": "Location 2", - "device_role__slug": "cer", - "nautobot_identifier": "2509af45-70e0-4708-87ca-8203b8570819" - } - ], - "prefixes": [ - { - "description": "co-intraprefix-01 Instance:a", - "status__name": "Active", - "nautobot_identifier": "4f2e9d74-3e3b-4231-a8b8-430726db0e1c", - "!create_or_update:prefix": "10.255.0.0/32" - }, - { - "description": "ce01-intraprefix Instance:a", - "status__name": "Active", - "nautobot_identifier": "6a109931-9194-4748-95d8-042156b786d8", - "!create_or_update:prefix": "10.255.0.1/32" - }, - { - "description": "ce-ces Mgmt Instance:a", - "status__name": "Active", - "nautobot_identifier": "0804b67b-ec96-4f79-96c0-e7750fd42b5a", - "!create_or_update:prefix": "10.250.0.0/30" - }, - { - "description": "co-cer Mgmt Instance:a", - "status__name": "Active", - "nautobot_identifier": "9806c31b-a01d-4537-bf08-ba2db697052e", - "!create_or_update:prefix": "10.250.100.0/30" - } - ] -} diff --git a/nautobot_design_builder/tests/testdata_reduce/test2/design.json b/nautobot_design_builder/tests/testdata_reduce/test2/design.json deleted file mode 100644 index baa58d29..00000000 --- a/nautobot_design_builder/tests/testdata_reduce/test2/design.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "manufacturers": [{"!create_or_update:name": "Juniper", "slug": "juniper"}], - "device_types": [ - { - "!create_or_update:model": "PTX10016", - "slug": "ptx10016", - "manufacturer__slug": "juniper", - "u_height": 21 - } - ], - "device_roles": [{"!create_or_update:name": "Core Router", "slug": "core_router", "color": "3f51b5"}], - "regions": { - "!create_or_update:name": "Americas", - "children": [ - { - "!create_or_update:name": "United States", - "children": [ - { - "!create_or_update:name": "US-East-1", - "sites": [ - {"!create_or_update:name": "IAD5", "status__name": "Active", "!ref": "iad5"}, - {"!create_or_update:name": "LGA1", "status__name": "Active", "!ref": "lga1"} - ] - }, - { - "!create_or_update:name": "US-West-1", - "sites": [ - {"!create_or_update:name": "LAX11", "status__name": "Active", "!ref": "lax11"}, - {"!create_or_update:name": "SEA1", "status__name": "Active", "!ref": "sea1"} - ] - } - ] - } - ] - }, - "devices": [ - { - "!create_or_update:name": "core0.iad5", - "site": "!ref:iad5", - "device_type__slug": "ptx10016", - "device_role__slug": "core_router", - "status__name": "Planned" - }, - { - "!create_or_update:name": "core0.lga1", - "site": "!ref:lga1", - "device_type__slug": "ptx10016", - "device_role__slug": "core_router", - "status__name": "Planned" - }, - { - "!create_or_update:name": "core0.lax11", - "site": "!ref:lax11", - "device_type__slug": "ptx10016", - "device_role__slug": "core_router", - "status__name": "Planned" - }, - { - "!create_or_update:name": "core0.sea1", - "site": "!ref:sea1", - "device_type__slug": "ptx10016", - "device_role__slug": "core_router", - "status__name": "Planned" - }, - { - "!create_or_update:name": "core1.iad5", - "site": "!ref:iad5", - "device_type__slug": "ptx10016", - "device_role__slug": "core_router", - "status__name": "Planned" - }, - { - "!create_or_update:name": "core1.lga1", - "site": "!ref:lga1", - "device_type__slug": "ptx10016", - "device_role__slug": "core_router", - "status__name": "Planned" - }, - { - "!create_or_update:name": "core1.lax11", - "site": "!ref:lax11", - "device_type__slug": "ptx10016", - "device_role__slug": "core_router", - "status__name": "Planned" - }, - { - "!create_or_update:name": "core1.sea1", - "site": "!ref:sea1", - "device_type__slug": "ptx10016", - "device_role__slug": "core_router", - "status__name": "Planned" - } - ] -} diff --git a/nautobot_design_builder/tests/testdata_reduce/test2/goal_design.json b/nautobot_design_builder/tests/testdata_reduce/test2/goal_design.json deleted file mode 100644 index c5a0ce65..00000000 --- a/nautobot_design_builder/tests/testdata_reduce/test2/goal_design.json +++ /dev/null @@ -1,115 +0,0 @@ -{ - "manufacturers": [], - "device_types": [], - "device_roles": [], - "regions": { - "children": [ - { - "children": [ - { - "sites": [ - { - "!ref": "iad5", - "status__name": "Active", - "nautobot_identifier": "a45b4b25-1e78-4c7b-95ad-b2880143cc19", - "!create_or_update:name": "IAD5" - }, - { - "!ref": "lga1", - "status__name": "Active", - "nautobot_identifier": "70232953-55f0-41c9-b5bb-bc23d6d88906", - "!create_or_update:name": "LGA1" - } - ], - "nautobot_identifier": "76a1c915-7b30-426b-adef-9721fb768fce", - "!create_or_update:name": "US-East-1" - }, - { - "sites": [ - { - "!ref": "lax11", - "status__name": "Active", - "nautobot_identifier": "5482d5c6-e4f7-4577-b3c0-50a396872f14", - "!create_or_update:name": "LAX11" - }, - { - "!ref": "sea1", - "status__name": "Active", - "nautobot_identifier": "618d24ac-6ccf-4f86-a0bd-c3e816cc9919", - "!create_or_update:name": "SEA1" - } - ], - "nautobot_identifier": "28a13a4a-9b08-4407-b407-c094d19eaf68", - "!create_or_update:name": "US-West-1" - } - ], - "nautobot_identifier": "aa1db811-16d8-4a56-ab59-b23bf7b920aa", - "!create_or_update:name": "United States" - } - ], - "nautobot_identifier": "d982ed3b-66ae-4aca-bc6e-0215f57f3b9c", - "!create_or_update:name": "Americas" - }, - "devices": [ - { - "site": "!ref:iad5", - "status__name": "Planned", - "device_role__slug": "core_router", - "device_type__slug": "ptx10016", - "nautobot_identifier": "ff4bb89f-972b-4b86-9055-a6a8291703b0", - "!create_or_update:name": "core0.iad5" - }, - { - "site": "!ref:lga1", - "status__name": "Planned", - "device_role__slug": "core_router", - "device_type__slug": "ptx10016", - "nautobot_identifier": "d2c289ed-e1c2-4643-a5bc-0768fa037b2d", - "!create_or_update:name": "core0.lga1" - }, - { - "site": "!ref:lax11", - "status__name": "Planned", - "device_role__slug": "core_router", - "device_type__slug": "ptx10016", - "nautobot_identifier": "503452bf-54b1-472b-846e-dc0bb5b42f67", - "!create_or_update:name": "core0.lax11" - }, - { - "site": "!ref:sea1", - "status__name": "Planned", - "device_role__slug": "core_router", - "device_type__slug": "ptx10016", - "nautobot_identifier": "d5b6ae22-c32c-4722-a350-254ff2caad18", - "!create_or_update:name": "core0.sea1" - }, - { - "!create_or_update:name": "core1.iad5", - "site": "!ref:iad5", - "device_type__slug": "ptx10016", - "device_role__slug": "core_router", - "status__name": "Planned" - }, - { - "!create_or_update:name": "core1.lga1", - "site": "!ref:lga1", - "device_type__slug": "ptx10016", - "device_role__slug": "core_router", - "status__name": "Planned" - }, - { - "!create_or_update:name": "core1.lax11", - "site": "!ref:lax11", - "device_type__slug": "ptx10016", - "device_role__slug": "core_router", - "status__name": "Planned" - }, - { - "!create_or_update:name": "core1.sea1", - "site": "!ref:sea1", - "device_type__slug": "ptx10016", - "device_role__slug": "core_router", - "status__name": "Planned" - } - ] -} diff --git a/nautobot_design_builder/tests/testdata_reduce/test2/goal_elements_to_be_decommissioned.json b/nautobot_design_builder/tests/testdata_reduce/test2/goal_elements_to_be_decommissioned.json deleted file mode 100644 index 0967ef42..00000000 --- a/nautobot_design_builder/tests/testdata_reduce/test2/goal_elements_to_be_decommissioned.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/nautobot_design_builder/tests/testdata_reduce/test2/previous_design.json b/nautobot_design_builder/tests/testdata_reduce/test2/previous_design.json deleted file mode 100644 index 964be256..00000000 --- a/nautobot_design_builder/tests/testdata_reduce/test2/previous_design.json +++ /dev/null @@ -1,108 +0,0 @@ -{ - "devices": [ - { - "site": "!ref:iad5", - "status__name": "Planned", - "device_role__slug": "core_router", - "device_type__slug": "ptx10016", - "nautobot_identifier": "ff4bb89f-972b-4b86-9055-a6a8291703b0", - "!create_or_update:name": "core0.iad5" - }, - { - "site": "!ref:lga1", - "status__name": "Planned", - "device_role__slug": "core_router", - "device_type__slug": "ptx10016", - "nautobot_identifier": "d2c289ed-e1c2-4643-a5bc-0768fa037b2d", - "!create_or_update:name": "core0.lga1" - }, - { - "site": "!ref:lax11", - "status__name": "Planned", - "device_role__slug": "core_router", - "device_type__slug": "ptx10016", - "nautobot_identifier": "503452bf-54b1-472b-846e-dc0bb5b42f67", - "!create_or_update:name": "core0.lax11" - }, - { - "site": "!ref:sea1", - "status__name": "Planned", - "device_role__slug": "core_router", - "device_type__slug": "ptx10016", - "nautobot_identifier": "d5b6ae22-c32c-4722-a350-254ff2caad18", - "!create_or_update:name": "core0.sea1" - } - ], - "regions": { - "children": [ - { - "children": [ - { - "sites": [ - { - "!ref": "iad5", - "status__name": "Active", - "nautobot_identifier": "a45b4b25-1e78-4c7b-95ad-b2880143cc19", - "!create_or_update:name": "IAD5" - }, - { - "!ref": "lga1", - "status__name": "Active", - "nautobot_identifier": "70232953-55f0-41c9-b5bb-bc23d6d88906", - "!create_or_update:name": "LGA1" - } - ], - "nautobot_identifier": "76a1c915-7b30-426b-adef-9721fb768fce", - "!create_or_update:name": "US-East-1" - }, - { - "sites": [ - { - "!ref": "lax11", - "status__name": "Active", - "nautobot_identifier": "5482d5c6-e4f7-4577-b3c0-50a396872f14", - "!create_or_update:name": "LAX11" - }, - { - "!ref": "sea1", - "status__name": "Active", - "nautobot_identifier": "618d24ac-6ccf-4f86-a0bd-c3e816cc9919", - "!create_or_update:name": "SEA1" - } - ], - "nautobot_identifier": "28a13a4a-9b08-4407-b407-c094d19eaf68", - "!create_or_update:name": "US-West-1" - } - ], - "nautobot_identifier": "aa1db811-16d8-4a56-ab59-b23bf7b920aa", - "!create_or_update:name": "United States" - } - ], - "nautobot_identifier": "d982ed3b-66ae-4aca-bc6e-0215f57f3b9c", - "!create_or_update:name": "Americas" - }, - "device_roles": [ - { - "slug": "core_router", - "color": "3f51b5", - "nautobot_identifier": "7f0e8caf-4c3d-4348-8576-ce8bfa0d6a9e", - "!create_or_update:name": "Core Router" - } - ], - "device_types": [ - { - "slug": "ptx10016", - "u_height": 21, - "manufacturer__slug": "juniper", - "nautobot_identifier": "44c91fff-548a-401e-8a26-24350ddeff66", - "!create_or_update:model": "PTX10016" - } - ], - "manufacturers": [ - { - "slug": "juniper", - "nautobot_identifier": "e763f36f-ce4b-4096-b160-5d03cd8f8915", - "!create_or_update:name": "Juniper" - } - ] -} diff --git a/nautobot_design_builder/tests/testdata_reduce/test3/design.json b/nautobot_design_builder/tests/testdata_reduce/test3/design.json deleted file mode 100644 index 11bef5b6..00000000 --- a/nautobot_design_builder/tests/testdata_reduce/test3/design.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "vrfs": [{"!create_or_update:name": "64501:2", "description": "VRF for customer xyz", "!ref": "my_vrf"}], - "prefixes": [ - {"!create_or_update:prefix": "192.0.2.0/24", "status__name": "Reserved"}, - { - "!create_or_update:prefix": "192.0.2.0/30", - "status__name": "Reserved", - "vrf": "!ref:my_vrf", - "description": "ertewr" - } - ], - "devices": [ - { - "!update:name": "core0.sea1", - "local_context_data": {"mpls_router": true}, - "interfaces": [ - { - "!create_or_update:name": "GigabitEthernet1/1", - "status__name": "Planned", - "type": "other", - "description": "ertewr", - "ip_addresses": [{"!create_or_update:address": "192.0.2.1/30", "status__name": "Reserved"}] - } - ] - }, - { - "!update:name": "core0.iad5", - "local_context_data": {"mpls_router": true}, - "interfaces": [ - { - "!create_or_update:name": "GigabitEthernet1/1", - "status__name": "Planned", - "type": "other", - "description": "ertewr", - "ip_addresses": [{"!create_or_update:address": "192.0.2.2/30", "status__name": "Reserved"}] - } - ] - } - ] -} diff --git a/nautobot_design_builder/tests/testdata_reduce/test3/goal_design.json b/nautobot_design_builder/tests/testdata_reduce/test3/goal_design.json deleted file mode 100644 index 1bed24b4..00000000 --- a/nautobot_design_builder/tests/testdata_reduce/test3/goal_design.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "vrfs": [{"!create_or_update:name": "64501:2", "description": "VRF for customer xyz", "!ref": "my_vrf"}], - "prefixes": [ - { - "vrf": "!ref:my_vrf", - "description": "ertewr", - "status__name": "Reserved", - "nautobot_identifier": "180df48c-7c39-4da2-ac18-6f335cbd2a0e", - "!create_or_update:prefix": "192.0.2.0/30" - } - ], - "devices": [ - { - "!update:name": "core0.sea1", - "local_context_data": {"mpls_router": true}, - "interfaces": [ - { - "!create_or_update:name": "GigabitEthernet1/1", - "status__name": "Planned", - "type": "other", - "description": "ertewr", - "ip_addresses": [{"!create_or_update:address": "192.0.2.1/30", "status__name": "Reserved"}] - } - ] - } - ] -} diff --git a/nautobot_design_builder/tests/testdata_reduce/test3/goal_elements_to_be_decommissioned.json b/nautobot_design_builder/tests/testdata_reduce/test3/goal_elements_to_be_decommissioned.json deleted file mode 100644 index 07848121..00000000 --- a/nautobot_design_builder/tests/testdata_reduce/test3/goal_elements_to_be_decommissioned.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "vrfs": [["d34f89aa-0199-4352-aa2f-311203bae138", "64501:1"]], - "devices": [["c8454078-d3d7-4243-a07f-99750d06c595", "core0.lax11"]], - "interfaces": [["0d95bbfc-4438-42e8-b24c-d5d878dd0880", "GigabitEthernet1/1"]], - "ip_addresses": [["ceaabdd5-811a-4981-aa83-c2c2ff52b081", "192.0.2.1/30"]] -} diff --git a/nautobot_design_builder/tests/testdata_reduce/test3/previous_design.json b/nautobot_design_builder/tests/testdata_reduce/test3/previous_design.json deleted file mode 100644 index a55ef3e8..00000000 --- a/nautobot_design_builder/tests/testdata_reduce/test3/previous_design.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "vrfs": [ - { - "!ref": "my_vrf", - "description": "VRF for customer abc", - "nautobot_identifier": "d34f89aa-0199-4352-aa2f-311203bae138", - "!create_or_update:name": "64501:1" - } - ], - "devices": [ - { - "interfaces": [ - { - "type": "other", - "description": "ertewr", - "ip_addresses": [ - { - "status__name": "Reserved", - "nautobot_identifier": "ceaabdd5-811a-4981-aa83-c2c2ff52b081", - "!create_or_update:address": "192.0.2.1/30" - } - ], - "status__name": "Planned", - "nautobot_identifier": "0d95bbfc-4438-42e8-b24c-d5d878dd0880", - "!create_or_update:name": "GigabitEthernet1/1" - } - ], - "!update:name": "core0.lax11", - "local_context_data": {"mpls_router": true}, - "nautobot_identifier": "c8454078-d3d7-4243-a07f-99750d06c595" - }, - { - "interfaces": [ - { - "type": "other", - "description": "ertewr", - "ip_addresses": [ - { - "status__name": "Reserved", - "nautobot_identifier": "bb27bc76-2973-42db-8e6d-5f79e1aecf92", - "!create_or_update:address": "192.0.2.2/30" - } - ], - "status__name": "Planned", - "nautobot_identifier": "4506fe8d-71a9-445e-9bf6-7127e84a3d22", - "!create_or_update:name": "GigabitEthernet1/1" - } - ], - "!update:name": "core0.iad5", - "local_context_data": {"mpls_router": true}, - "nautobot_identifier": "d14133b0-85dd-440b-99e8-4410078df052" - } - ], - "prefixes": [ - { - "status__name": "Reserved", - "nautobot_identifier": "22a1b725-a371-4130-8b2b-6b95b9b913ae", - "!create_or_update:prefix": "192.0.2.0/24" - }, - { - "vrf": "!ref:my_vrf", - "description": "ertewr", - "status__name": "Reserved", - "nautobot_identifier": "180df48c-7c39-4da2-ac18-6f335cbd2a0e", - "!create_or_update:prefix": "192.0.2.0/30" - } - ] -} diff --git a/nautobot_design_builder/tests/testdata_reduce/test4/design.json b/nautobot_design_builder/tests/testdata_reduce/test4/design.json deleted file mode 100644 index 9bdf7b04..00000000 --- a/nautobot_design_builder/tests/testdata_reduce/test4/design.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "manufacturers": [{"!create_or_update:name": "Juniper", "slug": "juniper"}], - "device_types": [ - { - "!create_or_update:model": "PTX10016", - "slug": "ptx10016", - "manufacturer__slug": "juniper", - "u_height": 21 - } - ], - "device_roles": [{"!create_or_update:name": "Core Router", "slug": "core_router", "color": "3f51b5"}], - "regions": { - "!create_or_update:name": "Americas", - "children": [ - { - "!create_or_update:name": "United States", - "children": [ - { - "!create_or_update:name": "US-East-1", - "sites": [ - {"!create_or_update:name": "IAD5", "status__name": "Active", "!ref": "iad5"}, - {"!create_or_update:name": "LGA1", "status__name": "Active", "!ref": "lga1"} - ] - }, - { - "!create_or_update:name": "US-West-1", - "sites": [ - {"!create_or_update:name": "LAX11", "status__name": "Active", "!ref": "lax11"}, - {"!create_or_update:name": "SEA1", "status__name": "Active", "!ref": "sea1"} - ] - } - ] - } - ] - }, - "devices": [ - { - "!create_or_update:name": "core0.iad5", - "site": "!ref:iad5", - "device_type__slug": "ptx10016", - "device_role__slug": "core_router", - "status__name": "Planned" - }, - { - "!create_or_update:name": "core0.lga1", - "site": "!ref:lga1", - "device_type__slug": "ptx10016", - "device_role__slug": "core_router", - "status__name": "Planned" - }, - { - "!create_or_update:name": "core0.lax11", - "site": "!ref:lax11", - "device_type__slug": "ptx10016", - "device_role__slug": "core_router", - "status__name": "Planned" - }, - { - "!create_or_update:name": "core0.sea1", - "site": "!ref:sea1", - "device_type__slug": "ptx10016", - "device_role__slug": "core_router", - "status__name": "Planned" - } - ] -} diff --git a/nautobot_design_builder/tests/testdata_reduce/test4/goal_design.json b/nautobot_design_builder/tests/testdata_reduce/test4/goal_design.json deleted file mode 100644 index ed5165e4..00000000 --- a/nautobot_design_builder/tests/testdata_reduce/test4/goal_design.json +++ /dev/null @@ -1,87 +0,0 @@ -{ - "manufacturers": [], - "device_types": [], - "device_roles": [], - "regions": { - "!create_or_update:name": "Americas", - "children": [ - { - "!create_or_update:name": "United States", - "children": [ - { - "!create_or_update:name": "US-East-1", - "sites": [ - { - "!create_or_update:name": "IAD5", - "status__name": "Active", - "!ref": "iad5", - "nautobot_identifier": "cf3c08fe-11b7-45b0-9aab-09f8df7bfc89" - }, - { - "!create_or_update:name": "LGA1", - "status__name": "Active", - "!ref": "lga1", - "nautobot_identifier": "4eef1fe2-d519-4c9d-ad45-feb04cdcba57" - } - ], - "nautobot_identifier": "0a43260d-0a95-4f2e-93d0-3ecef49069ef" - }, - { - "!create_or_update:name": "US-West-1", - "sites": [ - { - "!create_or_update:name": "LAX11", - "status__name": "Active", - "!ref": "lax11", - "nautobot_identifier": "8d1ed8a1-b503-49e5-99f4-20140f7cd255" - }, - { - "!create_or_update:name": "SEA1", - "status__name": "Active", - "!ref": "sea1", - "nautobot_identifier": "6118a8a4-332a-4b04-a0d6-57170ee0e475" - } - ], - "nautobot_identifier": "2889485e-6222-4634-9f86-bff0afd90939" - } - ], - "nautobot_identifier": "da9b46cd-1fc1-4d7b-b5e2-cf382df02b3b" - } - ], - "nautobot_identifier": "e7540dd8-7079-4b25-ad10-8681dd64a69f" - }, - "devices": [ - { - "!create_or_update:name": "core0.iad5", - "site": "!ref:iad5", - "device_type__slug": "ptx10016", - "device_role__slug": "core_router", - "status__name": "Planned", - "nautobot_identifier": "7d90ac27-3444-4c48-9669-4745c0fe4ffa" - }, - { - "!create_or_update:name": "core0.lga1", - "site": "!ref:lga1", - "device_type__slug": "ptx10016", - "device_role__slug": "core_router", - "status__name": "Planned", - "nautobot_identifier": "0a9382a4-6cb0-4fa7-834a-0ea9fba1a825" - }, - { - "!create_or_update:name": "core0.lax11", - "site": "!ref:lax11", - "device_type__slug": "ptx10016", - "device_role__slug": "core_router", - "status__name": "Planned", - "nautobot_identifier": "2d3c1d1a-df00-4f0e-bc3c-8899f12ab2cd" - }, - { - "!create_or_update:name": "core0.sea1", - "site": "!ref:sea1", - "device_type__slug": "ptx10016", - "device_role__slug": "core_router", - "status__name": "Planned", - "nautobot_identifier": "faa7b89b-a0da-4516-8c75-6d485288f08d" - } - ] -} diff --git a/nautobot_design_builder/tests/testdata_reduce/test4/goal_elements_to_be_decommissioned.json b/nautobot_design_builder/tests/testdata_reduce/test4/goal_elements_to_be_decommissioned.json deleted file mode 100644 index 781a29e7..00000000 --- a/nautobot_design_builder/tests/testdata_reduce/test4/goal_elements_to_be_decommissioned.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "devices": [ - ["6bb2e900-b53d-43df-9a88-048ab7c05bd0", "core1.iad5"], - ["d96aadd6-489c-41e6-b9eb-7f3dc7e7c197", "core1.lga1"], - ["7ecaca00-65e0-4214-a89d-8560002c4e87", "core1.lax11"], - ["dd3811ad-158e-464e-8629-0a3cd18aabf0", "core1.sea1"] - ] -} diff --git a/nautobot_design_builder/tests/testdata_reduce/test4/previous_design.json b/nautobot_design_builder/tests/testdata_reduce/test4/previous_design.json deleted file mode 100644 index c9777f8e..00000000 --- a/nautobot_design_builder/tests/testdata_reduce/test4/previous_design.json +++ /dev/null @@ -1,140 +0,0 @@ -{ - "devices": [ - { - "site": "!ref:iad5", - "status__name": "Planned", - "device_role__slug": "core_router", - "device_type__slug": "ptx10016", - "nautobot_identifier": "7d90ac27-3444-4c48-9669-4745c0fe4ffa", - "!create_or_update:name": "core0.iad5" - }, - { - "site": "!ref:lga1", - "status__name": "Planned", - "device_role__slug": "core_router", - "device_type__slug": "ptx10016", - "nautobot_identifier": "0a9382a4-6cb0-4fa7-834a-0ea9fba1a825", - "!create_or_update:name": "core0.lga1" - }, - { - "site": "!ref:lax11", - "status__name": "Planned", - "device_role__slug": "core_router", - "device_type__slug": "ptx10016", - "nautobot_identifier": "2d3c1d1a-df00-4f0e-bc3c-8899f12ab2cd", - "!create_or_update:name": "core0.lax11" - }, - { - "site": "!ref:sea1", - "status__name": "Planned", - "device_role__slug": "core_router", - "device_type__slug": "ptx10016", - "nautobot_identifier": "faa7b89b-a0da-4516-8c75-6d485288f08d", - "!create_or_update:name": "core0.sea1" - }, - { - "site": "!ref:iad5", - "status__name": "Planned", - "device_role__slug": "core_router", - "device_type__slug": "ptx10016", - "nautobot_identifier": "6bb2e900-b53d-43df-9a88-048ab7c05bd0", - "!create_or_update:name": "core1.iad5" - }, - { - "site": "!ref:lga1", - "status__name": "Planned", - "device_role__slug": "core_router", - "device_type__slug": "ptx10016", - "nautobot_identifier": "d96aadd6-489c-41e6-b9eb-7f3dc7e7c197", - "!create_or_update:name": "core1.lga1" - }, - { - "site": "!ref:lax11", - "status__name": "Planned", - "device_role__slug": "core_router", - "device_type__slug": "ptx10016", - "nautobot_identifier": "7ecaca00-65e0-4214-a89d-8560002c4e87", - "!create_or_update:name": "core1.lax11" - }, - { - "site": "!ref:sea1", - "status__name": "Planned", - "device_role__slug": "core_router", - "device_type__slug": "ptx10016", - "nautobot_identifier": "dd3811ad-158e-464e-8629-0a3cd18aabf0", - "!create_or_update:name": "core1.sea1" - } - ], - "regions": { - "children": [ - { - "children": [ - { - "sites": [ - { - "!ref": "iad5", - "status__name": "Active", - "nautobot_identifier": "cf3c08fe-11b7-45b0-9aab-09f8df7bfc89", - "!create_or_update:name": "IAD5" - }, - { - "!ref": "lga1", - "status__name": "Active", - "nautobot_identifier": "4eef1fe2-d519-4c9d-ad45-feb04cdcba57", - "!create_or_update:name": "LGA1" - } - ], - "nautobot_identifier": "0a43260d-0a95-4f2e-93d0-3ecef49069ef", - "!create_or_update:name": "US-East-1" - }, - { - "sites": [ - { - "!ref": "lax11", - "status__name": "Active", - "nautobot_identifier": "8d1ed8a1-b503-49e5-99f4-20140f7cd255", - "!create_or_update:name": "LAX11" - }, - { - "!ref": "sea1", - "status__name": "Active", - "nautobot_identifier": "6118a8a4-332a-4b04-a0d6-57170ee0e475", - "!create_or_update:name": "SEA1" - } - ], - "nautobot_identifier": "2889485e-6222-4634-9f86-bff0afd90939", - "!create_or_update:name": "US-West-1" - } - ], - "nautobot_identifier": "da9b46cd-1fc1-4d7b-b5e2-cf382df02b3b", - "!create_or_update:name": "United States" - } - ], - "nautobot_identifier": "e7540dd8-7079-4b25-ad10-8681dd64a69f", - "!create_or_update:name": "Americas" - }, - "device_roles": [ - { - "slug": "core_router", - "color": "3f51b5", - "nautobot_identifier": "d121e76b-3882-4224-8087-c41d38ef2257", - "!create_or_update:name": "Core Router" - } - ], - "device_types": [ - { - "slug": "ptx10016", - "u_height": 21, - "manufacturer__slug": "juniper", - "nautobot_identifier": "44f11fae-b5d2-480f-a8e0-36a3ff06f09a", - "!create_or_update:model": "PTX10016" - } - ], - "manufacturers": [ - { - "slug": "juniper", - "nautobot_identifier": "f50e67d8-1d31-4ec7-a59e-2435cda9870b", - "!create_or_update:name": "Juniper" - } - ] -} diff --git a/nautobot_design_builder/tests/testdata_reduce/test5/design.json b/nautobot_design_builder/tests/testdata_reduce/test5/design.json deleted file mode 100644 index 3ef1ae1e..00000000 --- a/nautobot_design_builder/tests/testdata_reduce/test5/design.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "vrfs": [{"!create_or_update:name": "64501:1", "description": "VRF for customer abc", "!ref": "my_vrf"}], - "prefixes": [ - {"!create_or_update:prefix": "192.0.2.0/24", "status__name": "Reserved"}, - { - "!create_or_update:prefix": "192.0.2.0/30", - "status__name": "Reserved", - "vrf": "!ref:my_vrf", - "description": "sadfasd" - } - ], - "devices": [ - { - "!update:name": "core1.lax11", - "local_context_data": {"mpls_router": true}, - "interfaces": [ - { - "!create_or_update:name": "GigabitEthernet1/1", - "status__name": "Planned", - "type": "other", - "description": "sadfasd", - "ip_addresses": [{"!create_or_update:address": "192.0.2.1/30", "status__name": "Reserved"}] - } - ] - }, - { - "!update:name": "core0.lax11", - "local_context_data": {"mpls_router": true}, - "interfaces": [ - { - "!create_or_update:name": "GigabitEthernet1/1", - "status__name": "Planned", - "type": "other", - "description": "sadfasd", - "!connect_cable": { - "status__name": "Planned", - "to": {"device__name": "core1.lax11", "name": "GigabitEthernet1/1"} - }, - "ip_addresses": [{"!create_or_update:address": "192.0.2.2/30", "status__name": "Reserved"}] - } - ] - } - ] -} diff --git a/nautobot_design_builder/tests/testdata_reduce/test5/goal_design.json b/nautobot_design_builder/tests/testdata_reduce/test5/goal_design.json deleted file mode 100644 index 9fd187ee..00000000 --- a/nautobot_design_builder/tests/testdata_reduce/test5/goal_design.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "vrfs": [ - { - "!create_or_update:name": "64501:1", - "description": "VRF for customer abc", - "!ref": "my_vrf", - "nautobot_identifier": "4757e7e5-2362-4199-adee-20cfa1a5b2fc" - } - ], - "prefixes": [ - { - "!create_or_update:prefix": "192.0.2.0/30", - "status__name": "Reserved", - "vrf": "!ref:my_vrf", - "description": "sadfasd", - "nautobot_identifier": "05540529-6ade-417c-88af-a9b1f4ae75f7" - } - ], - "devices": [ - { - "!update:name": "core0.lax11", - "local_context_data": {"mpls_router": true}, - "interfaces": [ - { - "!create_or_update:name": "GigabitEthernet1/1", - "status__name": "Planned", - "type": "other", - "description": "sadfasd", - "!connect_cable": { - "nautobot_identifier": "36f26409-5d65-4b50-8934-111f9aafa9ec", - "status__name": "Planned", - "to": {"device__name": "core1.lax11", "name": "GigabitEthernet1/1"} - }, - "ip_addresses": [{"!create_or_update:address": "192.0.2.2/30", "status__name": "Reserved"}] - } - ] - } - ] -} diff --git a/nautobot_design_builder/tests/testdata_reduce/test5/goal_elements_to_be_decommissioned.json b/nautobot_design_builder/tests/testdata_reduce/test5/goal_elements_to_be_decommissioned.json deleted file mode 100644 index 9d1fd8ff..00000000 --- a/nautobot_design_builder/tests/testdata_reduce/test5/goal_elements_to_be_decommissioned.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "interfaces": [["30b6689c-8ca6-47d0-8dbe-9c1d300860a6", "GigabitEthernet1/1"]], - "ip_addresses": [["053289c3-1469-4682-9b95-9e499b8563fb", "192.0.2.2/30"]], - "devices": [["a46729d6-6e71-4905-9833-24dd7841f98a", "core0.iad5"]] -} diff --git a/nautobot_design_builder/tests/testdata_reduce/test5/previous_design.json b/nautobot_design_builder/tests/testdata_reduce/test5/previous_design.json deleted file mode 100644 index 21f40113..00000000 --- a/nautobot_design_builder/tests/testdata_reduce/test5/previous_design.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "vrfs": [ - { - "!ref": "my_vrf", - "description": "VRF for customer abc", - "nautobot_identifier": "4757e7e5-2362-4199-adee-20cfa1a5b2fc", - "!create_or_update:name": "64501:1" - } - ], - "devices": [ - { - "interfaces": [ - { - "type": "other", - "description": "sadfasd", - "ip_addresses": [ - { - "status__name": "Reserved", - "nautobot_identifier": "8f9a5073-2975-4b9a-86d1-ebe54e73ca6c", - "!create_or_update:address": "192.0.2.1/30" - } - ], - "status__name": "Planned", - "nautobot_identifier": "b95378bd-5580-4eeb-9542-c298e8424399", - "!create_or_update:name": "GigabitEthernet1/1" - } - ], - "!update:name": "core1.lax11", - "local_context_data": {"mpls_router": true}, - "nautobot_identifier": "aee92e54-4763-4d76-9390-b3a714931a47" - }, - { - "interfaces": [ - { - "type": "other", - "description": "sadfasd", - "ip_addresses": [ - { - "status__name": "Reserved", - "nautobot_identifier": "053289c3-1469-4682-9b95-9e499b8563fb", - "!create_or_update:address": "192.0.2.2/30" - } - ], - "status__name": "Planned", - "!connect_cable": { - "to": {"name": "GigabitEthernet1/1", "device__name": "core1.lax11"}, - "status__name": "Planned", - "nautobot_identifier": "36f26409-5d65-4b50-8934-111f9aafa9ec" - }, - "nautobot_identifier": "30b6689c-8ca6-47d0-8dbe-9c1d300860a6", - "!create_or_update:name": "GigabitEthernet1/1" - } - ], - "!update:name": "core0.iad5", - "local_context_data": {"mpls_router": true}, - "nautobot_identifier": "a46729d6-6e71-4905-9833-24dd7841f98a" - } - ], - "prefixes": [ - { - "status__name": "Reserved", - "nautobot_identifier": "7909ae9d-02de-4034-9ef9-12e1499bc563", - "!create_or_update:prefix": "192.0.2.0/24" - }, - { - "vrf": "!ref:my_vrf", - "description": "sadfasd", - "status__name": "Reserved", - "nautobot_identifier": "05540529-6ade-417c-88af-a9b1f4ae75f7", - "!create_or_update:prefix": "192.0.2.0/30" - } - ] -} diff --git a/nautobot_design_builder/tests/util.py b/nautobot_design_builder/tests/util.py index 4a769127..92344e46 100644 --- a/nautobot_design_builder/tests/util.py +++ b/nautobot_design_builder/tests/util.py @@ -7,6 +7,18 @@ from nautobot_design_builder.models import Design, DesignInstance, Journal, JournalEntry +def populate_sample_data(): + """Populate the database with some sample data.""" + job = Job.objects.get(name="Initial Data") + job_result, _ = JobResult.objects.get_or_create( + name="Test", obj_type=ContentType.objects.get_for_model(Job), job_id=job.pk + ) + + design, _ = Design.objects.get_or_create(job=job) + design_instance, _ = DesignInstance.objects.get_or_create(design=design, name="Initial Data") + Journal.objects.get_or_create(design_instance=design_instance, job_result=job_result) + + def create_test_view_data(): """Creates test data for view and API view test cases.""" for i in range(1, 4): @@ -22,4 +34,6 @@ def create_test_view_data(): instance = DesignInstance.objects.create(design=design, name=f"Test Instance {i}") journal = Journal.objects.create(design_instance=instance, job_result=job_result) full_control = i == 1 # Have one record where full control is given, more than one where its not. - JournalEntry.objects.create(journal=journal, design_object=object_created_by_job, full_control=full_control) + JournalEntry.objects.create( + journal=journal, design_object=object_created_by_job, full_control=full_control, index=0 + ) diff --git a/nautobot_design_builder/util.py b/nautobot_design_builder/util.py index 2d614148..acccff90 100644 --- a/nautobot_design_builder/util.py +++ b/nautobot_design_builder/util.py @@ -325,34 +325,6 @@ def get_design_class(path: str, module_name: str, class_name: str) -> Type["Desi return getattr(module, class_name) -def custom_delete_order(key: str) -> int: - """Helper function to customize the order to decommission objects following Nautobot data model. - - Args: - key (str): key to evaluate. - - Returns: - (int): represents the ordering . - """ - ordered_list = [ - "tags", - "ip_addresses", - "prefixes", - "vrf", - "inventoryitems", - "interfaces", - "devices", - "racks", - "locations", - "sites", - "regions", - ] - if key in ordered_list: - return ordered_list.index(key) - # If not covered, return the lowest - return 0 - - # TODO: this is only available in Nautobot 2.x, recreating it here to reuse for Nautobot 1.x def get_changes_for_model(model): """Return a queryset of ObjectChanges for a model or instance. diff --git a/nautobot_design_builder/views.py b/nautobot_design_builder/views.py index 1f6a096e..a759fe98 100644 --- a/nautobot_design_builder/views.py +++ b/nautobot_design_builder/views.py @@ -155,7 +155,7 @@ def get_extra_context(self, request, instance=None): """Extend UI.""" context = super().get_extra_context(request, instance) if self.action == "retrieve": - entries = JournalEntry.objects.restrict(request.user, "view").filter(journal=instance) + entries = JournalEntry.objects.restrict(request.user, "view").filter(journal=instance).order_by("-index") entries_table = JournalEntryTable(entries) entries_table.columns.hide("journal") diff --git a/notes.txt b/notes.txt new file mode 100644 index 00000000..e69de29b diff --git a/poetry.lock b/poetry.lock old mode 100755 new mode 100644 index 1e392319..2a9e1d78 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "amqp" @@ -38,13 +38,13 @@ files = [ [[package]] name = "asgiref" -version = "3.7.2" +version = "3.8.1" description = "ASGI specs, helper code, and adapters" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "asgiref-3.7.2-py3-none-any.whl", hash = "sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e"}, - {file = "asgiref-3.7.2.tar.gz", hash = "sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed"}, + {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, + {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, ] [package.dependencies] @@ -55,22 +55,17 @@ tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] [[package]] name = "astroid" -version = "2.15.8" +version = "3.1.0" description = "An abstract syntax tree for Python with inference support." optional = false -python-versions = ">=3.7.2" +python-versions = ">=3.8.0" files = [ - {file = "astroid-2.15.8-py3-none-any.whl", hash = "sha256:1aa149fc5c6589e3d0ece885b4491acd80af4f087baafa3fb5203b113e68cd3c"}, - {file = "astroid-2.15.8.tar.gz", hash = "sha256:6c107453dffee9055899705de3c9ead36e74119cee151e5a9aaf7f0b0e020a6a"}, + {file = "astroid-3.1.0-py3-none-any.whl", hash = "sha256:951798f922990137ac090c53af473db7ab4e70c770e6d7fae0cec59f74411819"}, + {file = "astroid-3.1.0.tar.gz", hash = "sha256:ac248253bfa4bd924a0de213707e7ebeeb3138abeb48d798784ead1e56d419d4"}, ] [package.dependencies] -lazy-object-proxy = ">=1.4.0" typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} -wrapt = [ - {version = ">=1.11,<2", markers = "python_version < \"3.11\""}, - {version = ">=1.14,<2", markers = "python_version >= \"3.11\""}, -] [[package]] name = "asttokens" @@ -90,6 +85,21 @@ six = ">=1.12.0" astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"] test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] +[[package]] +name = "astunparse" +version = "1.6.3" +description = "An AST unparser for Python" +optional = false +python-versions = "*" +files = [ + {file = "astunparse-1.6.3-py2.py3-none-any.whl", hash = "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8"}, + {file = "astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872"}, +] + +[package.dependencies] +six = ">=1.6.1,<2.0" +wheel = ">=0.23.0,<1.0" + [[package]] name = "async-timeout" version = "4.0.3" @@ -122,18 +132,18 @@ tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "p [[package]] name = "autopep8" -version = "2.0.0" +version = "2.1.0" description = "A tool that automatically formats Python code to conform to the PEP 8 style guide" optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "autopep8-2.0.0-py2.py3-none-any.whl", hash = "sha256:ad924b42c2e27a1ac58e432166cc4588f5b80747de02d0d35b1ecbd3e7d57207"}, - {file = "autopep8-2.0.0.tar.gz", hash = "sha256:8b1659c7f003e693199f52caffdc06585bb0716900bbc6a7442fd931d658c077"}, + {file = "autopep8-2.1.0-py2.py3-none-any.whl", hash = "sha256:2bb76888c5edbcafe6aabab3c47ba534f5a2c2d245c2eddced4a30c4b4946357"}, + {file = "autopep8-2.1.0.tar.gz", hash = "sha256:1fa8964e4618929488f4ec36795c7ff12924a68b8bf01366c094fc52f770b6e7"}, ] [package.dependencies] -pycodestyle = ">=2.9.1" -tomli = "*" +pycodestyle = ">=2.11.0" +tomli = {version = "*", markers = "python_version < \"3.11\""} [[package]] name = "babel" @@ -196,13 +206,13 @@ tzdata = ["tzdata"] [[package]] name = "bandit" -version = "1.7.7" +version = "1.7.8" description = "Security oriented static analyser for python code." optional = false python-versions = ">=3.8" files = [ - {file = "bandit-1.7.7-py3-none-any.whl", hash = "sha256:17e60786a7ea3c9ec84569fd5aee09936d116cb0cb43151023258340dbffb7ed"}, - {file = "bandit-1.7.7.tar.gz", hash = "sha256:527906bec6088cb499aae31bc962864b4e77569e9d529ee51df3a93b4b8ab28a"}, + {file = "bandit-1.7.8-py3-none-any.whl", hash = "sha256:509f7af645bc0cd8fd4587abc1a038fc795636671ee8204d502b933aee44f381"}, + {file = "bandit-1.7.8.tar.gz", hash = "sha256:36de50f720856ab24a24dbaa5fee2c66050ed97c1477e0a1159deab1775eab6b"}, ] [package.dependencies] @@ -213,6 +223,7 @@ stevedore = ">=1.20.0" [package.extras] baseline = ["GitPython (>=3.1.30)"] +sarif = ["jschema-to-python (>=1.2.3)", "sarif-om (>=1.0.4)"] test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "pylint (==1.9.4)", "stestr (>=2.5.0)", "testscenarios (>=0.5.0)", "testtools (>=2.3.0)"] toml = ["tomli (>=1.1.0)"] yaml = ["PyYAML"] @@ -251,33 +262,33 @@ files = [ [[package]] name = "black" -version = "24.2.0" +version = "24.3.0" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" files = [ - {file = "black-24.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6981eae48b3b33399c8757036c7f5d48a535b962a7c2310d19361edeef64ce29"}, - {file = "black-24.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d533d5e3259720fdbc1b37444491b024003e012c5173f7d06825a77508085430"}, - {file = "black-24.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61a0391772490ddfb8a693c067df1ef5227257e72b0e4108482b8d41b5aee13f"}, - {file = "black-24.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:992e451b04667116680cb88f63449267c13e1ad134f30087dec8527242e9862a"}, - {file = "black-24.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:163baf4ef40e6897a2a9b83890e59141cc8c2a98f2dda5080dc15c00ee1e62cd"}, - {file = "black-24.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e37c99f89929af50ffaf912454b3e3b47fd64109659026b678c091a4cd450fb2"}, - {file = "black-24.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9de21bafcba9683853f6c96c2d515e364aee631b178eaa5145fc1c61a3cc92"}, - {file = "black-24.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:9db528bccb9e8e20c08e716b3b09c6bdd64da0dd129b11e160bf082d4642ac23"}, - {file = "black-24.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d84f29eb3ee44859052073b7636533ec995bd0f64e2fb43aeceefc70090e752b"}, - {file = "black-24.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e08fb9a15c914b81dd734ddd7fb10513016e5ce7e6704bdd5e1251ceee51ac9"}, - {file = "black-24.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:810d445ae6069ce64030c78ff6127cd9cd178a9ac3361435708b907d8a04c693"}, - {file = "black-24.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ba15742a13de85e9b8f3239c8f807723991fbfae24bad92d34a2b12e81904982"}, - {file = "black-24.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7e53a8c630f71db01b28cd9602a1ada68c937cbf2c333e6ed041390d6968faf4"}, - {file = "black-24.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:93601c2deb321b4bad8f95df408e3fb3943d85012dddb6121336b8e24a0d1218"}, - {file = "black-24.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0057f800de6acc4407fe75bb147b0c2b5cbb7c3ed110d3e5999cd01184d53b0"}, - {file = "black-24.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:faf2ee02e6612577ba0181f4347bcbcf591eb122f7841ae5ba233d12c39dcb4d"}, - {file = "black-24.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:057c3dc602eaa6fdc451069bd027a1b2635028b575a6c3acfd63193ced20d9c8"}, - {file = "black-24.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08654d0797e65f2423f850fc8e16a0ce50925f9337fb4a4a176a7aa4026e63f8"}, - {file = "black-24.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca610d29415ee1a30a3f30fab7a8f4144e9d34c89a235d81292a1edb2b55f540"}, - {file = "black-24.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:4dd76e9468d5536abd40ffbc7a247f83b2324f0c050556d9c371c2b9a9a95e31"}, - {file = "black-24.2.0-py3-none-any.whl", hash = "sha256:e8a6ae970537e67830776488bca52000eaa37fa63b9988e8c487458d9cd5ace6"}, - {file = "black-24.2.0.tar.gz", hash = "sha256:bce4f25c27c3435e4dace4815bcb2008b87e167e3bf4ee47ccdc5ce906eb4894"}, + {file = "black-24.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395"}, + {file = "black-24.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995"}, + {file = "black-24.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7"}, + {file = "black-24.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0"}, + {file = "black-24.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9"}, + {file = "black-24.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597"}, + {file = "black-24.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d"}, + {file = "black-24.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5"}, + {file = "black-24.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f"}, + {file = "black-24.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11"}, + {file = "black-24.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4"}, + {file = "black-24.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5"}, + {file = "black-24.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837"}, + {file = "black-24.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd"}, + {file = "black-24.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213"}, + {file = "black-24.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959"}, + {file = "black-24.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb"}, + {file = "black-24.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7"}, + {file = "black-24.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7"}, + {file = "black-24.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f"}, + {file = "black-24.3.0-py3-none-any.whl", hash = "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93"}, + {file = "black-24.3.0.tar.gz", hash = "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f"}, ] [package.dependencies] @@ -541,13 +552,13 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "click-didyoumean" -version = "0.3.0" +version = "0.3.1" description = "Enables git-like *did-you-mean* feature in click" optional = false -python-versions = ">=3.6.2,<4.0.0" +python-versions = ">=3.6.2" files = [ - {file = "click-didyoumean-0.3.0.tar.gz", hash = "sha256:f184f0d851d96b6d29297354ed981b7dd71df7ff500d82fa6d11f0856bee8035"}, - {file = "click_didyoumean-0.3.0-py3-none-any.whl", hash = "sha256:a0713dc7a1de3f06bc0df5a9567ad19ead2d3d5689b434768a6145bff77c0667"}, + {file = "click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c"}, + {file = "click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463"}, ] [package.dependencies] @@ -601,63 +612,63 @@ files = [ [[package]] name = "coverage" -version = "7.4.1" +version = "7.4.4" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:077d366e724f24fc02dbfe9d946534357fda71af9764ff99d73c3c596001bbd7"}, - {file = "coverage-7.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0193657651f5399d433c92f8ae264aff31fc1d066deee4b831549526433f3f61"}, - {file = "coverage-7.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d17bbc946f52ca67adf72a5ee783cd7cd3477f8f8796f59b4974a9b59cacc9ee"}, - {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3277f5fa7483c927fe3a7b017b39351610265308f5267ac6d4c2b64cc1d8d25"}, - {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dceb61d40cbfcf45f51e59933c784a50846dc03211054bd76b421a713dcdf19"}, - {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6008adeca04a445ea6ef31b2cbaf1d01d02986047606f7da266629afee982630"}, - {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c61f66d93d712f6e03369b6a7769233bfda880b12f417eefdd4f16d1deb2fc4c"}, - {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9bb62fac84d5f2ff523304e59e5c439955fb3b7f44e3d7b2085184db74d733b"}, - {file = "coverage-7.4.1-cp310-cp310-win32.whl", hash = "sha256:f86f368e1c7ce897bf2457b9eb61169a44e2ef797099fb5728482b8d69f3f016"}, - {file = "coverage-7.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:869b5046d41abfea3e381dd143407b0d29b8282a904a19cb908fa24d090cc018"}, - {file = "coverage-7.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8ffb498a83d7e0305968289441914154fb0ef5d8b3157df02a90c6695978295"}, - {file = "coverage-7.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3cacfaefe6089d477264001f90f55b7881ba615953414999c46cc9713ff93c8c"}, - {file = "coverage-7.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d6850e6e36e332d5511a48a251790ddc545e16e8beaf046c03985c69ccb2676"}, - {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18e961aa13b6d47f758cc5879383d27b5b3f3dcd9ce8cdbfdc2571fe86feb4dd"}, - {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfd1e1b9f0898817babf840b77ce9fe655ecbe8b1b327983df485b30df8cc011"}, - {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6b00e21f86598b6330f0019b40fb397e705135040dbedc2ca9a93c7441178e74"}, - {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:536d609c6963c50055bab766d9951b6c394759190d03311f3e9fcf194ca909e1"}, - {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7ac8f8eb153724f84885a1374999b7e45734bf93a87d8df1e7ce2146860edef6"}, - {file = "coverage-7.4.1-cp311-cp311-win32.whl", hash = "sha256:f3771b23bb3675a06f5d885c3630b1d01ea6cac9e84a01aaf5508706dba546c5"}, - {file = "coverage-7.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:9d2f9d4cc2a53b38cabc2d6d80f7f9b7e3da26b2f53d48f05876fef7956b6968"}, - {file = "coverage-7.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f68ef3660677e6624c8cace943e4765545f8191313a07288a53d3da188bd8581"}, - {file = "coverage-7.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23b27b8a698e749b61809fb637eb98ebf0e505710ec46a8aa6f1be7dc0dc43a6"}, - {file = "coverage-7.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e3424c554391dc9ef4a92ad28665756566a28fecf47308f91841f6c49288e66"}, - {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0860a348bf7004c812c8368d1fc7f77fe8e4c095d661a579196a9533778e156"}, - {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe558371c1bdf3b8fa03e097c523fb9645b8730399c14fe7721ee9c9e2a545d3"}, - {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3468cc8720402af37b6c6e7e2a9cdb9f6c16c728638a2ebc768ba1ef6f26c3a1"}, - {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:02f2edb575d62172aa28fe00efe821ae31f25dc3d589055b3fb64d51e52e4ab1"}, - {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ca6e61dc52f601d1d224526360cdeab0d0712ec104a2ce6cc5ccef6ed9a233bc"}, - {file = "coverage-7.4.1-cp312-cp312-win32.whl", hash = "sha256:ca7b26a5e456a843b9b6683eada193fc1f65c761b3a473941efe5a291f604c74"}, - {file = "coverage-7.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:85ccc5fa54c2ed64bd91ed3b4a627b9cce04646a659512a051fa82a92c04a448"}, - {file = "coverage-7.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8bdb0285a0202888d19ec6b6d23d5990410decb932b709f2b0dfe216d031d218"}, - {file = "coverage-7.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:918440dea04521f499721c039863ef95433314b1db00ff826a02580c1f503e45"}, - {file = "coverage-7.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:379d4c7abad5afbe9d88cc31ea8ca262296480a86af945b08214eb1a556a3e4d"}, - {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b094116f0b6155e36a304ff912f89bbb5067157aff5f94060ff20bbabdc8da06"}, - {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2f5968608b1fe2a1d00d01ad1017ee27efd99b3437e08b83ded9b7af3f6f766"}, - {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:10e88e7f41e6197ea0429ae18f21ff521d4f4490aa33048f6c6f94c6045a6a75"}, - {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a4a3907011d39dbc3e37bdc5df0a8c93853c369039b59efa33a7b6669de04c60"}, - {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d224f0c4c9c98290a6990259073f496fcec1b5cc613eecbd22786d398ded3ad"}, - {file = "coverage-7.4.1-cp38-cp38-win32.whl", hash = "sha256:23f5881362dcb0e1a92b84b3c2809bdc90db892332daab81ad8f642d8ed55042"}, - {file = "coverage-7.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:a07f61fc452c43cd5328b392e52555f7d1952400a1ad09086c4a8addccbd138d"}, - {file = "coverage-7.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e738a492b6221f8dcf281b67129510835461132b03024830ac0e554311a5c54"}, - {file = "coverage-7.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46342fed0fff72efcda77040b14728049200cbba1279e0bf1188f1f2078c1d70"}, - {file = "coverage-7.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9641e21670c68c7e57d2053ddf6c443e4f0a6e18e547e86af3fad0795414a628"}, - {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb2c2688ed93b027eb0d26aa188ada34acb22dceea256d76390eea135083950"}, - {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d12c923757de24e4e2110cf8832d83a886a4cf215c6e61ed506006872b43a6d1"}, - {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0491275c3b9971cdbd28a4595c2cb5838f08036bca31765bad5e17edf900b2c7"}, - {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8dfc5e195bbef80aabd81596ef52a1277ee7143fe419efc3c4d8ba2754671756"}, - {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1a78b656a4d12b0490ca72651fe4d9f5e07e3c6461063a9b6265ee45eb2bdd35"}, - {file = "coverage-7.4.1-cp39-cp39-win32.whl", hash = "sha256:f90515974b39f4dea2f27c0959688621b46d96d5a626cf9c53dbc653a895c05c"}, - {file = "coverage-7.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:64e723ca82a84053dd7bfcc986bdb34af8d9da83c521c19d6b472bc6880e191a"}, - {file = "coverage-7.4.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:32a8d985462e37cfdab611a6f95b09d7c091d07668fdc26e47a725ee575fe166"}, - {file = "coverage-7.4.1.tar.gz", hash = "sha256:1ed4b95480952b1a26d863e546fa5094564aa0065e1e5f0d4d0041f293251d04"}, + {file = "coverage-7.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2"}, + {file = "coverage-7.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf"}, + {file = "coverage-7.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8"}, + {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562"}, + {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2"}, + {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7"}, + {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87"}, + {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c"}, + {file = "coverage-7.4.4-cp310-cp310-win32.whl", hash = "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d"}, + {file = "coverage-7.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f"}, + {file = "coverage-7.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf"}, + {file = "coverage-7.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083"}, + {file = "coverage-7.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63"}, + {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f"}, + {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227"}, + {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd"}, + {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384"}, + {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b"}, + {file = "coverage-7.4.4-cp311-cp311-win32.whl", hash = "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286"}, + {file = "coverage-7.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec"}, + {file = "coverage-7.4.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76"}, + {file = "coverage-7.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818"}, + {file = "coverage-7.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978"}, + {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70"}, + {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51"}, + {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c"}, + {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48"}, + {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9"}, + {file = "coverage-7.4.4-cp312-cp312-win32.whl", hash = "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0"}, + {file = "coverage-7.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e"}, + {file = "coverage-7.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384"}, + {file = "coverage-7.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1"}, + {file = "coverage-7.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a"}, + {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409"}, + {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4cdc86d54b5da0df6d3d3a2f0b710949286094c3a6700c21e9015932b81447e"}, + {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd"}, + {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7"}, + {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c"}, + {file = "coverage-7.4.4-cp38-cp38-win32.whl", hash = "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e"}, + {file = "coverage-7.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8"}, + {file = "coverage-7.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d"}, + {file = "coverage-7.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357"}, + {file = "coverage-7.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e"}, + {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e"}, + {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4"}, + {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec"}, + {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd"}, + {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade"}, + {file = "coverage-7.4.4-cp39-cp39-win32.whl", hash = "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57"}, + {file = "coverage-7.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c"}, + {file = "coverage-7.4.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677"}, + {file = "coverage-7.4.4.tar.gz", hash = "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49"}, ] [package.extras] @@ -679,43 +690,43 @@ dev = ["polib"] [[package]] name = "cryptography" -version = "42.0.3" +version = "42.0.5" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-42.0.3-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:de5086cd475d67113ccb6f9fae6d8fe3ac54a4f9238fd08bfdb07b03d791ff0a"}, - {file = "cryptography-42.0.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:935cca25d35dda9e7bd46a24831dfd255307c55a07ff38fd1a92119cffc34857"}, - {file = "cryptography-42.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20100c22b298c9eaebe4f0b9032ea97186ac2555f426c3e70670f2517989543b"}, - {file = "cryptography-42.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2eb6368d5327d6455f20327fb6159b97538820355ec00f8cc9464d617caecead"}, - {file = "cryptography-42.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:39d5c93e95bcbc4c06313fc6a500cee414ee39b616b55320c1904760ad686938"}, - {file = "cryptography-42.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3d96ea47ce6d0055d5b97e761d37b4e84195485cb5a38401be341fabf23bc32a"}, - {file = "cryptography-42.0.3-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:d1998e545081da0ab276bcb4b33cce85f775adb86a516e8f55b3dac87f469548"}, - {file = "cryptography-42.0.3-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:93fbee08c48e63d5d1b39ab56fd3fdd02e6c2431c3da0f4edaf54954744c718f"}, - {file = "cryptography-42.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:90147dad8c22d64b2ff7331f8d4cddfdc3ee93e4879796f837bdbb2a0b141e0c"}, - {file = "cryptography-42.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4dcab7c25e48fc09a73c3e463d09ac902a932a0f8d0c568238b3696d06bf377b"}, - {file = "cryptography-42.0.3-cp37-abi3-win32.whl", hash = "sha256:1e935c2900fb53d31f491c0de04f41110351377be19d83d908c1fd502ae8daa5"}, - {file = "cryptography-42.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:762f3771ae40e111d78d77cbe9c1035e886ac04a234d3ee0856bf4ecb3749d54"}, - {file = "cryptography-42.0.3-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3ec384058b642f7fb7e7bff9664030011ed1af8f852540c76a1317a9dd0d20"}, - {file = "cryptography-42.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35772a6cffd1f59b85cb670f12faba05513446f80352fe811689b4e439b5d89e"}, - {file = "cryptography-42.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04859aa7f12c2b5f7e22d25198ddd537391f1695df7057c8700f71f26f47a129"}, - {file = "cryptography-42.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c3d1f5a1d403a8e640fa0887e9f7087331abb3f33b0f2207d2cc7f213e4a864c"}, - {file = "cryptography-42.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:df34312149b495d9d03492ce97471234fd9037aa5ba217c2a6ea890e9166f151"}, - {file = "cryptography-42.0.3-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:de4ae486041878dc46e571a4c70ba337ed5233a1344c14a0790c4c4be4bbb8b4"}, - {file = "cryptography-42.0.3-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:0fab2a5c479b360e5e0ea9f654bcebb535e3aa1e493a715b13244f4e07ea8eec"}, - {file = "cryptography-42.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25b09b73db78facdfd7dd0fa77a3f19e94896197c86e9f6dc16bce7b37a96504"}, - {file = "cryptography-42.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d5cf11bc7f0b71fb71af26af396c83dfd3f6eed56d4b6ef95d57867bf1e4ba65"}, - {file = "cryptography-42.0.3-cp39-abi3-win32.whl", hash = "sha256:0fea01527d4fb22ffe38cd98951c9044400f6eff4788cf52ae116e27d30a1ba3"}, - {file = "cryptography-42.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:2619487f37da18d6826e27854a7f9d4d013c51eafb066c80d09c63cf24505306"}, - {file = "cryptography-42.0.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ead69ba488f806fe1b1b4050febafdbf206b81fa476126f3e16110c818bac396"}, - {file = "cryptography-42.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:20180da1b508f4aefc101cebc14c57043a02b355d1a652b6e8e537967f1e1b46"}, - {file = "cryptography-42.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5fbf0f3f0fac7c089308bd771d2c6c7b7d53ae909dce1db52d8e921f6c19bb3a"}, - {file = "cryptography-42.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c23f03cfd7d9826cdcbad7850de67e18b4654179e01fe9bc623d37c2638eb4ef"}, - {file = "cryptography-42.0.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db0480ffbfb1193ac4e1e88239f31314fe4c6cdcf9c0b8712b55414afbf80db4"}, - {file = "cryptography-42.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:6c25e1e9c2ce682d01fc5e2dde6598f7313027343bd14f4049b82ad0402e52cd"}, - {file = "cryptography-42.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9541c69c62d7446539f2c1c06d7046aef822940d248fa4b8962ff0302862cc1f"}, - {file = "cryptography-42.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1b797099d221df7cce5ff2a1d272761d1554ddf9a987d3e11f6459b38cd300fd"}, - {file = "cryptography-42.0.3.tar.gz", hash = "sha256:069d2ce9be5526a44093a0991c450fe9906cdf069e0e7cd67d9dee49a62b9ebe"}, + {file = "cryptography-42.0.5-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:a30596bae9403a342c978fb47d9b0ee277699fa53bbafad14706af51fe543d16"}, + {file = "cryptography-42.0.5-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:b7ffe927ee6531c78f81aa17e684e2ff617daeba7f189f911065b2ea2d526dec"}, + {file = "cryptography-42.0.5-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2424ff4c4ac7f6b8177b53c17ed5d8fa74ae5955656867f5a8affaca36a27abb"}, + {file = "cryptography-42.0.5-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:329906dcc7b20ff3cad13c069a78124ed8247adcac44b10bea1130e36caae0b4"}, + {file = "cryptography-42.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:b03c2ae5d2f0fc05f9a2c0c997e1bc18c8229f392234e8a0194f202169ccd278"}, + {file = "cryptography-42.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8837fe1d6ac4a8052a9a8ddab256bc006242696f03368a4009be7ee3075cdb7"}, + {file = "cryptography-42.0.5-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:0270572b8bd2c833c3981724b8ee9747b3ec96f699a9665470018594301439ee"}, + {file = "cryptography-42.0.5-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:b8cac287fafc4ad485b8a9b67d0ee80c66bf3574f655d3b97ef2e1082360faf1"}, + {file = "cryptography-42.0.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:16a48c23a62a2f4a285699dba2e4ff2d1cff3115b9df052cdd976a18856d8e3d"}, + {file = "cryptography-42.0.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2bce03af1ce5a5567ab89bd90d11e7bbdff56b8af3acbbec1faded8f44cb06da"}, + {file = "cryptography-42.0.5-cp37-abi3-win32.whl", hash = "sha256:b6cd2203306b63e41acdf39aa93b86fb566049aeb6dc489b70e34bcd07adca74"}, + {file = "cryptography-42.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:98d8dc6d012b82287f2c3d26ce1d2dd130ec200c8679b6213b3c73c08b2b7940"}, + {file = "cryptography-42.0.5-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:5e6275c09d2badf57aea3afa80d975444f4be8d3bc58f7f80d2a484c6f9485c8"}, + {file = "cryptography-42.0.5-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4985a790f921508f36f81831817cbc03b102d643b5fcb81cd33df3fa291a1a1"}, + {file = "cryptography-42.0.5-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cde5f38e614f55e28d831754e8a3bacf9ace5d1566235e39d91b35502d6936e"}, + {file = "cryptography-42.0.5-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7367d7b2eca6513681127ebad53b2582911d1736dc2ffc19f2c3ae49997496bc"}, + {file = "cryptography-42.0.5-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cd2030f6650c089aeb304cf093f3244d34745ce0cfcc39f20c6fbfe030102e2a"}, + {file = "cryptography-42.0.5-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a2913c5375154b6ef2e91c10b5720ea6e21007412f6437504ffea2109b5a33d7"}, + {file = "cryptography-42.0.5-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:c41fb5e6a5fe9ebcd58ca3abfeb51dffb5d83d6775405305bfa8715b76521922"}, + {file = "cryptography-42.0.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3eaafe47ec0d0ffcc9349e1708be2aaea4c6dd4978d76bf6eb0cb2c13636c6fc"}, + {file = "cryptography-42.0.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1b95b98b0d2af784078fa69f637135e3c317091b615cd0905f8b8a087e86fa30"}, + {file = "cryptography-42.0.5-cp39-abi3-win32.whl", hash = "sha256:1f71c10d1e88467126f0efd484bd44bca5e14c664ec2ede64c32f20875c0d413"}, + {file = "cryptography-42.0.5-cp39-abi3-win_amd64.whl", hash = "sha256:a011a644f6d7d03736214d38832e030d8268bcff4a41f728e6030325fea3e400"}, + {file = "cryptography-42.0.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9481ffe3cf013b71b2428b905c4f7a9a4f76ec03065b05ff499bb5682a8d9ad8"}, + {file = "cryptography-42.0.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:ba334e6e4b1d92442b75ddacc615c5476d4ad55cc29b15d590cc6b86efa487e2"}, + {file = "cryptography-42.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ba3e4a42397c25b7ff88cdec6e2a16c2be18720f317506ee25210f6d31925f9c"}, + {file = "cryptography-42.0.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:111a0d8553afcf8eb02a4fea6ca4f59d48ddb34497aa8706a6cf536f1a5ec576"}, + {file = "cryptography-42.0.5-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cd65d75953847815962c84a4654a84850b2bb4aed3f26fadcc1c13892e1e29f6"}, + {file = "cryptography-42.0.5-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e807b3188f9eb0eaa7bbb579b462c5ace579f1cedb28107ce8b48a9f7ad3679e"}, + {file = "cryptography-42.0.5-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f12764b8fffc7a123f641d7d049d382b73f96a34117e0b637b80643169cec8ac"}, + {file = "cryptography-42.0.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:37dd623507659e08be98eec89323469e8c7b4c1407c85112634ae3dbdb926fdd"}, + {file = "cryptography-42.0.5.tar.gz", hash = "sha256:6fe07eec95dfd477eb9530aef5bead34fec819b3aaf6c5bd6d20565da607bfe1"}, ] [package.dependencies] @@ -781,13 +792,13 @@ profile = ["gprof2dot (>=2022.7.29)"] [[package]] name = "django" -version = "3.2.24" +version = "3.2.25" description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.6" files = [ - {file = "Django-3.2.24-py3-none-any.whl", hash = "sha256:5dd5b787c3ba39637610fe700f54bf158e33560ea0dba600c19921e7ff926ec5"}, - {file = "Django-3.2.24.tar.gz", hash = "sha256:aaee9fb0fb4ebd4311520887ad2e33313d368846607f82a9a0ed461cd4c35b18"}, + {file = "Django-3.2.25-py3-none-any.whl", hash = "sha256:a52ea7fcf280b16f7b739cec38fa6d3f8953a5456986944c3ca97e79882b4e38"}, + {file = "Django-3.2.25.tar.gz", hash = "sha256:7ca38a78654aee72378594d63e51636c04b8e28574f5505dff630895b5472777"}, ] [package.dependencies] @@ -968,13 +979,13 @@ jinja2 = ">=3" [[package]] name = "django-picklefield" -version = "3.1" +version = "3.2" description = "Pickled object field for Django" optional = false python-versions = ">=3" files = [ - {file = "django-picklefield-3.1.tar.gz", hash = "sha256:c786cbeda78d6def2b43bff4840d19787809c8909f7ad683961703060398d356"}, - {file = "django_picklefield-3.1-py3-none-any.whl", hash = "sha256:d77c504df7311e8ec14e8b779f10ca6fec74de6c7f8e2c136e1ef60cf955125d"}, + {file = "django-picklefield-3.2.tar.gz", hash = "sha256:aa463f5d79d497dbe789f14b45180f00a51d0d670067d0729f352a3941cdfa4d"}, + {file = "django_picklefield-3.2-py3-none-any.whl", hash = "sha256:e9a73539d110f69825d9320db18bcb82e5189ff48dbed41821c026a20497764c"}, ] [package.dependencies] @@ -1169,13 +1180,13 @@ sidecar = ["drf-spectacular-sidecar"] [[package]] name = "drf-spectacular-sidecar" -version = "2024.2.1" +version = "2024.4.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.2.1.tar.gz", hash = "sha256:db95a38971c9be09986356f82041fac60183d28ebdf60c0c51eb8c1f86da3937"}, - {file = "drf_spectacular_sidecar-2024.2.1-py3-none-any.whl", hash = "sha256:dc819ef7a35448c18b2bf4273b38fe1468e14daea5fc8675afb5d0f9e6d9a0ba"}, + {file = "drf-spectacular-sidecar-2024.4.1.tar.gz", hash = "sha256:68532dd094714f79c1775c00848f22c10f004826abc856442ff30c3bc9c40bb4"}, + {file = "drf_spectacular_sidecar-2024.4.1-py3-none-any.whl", hash = "sha256:8359befe69a8953fea86be01c1ff37038854a62546225551de16c47c07dccd4e"}, ] [package.dependencies] @@ -1211,19 +1222,19 @@ tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipyth [[package]] name = "flake8" -version = "5.0.4" +version = "7.0.0" description = "the modular source code checker: pep8 pyflakes and co" optional = false -python-versions = ">=3.6.1" +python-versions = ">=3.8.1" files = [ - {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, - {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, + {file = "flake8-7.0.0-py2.py3-none-any.whl", hash = "sha256:a6dfbb75e03252917f2473ea9653f7cd799c3064e54d4c8140044c5c065f53c3"}, + {file = "flake8-7.0.0.tar.gz", hash = "sha256:33f96621059e65eec474169085dc92bf26e7b2d47366b70be2f67ab80dc25132"}, ] [package.dependencies] mccabe = ">=0.7.0,<0.8.0" -pycodestyle = ">=2.9.0,<2.10.0" -pyflakes = ">=2.5.0,<2.6.0" +pycodestyle = ">=2.11.0,<2.12.0" +pyflakes = ">=3.2.0,<3.3.0" [[package]] name = "ghp-import" @@ -1258,20 +1269,21 @@ smmap = ">=3.0.1,<6" [[package]] name = "gitpython" -version = "3.1.42" +version = "3.1.43" description = "GitPython is a Python library used to interact with Git repositories" optional = false python-versions = ">=3.7" files = [ - {file = "GitPython-3.1.42-py3-none-any.whl", hash = "sha256:1bf9cd7c9e7255f77778ea54359e54ac22a72a5b51288c457c881057b7bb9ecd"}, - {file = "GitPython-3.1.42.tar.gz", hash = "sha256:2d99869e0fef71a73cbd242528105af1d6c1b108c60dfabd994bf292f76c3ceb"}, + {file = "GitPython-3.1.43-py3-none-any.whl", hash = "sha256:eec7ec56b92aad751f9912a73404bc02ba212a23adb2c7098ee668417051a1ff"}, + {file = "GitPython-3.1.43.tar.gz", hash = "sha256:35f314a9f878467f5453cc1fee295c3e18e52f1b99f10f6cf5b1682e968a9e7c"}, ] [package.dependencies] gitdb = ">=4.0.1,<5" [package.extras] -test = ["black", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar"] +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)"] +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]] name = "gprof2dot" @@ -1378,65 +1390,66 @@ six = ">=1.12" [[package]] name = "griffe" -version = "0.40.1" +version = "0.42.1" description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." optional = false python-versions = ">=3.8" files = [ - {file = "griffe-0.40.1-py3-none-any.whl", hash = "sha256:5b8c023f366fe273e762131fe4bfd141ea56c09b3cb825aa92d06a82681cfd93"}, - {file = "griffe-0.40.1.tar.gz", hash = "sha256:66c48a62e2ce5784b6940e603300fcfb807b6f099b94e7f753f1841661fd5c7c"}, + {file = "griffe-0.42.1-py3-none-any.whl", hash = "sha256:7e805e35617601355edcac0d3511cedc1ed0cb1f7645e2d336ae4b05bbae7b3b"}, + {file = "griffe-0.42.1.tar.gz", hash = "sha256:57046131384043ed078692b85d86b76568a686266cc036b9b56b704466f803ce"}, ] [package.dependencies] +astunparse = {version = ">=1.6", markers = "python_version < \"3.9\""} colorama = ">=0.4" [[package]] name = "idna" -version = "3.6" +version = "3.7" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" files = [ - {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, - {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, ] [[package]] name = "importlib-metadata" -version = "7.0.1" +version = "7.1.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_metadata-7.0.1-py3-none-any.whl", hash = "sha256:4805911c3a4ec7c3966410053e9ec6a1fecd629117df5adee56dfc9432a1081e"}, - {file = "importlib_metadata-7.0.1.tar.gz", hash = "sha256:f238736bb06590ae52ac1fab06a3a9ef1d8dce2b7a35b5ab329371d6c8f5d2cc"}, + {file = "importlib_metadata-7.1.0-py3-none-any.whl", hash = "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570"}, + {file = "importlib_metadata-7.1.0.tar.gz", hash = "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2"}, ] [package.dependencies] zipp = ">=0.5" [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] [[package]] name = "importlib-resources" -version = "5.13.0" +version = "6.4.0" description = "Read resources from Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_resources-5.13.0-py3-none-any.whl", hash = "sha256:9f7bd0c97b79972a6cce36a366356d16d5e13b09679c11a58f1014bfdf8e64b2"}, - {file = "importlib_resources-5.13.0.tar.gz", hash = "sha256:82d5c6cca930697dbbd86c93333bb2c2e72861d4789a11c2662b933e5ad2b528"}, + {file = "importlib_resources-6.4.0-py3-none-any.whl", hash = "sha256:50d10f043df931902d4194ea07ec57960f66a80449ff867bfe782b4c486ba78c"}, + {file = "importlib_resources-6.4.0.tar.gz", hash = "sha256:cdb2b453b8046ca4e3798eb1d84f3cce1446a0e8e7b5ef4efb600f19fc398145"}, ] [package.dependencies] zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["jaraco.test (>=5.4)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)", "zipp (>=3.17)"] [[package]] name = "inflection" @@ -1589,13 +1602,13 @@ referencing = ">=0.31.0" [[package]] name = "kombu" -version = "5.3.5" +version = "5.3.7" description = "Messaging library for Python." optional = false python-versions = ">=3.8" files = [ - {file = "kombu-5.3.5-py3-none-any.whl", hash = "sha256:0eac1bbb464afe6fb0924b21bf79460416d25d8abc52546d4f16cad94f789488"}, - {file = "kombu-5.3.5.tar.gz", hash = "sha256:30e470f1a6b49c70dc6f6d13c3e4cc4e178aa6c469ceb6bcd55645385fc84b93"}, + {file = "kombu-5.3.7-py3-none-any.whl", hash = "sha256:5634c511926309c7f9789f1433e9ed402616b56836ef9878f01bd59267b4c7a9"}, + {file = "kombu-5.3.7.tar.gz", hash = "sha256:011c4cd9a355c14a1de8d35d257314a1d2456d52b7140388561acac3cf1a97bf"}, ] [package.dependencies] @@ -1614,163 +1627,200 @@ mongodb = ["pymongo (>=4.1.1)"] msgpack = ["msgpack"] pyro = ["pyro4"] qpid = ["qpid-python (>=0.26)", "qpid-tools (>=0.26)"] -redis = ["redis (>=4.5.2,!=4.5.5,<6.0.0)"] +redis = ["redis (>=4.5.2,!=4.5.5,!=5.0.2)"] slmq = ["softlayer-messaging (>=1.0.3)"] sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"] sqs = ["boto3 (>=1.26.143)", "pycurl (>=7.43.0.5)", "urllib3 (>=1.26.16)"] yaml = ["PyYAML (>=3.10)"] zookeeper = ["kazoo (>=2.8.0)"] -[[package]] -name = "lazy-object-proxy" -version = "1.10.0" -description = "A fast and thorough lazy object proxy." -optional = false -python-versions = ">=3.8" -files = [ - {file = "lazy-object-proxy-1.10.0.tar.gz", hash = "sha256:78247b6d45f43a52ef35c25b5581459e85117225408a4128a3daf8bf9648ac69"}, - {file = "lazy_object_proxy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:855e068b0358ab916454464a884779c7ffa312b8925c6f7401e952dcf3b89977"}, - {file = "lazy_object_proxy-1.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab7004cf2e59f7c2e4345604a3e6ea0d92ac44e1c2375527d56492014e690c3"}, - {file = "lazy_object_proxy-1.10.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc0d2fc424e54c70c4bc06787e4072c4f3b1aa2f897dfdc34ce1013cf3ceef05"}, - {file = "lazy_object_proxy-1.10.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e2adb09778797da09d2b5ebdbceebf7dd32e2c96f79da9052b2e87b6ea495895"}, - {file = "lazy_object_proxy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b1f711e2c6dcd4edd372cf5dec5c5a30d23bba06ee012093267b3376c079ec83"}, - {file = "lazy_object_proxy-1.10.0-cp310-cp310-win32.whl", hash = "sha256:76a095cfe6045c7d0ca77db9934e8f7b71b14645f0094ffcd842349ada5c5fb9"}, - {file = "lazy_object_proxy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:b4f87d4ed9064b2628da63830986c3d2dca7501e6018347798313fcf028e2fd4"}, - {file = "lazy_object_proxy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fec03caabbc6b59ea4a638bee5fce7117be8e99a4103d9d5ad77f15d6f81020c"}, - {file = "lazy_object_proxy-1.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02c83f957782cbbe8136bee26416686a6ae998c7b6191711a04da776dc9e47d4"}, - {file = "lazy_object_proxy-1.10.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:009e6bb1f1935a62889ddc8541514b6a9e1fcf302667dcb049a0be5c8f613e56"}, - {file = "lazy_object_proxy-1.10.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75fc59fc450050b1b3c203c35020bc41bd2695ed692a392924c6ce180c6f1dc9"}, - {file = "lazy_object_proxy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:782e2c9b2aab1708ffb07d4bf377d12901d7a1d99e5e410d648d892f8967ab1f"}, - {file = "lazy_object_proxy-1.10.0-cp311-cp311-win32.whl", hash = "sha256:edb45bb8278574710e68a6b021599a10ce730d156e5b254941754a9cc0b17d03"}, - {file = "lazy_object_proxy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:e271058822765ad5e3bca7f05f2ace0de58a3f4e62045a8c90a0dfd2f8ad8cc6"}, - {file = "lazy_object_proxy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e98c8af98d5707dcdecc9ab0863c0ea6e88545d42ca7c3feffb6b4d1e370c7ba"}, - {file = "lazy_object_proxy-1.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:952c81d415b9b80ea261d2372d2a4a2332a3890c2b83e0535f263ddfe43f0d43"}, - {file = "lazy_object_proxy-1.10.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80b39d3a151309efc8cc48675918891b865bdf742a8616a337cb0090791a0de9"}, - {file = "lazy_object_proxy-1.10.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e221060b701e2aa2ea991542900dd13907a5c90fa80e199dbf5a03359019e7a3"}, - {file = "lazy_object_proxy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:92f09ff65ecff3108e56526f9e2481b8116c0b9e1425325e13245abfd79bdb1b"}, - {file = "lazy_object_proxy-1.10.0-cp312-cp312-win32.whl", hash = "sha256:3ad54b9ddbe20ae9f7c1b29e52f123120772b06dbb18ec6be9101369d63a4074"}, - {file = "lazy_object_proxy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:127a789c75151db6af398b8972178afe6bda7d6f68730c057fbbc2e96b08d282"}, - {file = "lazy_object_proxy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9e4ed0518a14dd26092614412936920ad081a424bdcb54cc13349a8e2c6d106a"}, - {file = "lazy_object_proxy-1.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ad9e6ed739285919aa9661a5bbed0aaf410aa60231373c5579c6b4801bd883c"}, - {file = "lazy_object_proxy-1.10.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fc0a92c02fa1ca1e84fc60fa258458e5bf89d90a1ddaeb8ed9cc3147f417255"}, - {file = "lazy_object_proxy-1.10.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0aefc7591920bbd360d57ea03c995cebc204b424524a5bd78406f6e1b8b2a5d8"}, - {file = "lazy_object_proxy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5faf03a7d8942bb4476e3b62fd0f4cf94eaf4618e304a19865abf89a35c0bbee"}, - {file = "lazy_object_proxy-1.10.0-cp38-cp38-win32.whl", hash = "sha256:e333e2324307a7b5d86adfa835bb500ee70bfcd1447384a822e96495796b0ca4"}, - {file = "lazy_object_proxy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:cb73507defd385b7705c599a94474b1d5222a508e502553ef94114a143ec6696"}, - {file = "lazy_object_proxy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:366c32fe5355ef5fc8a232c5436f4cc66e9d3e8967c01fb2e6302fd6627e3d94"}, - {file = "lazy_object_proxy-1.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2297f08f08a2bb0d32a4265e98a006643cd7233fb7983032bd61ac7a02956b3b"}, - {file = "lazy_object_proxy-1.10.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18dd842b49456aaa9a7cf535b04ca4571a302ff72ed8740d06b5adcd41fe0757"}, - {file = "lazy_object_proxy-1.10.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:217138197c170a2a74ca0e05bddcd5f1796c735c37d0eee33e43259b192aa424"}, - {file = "lazy_object_proxy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9a3a87cf1e133e5b1994144c12ca4aa3d9698517fe1e2ca82977781b16955658"}, - {file = "lazy_object_proxy-1.10.0-cp39-cp39-win32.whl", hash = "sha256:30b339b2a743c5288405aa79a69e706a06e02958eab31859f7f3c04980853b70"}, - {file = "lazy_object_proxy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:a899b10e17743683b293a729d3a11f2f399e8a90c73b089e29f5d0fe3509f0dd"}, - {file = "lazy_object_proxy-1.10.0-pp310.pp311.pp312.pp38.pp39-none-any.whl", hash = "sha256:80fa48bd89c8f2f456fc0765c11c23bf5af827febacd2f523ca5bc1893fcc09d"}, -] - [[package]] name = "lxml" -version = "5.1.0" +version = "5.2.1" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." optional = false python-versions = ">=3.6" files = [ - {file = "lxml-5.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9d3c0f8567ffe7502d969c2c1b809892dc793b5d0665f602aad19895f8d508da"}, - {file = "lxml-5.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5fcfbebdb0c5d8d18b84118842f31965d59ee3e66996ac842e21f957eb76138c"}, - {file = "lxml-5.1.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f37c6d7106a9d6f0708d4e164b707037b7380fcd0b04c5bd9cae1fb46a856fb"}, - {file = "lxml-5.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2befa20a13f1a75c751f47e00929fb3433d67eb9923c2c0b364de449121f447c"}, - {file = "lxml-5.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22b7ee4c35f374e2c20337a95502057964d7e35b996b1c667b5c65c567d2252a"}, - {file = "lxml-5.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bf8443781533b8d37b295016a4b53c1494fa9a03573c09ca5104550c138d5c05"}, - {file = "lxml-5.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:82bddf0e72cb2af3cbba7cec1d2fd11fda0de6be8f4492223d4a268713ef2147"}, - {file = "lxml-5.1.0-cp310-cp310-win32.whl", hash = "sha256:b66aa6357b265670bb574f050ffceefb98549c721cf28351b748be1ef9577d93"}, - {file = "lxml-5.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:4946e7f59b7b6a9e27bef34422f645e9a368cb2be11bf1ef3cafc39a1f6ba68d"}, - {file = "lxml-5.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ed8c3d2cd329bf779b7ed38db176738f3f8be637bb395ce9629fc76f78afe3d4"}, - {file = "lxml-5.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:436a943c2900bb98123b06437cdd30580a61340fbdb7b28aaf345a459c19046a"}, - {file = "lxml-5.1.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:acb6b2f96f60f70e7f34efe0c3ea34ca63f19ca63ce90019c6cbca6b676e81fa"}, - {file = "lxml-5.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:af8920ce4a55ff41167ddbc20077f5698c2e710ad3353d32a07d3264f3a2021e"}, - {file = "lxml-5.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cfced4a069003d8913408e10ca8ed092c49a7f6cefee9bb74b6b3e860683b45"}, - {file = "lxml-5.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9e5ac3437746189a9b4121db2a7b86056ac8786b12e88838696899328fc44bb2"}, - {file = "lxml-5.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f4c9bda132ad108b387c33fabfea47866af87f4ea6ffb79418004f0521e63204"}, - {file = "lxml-5.1.0-cp311-cp311-win32.whl", hash = "sha256:bc64d1b1dab08f679fb89c368f4c05693f58a9faf744c4d390d7ed1d8223869b"}, - {file = "lxml-5.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:a5ab722ae5a873d8dcee1f5f45ddd93c34210aed44ff2dc643b5025981908cda"}, - {file = "lxml-5.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6f11b77ec0979f7e4dc5ae081325a2946f1fe424148d3945f943ceaede98adb8"}, - {file = "lxml-5.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a36c506e5f8aeb40680491d39ed94670487ce6614b9d27cabe45d94cd5d63e1e"}, - {file = "lxml-5.1.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f643ffd2669ffd4b5a3e9b41c909b72b2a1d5e4915da90a77e119b8d48ce867a"}, - {file = "lxml-5.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16dd953fb719f0ffc5bc067428fc9e88f599e15723a85618c45847c96f11f431"}, - {file = "lxml-5.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16018f7099245157564d7148165132c70adb272fb5a17c048ba70d9cc542a1a1"}, - {file = "lxml-5.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:82cd34f1081ae4ea2ede3d52f71b7be313756e99b4b5f829f89b12da552d3aa3"}, - {file = "lxml-5.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:19a1bc898ae9f06bccb7c3e1dfd73897ecbbd2c96afe9095a6026016e5ca97b8"}, - {file = "lxml-5.1.0-cp312-cp312-win32.whl", hash = "sha256:13521a321a25c641b9ea127ef478b580b5ec82aa2e9fc076c86169d161798b01"}, - {file = "lxml-5.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:1ad17c20e3666c035db502c78b86e58ff6b5991906e55bdbef94977700c72623"}, - {file = "lxml-5.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:24ef5a4631c0b6cceaf2dbca21687e29725b7c4e171f33a8f8ce23c12558ded1"}, - {file = "lxml-5.1.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d2900b7f5318bc7ad8631d3d40190b95ef2aa8cc59473b73b294e4a55e9f30f"}, - {file = "lxml-5.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:601f4a75797d7a770daed8b42b97cd1bb1ba18bd51a9382077a6a247a12aa38d"}, - {file = "lxml-5.1.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4b68c961b5cc402cbd99cca5eb2547e46ce77260eb705f4d117fd9c3f932b95"}, - {file = "lxml-5.1.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:afd825e30f8d1f521713a5669b63657bcfe5980a916c95855060048b88e1adb7"}, - {file = "lxml-5.1.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:262bc5f512a66b527d026518507e78c2f9c2bd9eb5c8aeeb9f0eb43fcb69dc67"}, - {file = "lxml-5.1.0-cp36-cp36m-win32.whl", hash = "sha256:e856c1c7255c739434489ec9c8aa9cdf5179785d10ff20add308b5d673bed5cd"}, - {file = "lxml-5.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c7257171bb8d4432fe9d6fdde4d55fdbe663a63636a17f7f9aaba9bcb3153ad7"}, - {file = "lxml-5.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b9e240ae0ba96477682aa87899d94ddec1cc7926f9df29b1dd57b39e797d5ab5"}, - {file = "lxml-5.1.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a96f02ba1bcd330807fc060ed91d1f7a20853da6dd449e5da4b09bfcc08fdcf5"}, - {file = "lxml-5.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e3898ae2b58eeafedfe99e542a17859017d72d7f6a63de0f04f99c2cb125936"}, - {file = "lxml-5.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61c5a7edbd7c695e54fca029ceb351fc45cd8860119a0f83e48be44e1c464862"}, - {file = "lxml-5.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3aeca824b38ca78d9ee2ab82bd9883083d0492d9d17df065ba3b94e88e4d7ee6"}, - {file = "lxml-5.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8f52fe6859b9db71ee609b0c0a70fea5f1e71c3462ecf144ca800d3f434f0764"}, - {file = "lxml-5.1.0-cp37-cp37m-win32.whl", hash = "sha256:d42e3a3fc18acc88b838efded0e6ec3edf3e328a58c68fbd36a7263a874906c8"}, - {file = "lxml-5.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:eac68f96539b32fce2c9b47eb7c25bb2582bdaf1bbb360d25f564ee9e04c542b"}, - {file = "lxml-5.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c26aab6ea9c54d3bed716b8851c8bfc40cb249b8e9880e250d1eddde9f709bf5"}, - {file = "lxml-5.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cfbac9f6149174f76df7e08c2e28b19d74aed90cad60383ad8671d3af7d0502f"}, - {file = "lxml-5.1.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:342e95bddec3a698ac24378d61996b3ee5ba9acfeb253986002ac53c9a5f6f84"}, - {file = "lxml-5.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:725e171e0b99a66ec8605ac77fa12239dbe061482ac854d25720e2294652eeaa"}, - {file = "lxml-5.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d184e0d5c918cff04cdde9dbdf9600e960161d773666958c9d7b565ccc60c45"}, - {file = "lxml-5.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:98f3f020a2b736566c707c8e034945c02aa94e124c24f77ca097c446f81b01f1"}, - {file = "lxml-5.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d48fc57e7c1e3df57be5ae8614bab6d4e7b60f65c5457915c26892c41afc59e"}, - {file = "lxml-5.1.0-cp38-cp38-win32.whl", hash = "sha256:7ec465e6549ed97e9f1e5ed51c657c9ede767bc1c11552f7f4d022c4df4a977a"}, - {file = "lxml-5.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:b21b4031b53d25b0858d4e124f2f9131ffc1530431c6d1321805c90da78388d1"}, - {file = "lxml-5.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6a2a2c724d97c1eb8cf966b16ca2915566a4904b9aad2ed9a09c748ffe14f969"}, - {file = "lxml-5.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:843b9c835580d52828d8f69ea4302537337a21e6b4f1ec711a52241ba4a824f3"}, - {file = "lxml-5.1.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9b99f564659cfa704a2dd82d0684207b1aadf7d02d33e54845f9fc78e06b7581"}, - {file = "lxml-5.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f8b0c78e7aac24979ef09b7f50da871c2de2def043d468c4b41f512d831e912"}, - {file = "lxml-5.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bcf86dfc8ff3e992fed847c077bd875d9e0ba2fa25d859c3a0f0f76f07f0c8d"}, - {file = "lxml-5.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:49a9b4af45e8b925e1cd6f3b15bbba2c81e7dba6dce170c677c9cda547411e14"}, - {file = "lxml-5.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:280f3edf15c2a967d923bcfb1f8f15337ad36f93525828b40a0f9d6c2ad24890"}, - {file = "lxml-5.1.0-cp39-cp39-win32.whl", hash = "sha256:ed7326563024b6e91fef6b6c7a1a2ff0a71b97793ac33dbbcf38f6005e51ff6e"}, - {file = "lxml-5.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:8d7b4beebb178e9183138f552238f7e6613162a42164233e2bda00cb3afac58f"}, - {file = "lxml-5.1.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9bd0ae7cc2b85320abd5e0abad5ccee5564ed5f0cc90245d2f9a8ef330a8deae"}, - {file = "lxml-5.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8c1d679df4361408b628f42b26a5d62bd3e9ba7f0c0e7969f925021554755aa"}, - {file = "lxml-5.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2ad3a8ce9e8a767131061a22cd28fdffa3cd2dc193f399ff7b81777f3520e372"}, - {file = "lxml-5.1.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:304128394c9c22b6569eba2a6d98392b56fbdfbad58f83ea702530be80d0f9df"}, - {file = "lxml-5.1.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d74fcaf87132ffc0447b3c685a9f862ffb5b43e70ea6beec2fb8057d5d2a1fea"}, - {file = "lxml-5.1.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:8cf5877f7ed384dabfdcc37922c3191bf27e55b498fecece9fd5c2c7aaa34c33"}, - {file = "lxml-5.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:877efb968c3d7eb2dad540b6cabf2f1d3c0fbf4b2d309a3c141f79c7e0061324"}, - {file = "lxml-5.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f14a4fb1c1c402a22e6a341a24c1341b4a3def81b41cd354386dcb795f83897"}, - {file = "lxml-5.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:25663d6e99659544ee8fe1b89b1a8c0aaa5e34b103fab124b17fa958c4a324a6"}, - {file = "lxml-5.1.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8b9f19df998761babaa7f09e6bc169294eefafd6149aaa272081cbddc7ba4ca3"}, - {file = "lxml-5.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e53d7e6a98b64fe54775d23a7c669763451340c3d44ad5e3a3b48a1efbdc96f"}, - {file = "lxml-5.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c3cd1fc1dc7c376c54440aeaaa0dcc803d2126732ff5c6b68ccd619f2e64be4f"}, - {file = "lxml-5.1.0.tar.gz", hash = "sha256:3eea6ed6e6c918e468e693c41ef07f3c3acc310b70ddd9cc72d9ef84bc9564ca"}, + {file = "lxml-5.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1f7785f4f789fdb522729ae465adcaa099e2a3441519df750ebdccc481d961a1"}, + {file = "lxml-5.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6cc6ee342fb7fa2471bd9b6d6fdfc78925a697bf5c2bcd0a302e98b0d35bfad3"}, + {file = "lxml-5.2.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:794f04eec78f1d0e35d9e0c36cbbb22e42d370dda1609fb03bcd7aeb458c6377"}, + {file = "lxml-5.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817d420c60a5183953c783b0547d9eb43b7b344a2c46f69513d5952a78cddf3"}, + {file = "lxml-5.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2213afee476546a7f37c7a9b4ad4d74b1e112a6fafffc9185d6d21f043128c81"}, + {file = "lxml-5.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b070bbe8d3f0f6147689bed981d19bbb33070225373338df755a46893528104a"}, + {file = "lxml-5.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e02c5175f63effbd7c5e590399c118d5db6183bbfe8e0d118bdb5c2d1b48d937"}, + {file = "lxml-5.2.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:3dc773b2861b37b41a6136e0b72a1a44689a9c4c101e0cddb6b854016acc0aa8"}, + {file = "lxml-5.2.1-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:d7520db34088c96cc0e0a3ad51a4fd5b401f279ee112aa2b7f8f976d8582606d"}, + {file = "lxml-5.2.1-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:bcbf4af004f98793a95355980764b3d80d47117678118a44a80b721c9913436a"}, + {file = "lxml-5.2.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a2b44bec7adf3e9305ce6cbfa47a4395667e744097faed97abb4728748ba7d47"}, + {file = "lxml-5.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1c5bb205e9212d0ebddf946bc07e73fa245c864a5f90f341d11ce7b0b854475d"}, + {file = "lxml-5.2.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2c9d147f754b1b0e723e6afb7ba1566ecb162fe4ea657f53d2139bbf894d050a"}, + {file = "lxml-5.2.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:3545039fa4779be2df51d6395e91a810f57122290864918b172d5dc7ca5bb433"}, + {file = "lxml-5.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a91481dbcddf1736c98a80b122afa0f7296eeb80b72344d7f45dc9f781551f56"}, + {file = "lxml-5.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2ddfe41ddc81f29a4c44c8ce239eda5ade4e7fc305fb7311759dd6229a080052"}, + {file = "lxml-5.2.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a7baf9ffc238e4bf401299f50e971a45bfcc10a785522541a6e3179c83eabf0a"}, + {file = "lxml-5.2.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:31e9a882013c2f6bd2f2c974241bf4ba68c85eba943648ce88936d23209a2e01"}, + {file = "lxml-5.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0a15438253b34e6362b2dc41475e7f80de76320f335e70c5528b7148cac253a1"}, + {file = "lxml-5.2.1-cp310-cp310-win32.whl", hash = "sha256:6992030d43b916407c9aa52e9673612ff39a575523c5f4cf72cdef75365709a5"}, + {file = "lxml-5.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:da052e7962ea2d5e5ef5bc0355d55007407087392cf465b7ad84ce5f3e25fe0f"}, + {file = "lxml-5.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:70ac664a48aa64e5e635ae5566f5227f2ab7f66a3990d67566d9907edcbbf867"}, + {file = "lxml-5.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1ae67b4e737cddc96c99461d2f75d218bdf7a0c3d3ad5604d1f5e7464a2f9ffe"}, + {file = "lxml-5.2.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f18a5a84e16886898e51ab4b1d43acb3083c39b14c8caeb3589aabff0ee0b270"}, + {file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6f2c8372b98208ce609c9e1d707f6918cc118fea4e2c754c9f0812c04ca116d"}, + {file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:394ed3924d7a01b5bd9a0d9d946136e1c2f7b3dc337196d99e61740ed4bc6fe1"}, + {file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d077bc40a1fe984e1a9931e801e42959a1e6598edc8a3223b061d30fbd26bbc"}, + {file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:764b521b75701f60683500d8621841bec41a65eb739b8466000c6fdbc256c240"}, + {file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:3a6b45da02336895da82b9d472cd274b22dc27a5cea1d4b793874eead23dd14f"}, + {file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:5ea7b6766ac2dfe4bcac8b8595107665a18ef01f8c8343f00710b85096d1b53a"}, + {file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:e196a4ff48310ba62e53a8e0f97ca2bca83cdd2fe2934d8b5cb0df0a841b193a"}, + {file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:200e63525948e325d6a13a76ba2911f927ad399ef64f57898cf7c74e69b71095"}, + {file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dae0ed02f6b075426accbf6b2863c3d0a7eacc1b41fb40f2251d931e50188dad"}, + {file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:ab31a88a651039a07a3ae327d68ebdd8bc589b16938c09ef3f32a4b809dc96ef"}, + {file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:df2e6f546c4df14bc81f9498bbc007fbb87669f1bb707c6138878c46b06f6510"}, + {file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5dd1537e7cc06efd81371f5d1a992bd5ab156b2b4f88834ca852de4a8ea523fa"}, + {file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9b9ec9c9978b708d488bec36b9e4c94d88fd12ccac3e62134a9d17ddba910ea9"}, + {file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8e77c69d5892cb5ba71703c4057091e31ccf534bd7f129307a4d084d90d014b8"}, + {file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a8d5c70e04aac1eda5c829a26d1f75c6e5286c74743133d9f742cda8e53b9c2f"}, + {file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c94e75445b00319c1fad60f3c98b09cd63fe1134a8a953dcd48989ef42318534"}, + {file = "lxml-5.2.1-cp311-cp311-win32.whl", hash = "sha256:4951e4f7a5680a2db62f7f4ab2f84617674d36d2d76a729b9a8be4b59b3659be"}, + {file = "lxml-5.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:5c670c0406bdc845b474b680b9a5456c561c65cf366f8db5a60154088c92d102"}, + {file = "lxml-5.2.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:abc25c3cab9ec7fcd299b9bcb3b8d4a1231877e425c650fa1c7576c5107ab851"}, + {file = "lxml-5.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6935bbf153f9a965f1e07c2649c0849d29832487c52bb4a5c5066031d8b44fd5"}, + {file = "lxml-5.2.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d793bebb202a6000390a5390078e945bbb49855c29c7e4d56a85901326c3b5d9"}, + {file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd5562927cdef7c4f5550374acbc117fd4ecc05b5007bdfa57cc5355864e0a4"}, + {file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0e7259016bc4345a31af861fdce942b77c99049d6c2107ca07dc2bba2435c1d9"}, + {file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:530e7c04f72002d2f334d5257c8a51bf409db0316feee7c87e4385043be136af"}, + {file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59689a75ba8d7ffca577aefd017d08d659d86ad4585ccc73e43edbfc7476781a"}, + {file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f9737bf36262046213a28e789cc82d82c6ef19c85a0cf05e75c670a33342ac2c"}, + {file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:3a74c4f27167cb95c1d4af1c0b59e88b7f3e0182138db2501c353555f7ec57f4"}, + {file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:68a2610dbe138fa8c5826b3f6d98a7cfc29707b850ddcc3e21910a6fe51f6ca0"}, + {file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f0a1bc63a465b6d72569a9bba9f2ef0334c4e03958e043da1920299100bc7c08"}, + {file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c2d35a1d047efd68027817b32ab1586c1169e60ca02c65d428ae815b593e65d4"}, + {file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:79bd05260359170f78b181b59ce871673ed01ba048deef4bf49a36ab3e72e80b"}, + {file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:865bad62df277c04beed9478fe665b9ef63eb28fe026d5dedcb89b537d2e2ea6"}, + {file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:44f6c7caff88d988db017b9b0e4ab04934f11e3e72d478031efc7edcac6c622f"}, + {file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:71e97313406ccf55d32cc98a533ee05c61e15d11b99215b237346171c179c0b0"}, + {file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:057cdc6b86ab732cf361f8b4d8af87cf195a1f6dc5b0ff3de2dced242c2015e0"}, + {file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f3bbbc998d42f8e561f347e798b85513ba4da324c2b3f9b7969e9c45b10f6169"}, + {file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:491755202eb21a5e350dae00c6d9a17247769c64dcf62d8c788b5c135e179dc4"}, + {file = "lxml-5.2.1-cp312-cp312-win32.whl", hash = "sha256:8de8f9d6caa7f25b204fc861718815d41cbcf27ee8f028c89c882a0cf4ae4134"}, + {file = "lxml-5.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:f2a9efc53d5b714b8df2b4b3e992accf8ce5bbdfe544d74d5c6766c9e1146a3a"}, + {file = "lxml-5.2.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:70a9768e1b9d79edca17890175ba915654ee1725975d69ab64813dd785a2bd5c"}, + {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c38d7b9a690b090de999835f0443d8aa93ce5f2064035dfc48f27f02b4afc3d0"}, + {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5670fb70a828663cc37552a2a85bf2ac38475572b0e9b91283dc09efb52c41d1"}, + {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:958244ad566c3ffc385f47dddde4145088a0ab893504b54b52c041987a8c1863"}, + {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b6241d4eee5f89453307c2f2bfa03b50362052ca0af1efecf9fef9a41a22bb4f"}, + {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:2a66bf12fbd4666dd023b6f51223aed3d9f3b40fef06ce404cb75bafd3d89536"}, + {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:9123716666e25b7b71c4e1789ec829ed18663152008b58544d95b008ed9e21e9"}, + {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:0c3f67e2aeda739d1cc0b1102c9a9129f7dc83901226cc24dd72ba275ced4218"}, + {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:5d5792e9b3fb8d16a19f46aa8208987cfeafe082363ee2745ea8b643d9cc5b45"}, + {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:88e22fc0a6684337d25c994381ed8a1580a6f5ebebd5ad41f89f663ff4ec2885"}, + {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:21c2e6b09565ba5b45ae161b438e033a86ad1736b8c838c766146eff8ceffff9"}, + {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_2_s390x.whl", hash = "sha256:afbbdb120d1e78d2ba8064a68058001b871154cc57787031b645c9142b937a62"}, + {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:627402ad8dea044dde2eccde4370560a2b750ef894c9578e1d4f8ffd54000461"}, + {file = "lxml-5.2.1-cp36-cp36m-win32.whl", hash = "sha256:e89580a581bf478d8dcb97d9cd011d567768e8bc4095f8557b21c4d4c5fea7d0"}, + {file = "lxml-5.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:59565f10607c244bc4c05c0c5fa0c190c990996e0c719d05deec7030c2aa8289"}, + {file = "lxml-5.2.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:857500f88b17a6479202ff5fe5f580fc3404922cd02ab3716197adf1ef628029"}, + {file = "lxml-5.2.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:56c22432809085b3f3ae04e6e7bdd36883d7258fcd90e53ba7b2e463efc7a6af"}, + {file = "lxml-5.2.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a55ee573116ba208932e2d1a037cc4b10d2c1cb264ced2184d00b18ce585b2c0"}, + {file = "lxml-5.2.1-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:6cf58416653c5901e12624e4013708b6e11142956e7f35e7a83f1ab02f3fe456"}, + {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:64c2baa7774bc22dd4474248ba16fe1a7f611c13ac6123408694d4cc93d66dbd"}, + {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:74b28c6334cca4dd704e8004cba1955af0b778cf449142e581e404bd211fb619"}, + {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:7221d49259aa1e5a8f00d3d28b1e0b76031655ca74bb287123ef56c3db92f213"}, + {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3dbe858ee582cbb2c6294dc85f55b5f19c918c2597855e950f34b660f1a5ede6"}, + {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:04ab5415bf6c86e0518d57240a96c4d1fcfc3cb370bb2ac2a732b67f579e5a04"}, + {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:6ab833e4735a7e5533711a6ea2df26459b96f9eec36d23f74cafe03631647c41"}, + {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f443cdef978430887ed55112b491f670bba6462cea7a7742ff8f14b7abb98d75"}, + {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:9e2addd2d1866fe112bc6f80117bcc6bc25191c5ed1bfbcf9f1386a884252ae8"}, + {file = "lxml-5.2.1-cp37-cp37m-win32.whl", hash = "sha256:f51969bac61441fd31f028d7b3b45962f3ecebf691a510495e5d2cd8c8092dbd"}, + {file = "lxml-5.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:b0b58fbfa1bf7367dde8a557994e3b1637294be6cf2169810375caf8571a085c"}, + {file = "lxml-5.2.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3e183c6e3298a2ed5af9d7a356ea823bccaab4ec2349dc9ed83999fd289d14d5"}, + {file = "lxml-5.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:804f74efe22b6a227306dd890eecc4f8c59ff25ca35f1f14e7482bbce96ef10b"}, + {file = "lxml-5.2.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:08802f0c56ed150cc6885ae0788a321b73505d2263ee56dad84d200cab11c07a"}, + {file = "lxml-5.2.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f8c09ed18ecb4ebf23e02b8e7a22a05d6411911e6fabef3a36e4f371f4f2585"}, + {file = "lxml-5.2.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3d30321949861404323c50aebeb1943461a67cd51d4200ab02babc58bd06a86"}, + {file = "lxml-5.2.1-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:b560e3aa4b1d49e0e6c847d72665384db35b2f5d45f8e6a5c0072e0283430533"}, + {file = "lxml-5.2.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:058a1308914f20784c9f4674036527e7c04f7be6fb60f5d61353545aa7fcb739"}, + {file = "lxml-5.2.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:adfb84ca6b87e06bc6b146dc7da7623395db1e31621c4785ad0658c5028b37d7"}, + {file = "lxml-5.2.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:417d14450f06d51f363e41cace6488519038f940676ce9664b34ebf5653433a5"}, + {file = "lxml-5.2.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a2dfe7e2473f9b59496247aad6e23b405ddf2e12ef0765677b0081c02d6c2c0b"}, + {file = "lxml-5.2.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bf2e2458345d9bffb0d9ec16557d8858c9c88d2d11fed53998512504cd9df49b"}, + {file = "lxml-5.2.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:58278b29cb89f3e43ff3e0c756abbd1518f3ee6adad9e35b51fb101c1c1daaec"}, + {file = "lxml-5.2.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:64641a6068a16201366476731301441ce93457eb8452056f570133a6ceb15fca"}, + {file = "lxml-5.2.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:78bfa756eab503673991bdcf464917ef7845a964903d3302c5f68417ecdc948c"}, + {file = "lxml-5.2.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:11a04306fcba10cd9637e669fd73aa274c1c09ca64af79c041aa820ea992b637"}, + {file = "lxml-5.2.1-cp38-cp38-win32.whl", hash = "sha256:66bc5eb8a323ed9894f8fa0ee6cb3e3fb2403d99aee635078fd19a8bc7a5a5da"}, + {file = "lxml-5.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:9676bfc686fa6a3fa10cd4ae6b76cae8be26eb5ec6811d2a325636c460da1806"}, + {file = "lxml-5.2.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cf22b41fdae514ee2f1691b6c3cdeae666d8b7fa9434de445f12bbeee0cf48dd"}, + {file = "lxml-5.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ec42088248c596dbd61d4ae8a5b004f97a4d91a9fd286f632e42e60b706718d7"}, + {file = "lxml-5.2.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd53553ddad4a9c2f1f022756ae64abe16da1feb497edf4d9f87f99ec7cf86bd"}, + {file = "lxml-5.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feaa45c0eae424d3e90d78823f3828e7dc42a42f21ed420db98da2c4ecf0a2cb"}, + {file = "lxml-5.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddc678fb4c7e30cf830a2b5a8d869538bc55b28d6c68544d09c7d0d8f17694dc"}, + {file = "lxml-5.2.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:853e074d4931dbcba7480d4dcab23d5c56bd9607f92825ab80ee2bd916edea53"}, + {file = "lxml-5.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc4691d60512798304acb9207987e7b2b7c44627ea88b9d77489bbe3e6cc3bd4"}, + {file = "lxml-5.2.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:beb72935a941965c52990f3a32d7f07ce869fe21c6af8b34bf6a277b33a345d3"}, + {file = "lxml-5.2.1-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:6588c459c5627fefa30139be4d2e28a2c2a1d0d1c265aad2ba1935a7863a4913"}, + {file = "lxml-5.2.1-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:588008b8497667f1ddca7c99f2f85ce8511f8f7871b4a06ceede68ab62dff64b"}, + {file = "lxml-5.2.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b6787b643356111dfd4032b5bffe26d2f8331556ecb79e15dacb9275da02866e"}, + {file = "lxml-5.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7c17b64b0a6ef4e5affae6a3724010a7a66bda48a62cfe0674dabd46642e8b54"}, + {file = "lxml-5.2.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:27aa20d45c2e0b8cd05da6d4759649170e8dfc4f4e5ef33a34d06f2d79075d57"}, + {file = "lxml-5.2.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:d4f2cc7060dc3646632d7f15fe68e2fa98f58e35dd5666cd525f3b35d3fed7f8"}, + {file = "lxml-5.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff46d772d5f6f73564979cd77a4fffe55c916a05f3cb70e7c9c0590059fb29ef"}, + {file = "lxml-5.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:96323338e6c14e958d775700ec8a88346014a85e5de73ac7967db0367582049b"}, + {file = "lxml-5.2.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:52421b41ac99e9d91934e4d0d0fe7da9f02bfa7536bb4431b4c05c906c8c6919"}, + {file = "lxml-5.2.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:7a7efd5b6d3e30d81ec68ab8a88252d7c7c6f13aaa875009fe3097eb4e30b84c"}, + {file = "lxml-5.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0ed777c1e8c99b63037b91f9d73a6aad20fd035d77ac84afcc205225f8f41188"}, + {file = "lxml-5.2.1-cp39-cp39-win32.whl", hash = "sha256:644df54d729ef810dcd0f7732e50e5ad1bd0a135278ed8d6bcb06f33b6b6f708"}, + {file = "lxml-5.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:9ca66b8e90daca431b7ca1408cae085d025326570e57749695d6a01454790e95"}, + {file = "lxml-5.2.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9b0ff53900566bc6325ecde9181d89afadc59c5ffa39bddf084aaedfe3b06a11"}, + {file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd6037392f2d57793ab98d9e26798f44b8b4da2f2464388588f48ac52c489ea1"}, + {file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b9c07e7a45bb64e21df4b6aa623cb8ba214dfb47d2027d90eac197329bb5e94"}, + {file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3249cc2989d9090eeac5467e50e9ec2d40704fea9ab72f36b034ea34ee65ca98"}, + {file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f42038016852ae51b4088b2862126535cc4fc85802bfe30dea3500fdfaf1864e"}, + {file = "lxml-5.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:533658f8fbf056b70e434dff7e7aa611bcacb33e01f75de7f821810e48d1bb66"}, + {file = "lxml-5.2.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:622020d4521e22fb371e15f580d153134bfb68d6a429d1342a25f051ec72df1c"}, + {file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efa7b51824aa0ee957ccd5a741c73e6851de55f40d807f08069eb4c5a26b2baa"}, + {file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c6ad0fbf105f6bcc9300c00010a2ffa44ea6f555df1a2ad95c88f5656104817"}, + {file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e233db59c8f76630c512ab4a4daf5a5986da5c3d5b44b8e9fc742f2a24dbd460"}, + {file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6a014510830df1475176466b6087fc0c08b47a36714823e58d8b8d7709132a96"}, + {file = "lxml-5.2.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:d38c8f50ecf57f0463399569aa388b232cf1a2ffb8f0a9a5412d0db57e054860"}, + {file = "lxml-5.2.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5aea8212fb823e006b995c4dda533edcf98a893d941f173f6c9506126188860d"}, + {file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff097ae562e637409b429a7ac958a20aab237a0378c42dabaa1e3abf2f896e5f"}, + {file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f5d65c39f16717a47c36c756af0fb36144069c4718824b7533f803ecdf91138"}, + {file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3d0c3dd24bb4605439bf91068598d00c6370684f8de4a67c2992683f6c309d6b"}, + {file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e32be23d538753a8adb6c85bd539f5fd3b15cb987404327c569dfc5fd8366e85"}, + {file = "lxml-5.2.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cc518cea79fd1e2f6c90baafa28906d4309d24f3a63e801d855e7424c5b34144"}, + {file = "lxml-5.2.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a0af35bd8ebf84888373630f73f24e86bf016642fb8576fba49d3d6b560b7cbc"}, + {file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8aca2e3a72f37bfc7b14ba96d4056244001ddcc18382bd0daa087fd2e68a354"}, + {file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ca1e8188b26a819387b29c3895c47a5e618708fe6f787f3b1a471de2c4a94d9"}, + {file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c8ba129e6d3b0136a0f50345b2cb3db53f6bda5dd8c7f5d83fbccba97fb5dcb5"}, + {file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e998e304036198b4f6914e6a1e2b6f925208a20e2042563d9734881150c6c246"}, + {file = "lxml-5.2.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d3be9b2076112e51b323bdf6d5a7f8a798de55fb8d95fcb64bd179460cdc0704"}, + {file = "lxml-5.2.1.tar.gz", hash = "sha256:3f7765e69bbce0906a7c74d5fe46d2c7a7596147318dbc08e4a2431f3060e306"}, ] [package.extras] cssselect = ["cssselect (>=0.7)"] +html-clean = ["lxml-html-clean"] html5 = ["html5lib"] htmlsoup = ["BeautifulSoup4"] -source = ["Cython (>=3.0.7)"] +source = ["Cython (>=3.0.10)"] [[package]] name = "markdown" -version = "3.3.7" -description = "Python implementation of Markdown." +version = "3.5.2" +description = "Python implementation of John Gruber's Markdown." optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "Markdown-3.3.7-py3-none-any.whl", hash = "sha256:f5da449a6e1c989a4cea2631aa8ee67caa5a2ef855d551c88f9e309f4634c621"}, - {file = "Markdown-3.3.7.tar.gz", hash = "sha256:cbb516f16218e643d8e0a95b309f77eb118cb138d39a4f27851e6a63581db874"}, + {file = "Markdown-3.5.2-py3-none-any.whl", hash = "sha256:d43323865d89fc0cb9b20c75fc8ad313af307cc087e84b657d9eec768eddeadd"}, + {file = "Markdown-3.5.2.tar.gz", hash = "sha256:e1ac7b3dc550ee80e602e71c1d168002f062e49f1b11e26a36264dafd4df2ef8"}, ] [package.dependencies] importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} [package.extras] +docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] testing = ["coverage", "pyyaml"] [[package]] @@ -1799,13 +1849,13 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] name = "markdown2" -version = "2.4.12" +version = "2.4.13" description = "A fast and complete Python implementation of Markdown" optional = false python-versions = ">=3.5, <4" files = [ - {file = "markdown2-2.4.12-py2.py3-none-any.whl", hash = "sha256:98f47591006f0ace0644cbece03fed6f3845513286f6c6e9f8bcf6a575174e2c"}, - {file = "markdown2-2.4.12.tar.gz", hash = "sha256:1bc8692696954d597778e0e25713c14ca56d87992070dedd95c17eddaf709204"}, + {file = "markdown2-2.4.13-py2.py3-none-any.whl", hash = "sha256:855bde5cbcceb9beda7c80efdf7f406c23e6079172c497fcfce22fdce998e892"}, + {file = "markdown2-2.4.13.tar.gz", hash = "sha256:18ceb56590da77f2c22382e55be48c15b3c8f0c71d6398def387275e6c347a9f"}, ] [package.extras] @@ -1962,17 +2012,18 @@ min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-imp [[package]] name = "mkdocs-autorefs" -version = "0.5.0" +version = "1.0.1" description = "Automatically link across pages in MkDocs." optional = false python-versions = ">=3.8" files = [ - {file = "mkdocs_autorefs-0.5.0-py3-none-any.whl", hash = "sha256:7930fcb8ac1249f10e683967aeaddc0af49d90702af111a5e390e8b20b3d97ff"}, - {file = "mkdocs_autorefs-0.5.0.tar.gz", hash = "sha256:9a5054a94c08d28855cfab967ada10ed5be76e2bfad642302a610b252c3274c0"}, + {file = "mkdocs_autorefs-1.0.1-py3-none-any.whl", hash = "sha256:aacdfae1ab197780fb7a2dac92ad8a3d8f7ca8049a9cbe56a4218cd52e8da570"}, + {file = "mkdocs_autorefs-1.0.1.tar.gz", hash = "sha256:f684edf847eced40b570b57846b15f0bf57fb93ac2c510450775dcf16accb971"}, ] [package.dependencies] Markdown = ">=3.3" +markupsafe = ">=2.0.1" mkdocs = ">=1.1" [[package]] @@ -2077,18 +2128,18 @@ files = [ [[package]] name = "nautobot" -version = "2.1.4" +version = "2.2.0" description = "Source of truth and network automation platform." optional = false -python-versions = ">=3.8,<3.12" +python-versions = "<3.12,>=3.8" files = [ - {file = "nautobot-2.1.4-py3-none-any.whl", hash = "sha256:b1311cb8bda428ee1b5b7074ce75ef99aaffd31a29207a69339fa92cea2de729"}, - {file = "nautobot-2.1.4.tar.gz", hash = "sha256:50e64ba399485631fc694c489b3b47a3c300f7914f8856cff2819d076474245b"}, + {file = "nautobot-2.2.0-py3-none-any.whl", hash = "sha256:918881373371661ee4fefb3177b8f28a86068164085b1383cc84966f913eca46"}, + {file = "nautobot-2.2.0.tar.gz", hash = "sha256:2232f8296d0b78885e02ab055d0b15e3a6303f633a0d0952c84c76f5978f9b4f"}, ] [package.dependencies] celery = ">=5.3.1,<5.4.0" -Django = ">=3.2.24,<3.3.0" +Django = ">=3.2.25,<3.3.0" django-ajax-tables = ">=1.1.1,<1.2.0" django-celery-beat = ">=2.5.0,<2.6.0" django-celery-results = ">=2.4.0,<2.5.0" @@ -2116,7 +2167,7 @@ graphene-django = ">=2.16.0,<2.17.0" graphene-django-optimizer = ">=0.8.0,<0.9.0" Jinja2 = ">=3.1.3,<3.2.0" jsonschema = ">=4.7.0,<4.19.0" -Markdown = ">=3.3.7,<3.4.0" +Markdown = ">=3.3.7,<3.6.0" MarkupSafe = ">=2.1.5,<2.2.0" netaddr = ">=0.8.0,<0.9.0" netutils = ">=1.6.0,<2.0.0" @@ -2132,26 +2183,27 @@ social-auth-app-django = ">=5.2.0,<5.3.0" svgwrite = ">=1.4.2,<1.5.0" [package.extras] -all = ["django-auth-ldap (>=4.3.0,<4.4.0)", "django-storages (>=1.13.2,<1.14.0)", "mysqlclient (>=2.2.3,<2.3.0)", "napalm (>=4.1.0,<4.2.0)", "social-auth-core[openidconnect,saml] (>=4.4.2,<4.5.0)"] +all = ["django-auth-ldap (>=4.3.0,<4.4.0)", "django-storages (>=1.13.2,<1.14.0)", "mysqlclient (>=2.2.3,<2.3.0)", "napalm (>=4.1.0,<4.2.0)", "social-auth-core[openidconnect,saml] (>=4.5.3,<4.6.0)"] ldap = ["django-auth-ldap (>=4.3.0,<4.4.0)"] mysql = ["mysqlclient (>=2.2.3,<2.3.0)"] napalm = ["napalm (>=4.1.0,<4.2.0)"] remote-storage = ["django-storages (>=1.13.2,<1.14.0)"] -sso = ["social-auth-core[openidconnect,saml] (>=4.4.2,<4.5.0)"] +sso = ["social-auth-core[openidconnect,saml] (>=4.5.3,<4.6.0)"] [[package]] name = "nautobot-bgp-models" -version = "0.20.1" -description = "Nautobot BGP Models Plugin" +version = "2.0.0" +description = "Nautobot BGP Models App" optional = true python-versions = ">=3.8,<3.12" files = [ - {file = "nautobot_bgp_models-0.20.1-py3-none-any.whl", hash = "sha256:d670a80aa5073cb11a7d560d9282ffa1e7cc2a1810702514793ce846225fafdd"}, - {file = "nautobot_bgp_models-0.20.1.tar.gz", hash = "sha256:ca78171f6e91a946f9ba075a87704494ddbd4d65c386d7db2f841628b29c3552"}, + {file = "nautobot_bgp_models-2.0.0-py3-none-any.whl", hash = "sha256:2d8ac457a29ec6cf0d7bf99320ddddb8f0232302e5d09b044e5708b9c6824c8c"}, + {file = "nautobot_bgp_models-2.0.0.tar.gz", hash = "sha256:97dc0b3179a5548c05a8ea20ee46e2c0e5a2fb7218c66a4ff8c609c374ef9199"}, ] [package.dependencies] -nautobot = ">=2.0.0,<3.0.0" +nautobot = ">=2.0.3,<3.0.0" +toml = ">=0.10.2,<0.11.0" [[package]] name = "netaddr" @@ -2166,13 +2218,13 @@ files = [ [[package]] name = "netutils" -version = "1.6.0" +version = "1.8.0" description = "Common helper functions useful in network automation." optional = false -python-versions = ">=3.8,<4.0" +python-versions = "<4.0,>=3.8" files = [ - {file = "netutils-1.6.0-py3-none-any.whl", hash = "sha256:e755e6141d0968f1deeb61693a4023f4f5fe1f0dde25d94ac1008f8191d8d237"}, - {file = "netutils-1.6.0.tar.gz", hash = "sha256:bd2fa691e172fe9d5c9e6fc5e2593316eb7fd2c36450454894ed13b274763d70"}, + {file = "netutils-1.8.0-py3-none-any.whl", hash = "sha256:5e705793528d8e771edae6648b15c9f9a7c3cfc9c749299f6ff4a35454545858"}, + {file = "netutils-1.8.0.tar.gz", hash = "sha256:d5e0205c2e8f095314cf755f4dbda956db42a97502501824c6c4764726eda93f"}, ] [package.extras] @@ -2180,27 +2232,27 @@ optionals = ["jsonschema (>=4.17.3,<5.0.0)", "napalm (>=4.0.0,<5.0.0)"] [[package]] name = "nh3" -version = "0.2.15" +version = "0.2.17" description = "Python bindings to the ammonia HTML sanitization library." optional = false python-versions = "*" files = [ - {file = "nh3-0.2.15-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:9c0d415f6b7f2338f93035bba5c0d8c1b464e538bfbb1d598acd47d7969284f0"}, - {file = "nh3-0.2.15-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:6f42f99f0cf6312e470b6c09e04da31f9abaadcd3eb591d7d1a88ea931dca7f3"}, - {file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac19c0d68cd42ecd7ead91a3a032fdfff23d29302dbb1311e641a130dfefba97"}, - {file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f0d77272ce6d34db6c87b4f894f037d55183d9518f948bba236fe81e2bb4e28"}, - {file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8d595df02413aa38586c24811237e95937ef18304e108b7e92c890a06793e3bf"}, - {file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86e447a63ca0b16318deb62498db4f76fc60699ce0a1231262880b38b6cff911"}, - {file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3277481293b868b2715907310c7be0f1b9d10491d5adf9fce11756a97e97eddf"}, - {file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60684857cfa8fdbb74daa867e5cad3f0c9789415aba660614fe16cd66cbb9ec7"}, - {file = "nh3-0.2.15-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3b803a5875e7234907f7d64777dfde2b93db992376f3d6d7af7f3bc347deb305"}, - {file = "nh3-0.2.15-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0d02d0ff79dfd8208ed25a39c12cbda092388fff7f1662466e27d97ad011b770"}, - {file = "nh3-0.2.15-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f3b53ba93bb7725acab1e030bc2ecd012a817040fd7851b332f86e2f9bb98dc6"}, - {file = "nh3-0.2.15-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:b1e97221cedaf15a54f5243f2c5894bb12ca951ae4ddfd02a9d4ea9df9e1a29d"}, - {file = "nh3-0.2.15-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5167a6403d19c515217b6bcaaa9be420974a6ac30e0da9e84d4fc67a5d474c5"}, - {file = "nh3-0.2.15-cp37-abi3-win32.whl", hash = "sha256:427fecbb1031db085eaac9931362adf4a796428ef0163070c484b5a768e71601"}, - {file = "nh3-0.2.15-cp37-abi3-win_amd64.whl", hash = "sha256:bc2d086fb540d0fa52ce35afaded4ea526b8fc4d3339f783db55c95de40ef02e"}, - {file = "nh3-0.2.15.tar.gz", hash = "sha256:d1e30ff2d8d58fb2a14961f7aac1bbb1c51f9bdd7da727be35c63826060b0bf3"}, + {file = "nh3-0.2.17-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:551672fd71d06cd828e282abdb810d1be24e1abb7ae2543a8fa36a71c1006fe9"}, + {file = "nh3-0.2.17-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c551eb2a3876e8ff2ac63dff1585236ed5dfec5ffd82216a7a174f7c5082a78a"}, + {file = "nh3-0.2.17-cp37-abi3-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:66f17d78826096291bd264f260213d2b3905e3c7fae6dfc5337d49429f1dc9f3"}, + {file = "nh3-0.2.17-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0316c25b76289cf23be6b66c77d3608a4fdf537b35426280032f432f14291b9a"}, + {file = "nh3-0.2.17-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:22c26e20acbb253a5bdd33d432a326d18508a910e4dcf9a3316179860d53345a"}, + {file = "nh3-0.2.17-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:85cdbcca8ef10733bd31f931956f7fbb85145a4d11ab9e6742bbf44d88b7e351"}, + {file = "nh3-0.2.17-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:40015514022af31975c0b3bca4014634fa13cb5dc4dbcbc00570acc781316dcc"}, + {file = "nh3-0.2.17-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ba73a2f8d3a1b966e9cdba7b211779ad8a2561d2dba9674b8a19ed817923f65f"}, + {file = "nh3-0.2.17-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c21bac1a7245cbd88c0b0e4a420221b7bfa838a2814ee5bb924e9c2f10a1120b"}, + {file = "nh3-0.2.17-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d7a25fd8c86657f5d9d576268e3b3767c5cd4f42867c9383618be8517f0f022a"}, + {file = "nh3-0.2.17-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:c790769152308421283679a142dbdb3d1c46c79c823008ecea8e8141db1a2062"}, + {file = "nh3-0.2.17-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:b4427ef0d2dfdec10b641ed0bdaf17957eb625b2ec0ea9329b3d28806c153d71"}, + {file = "nh3-0.2.17-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a3f55fabe29164ba6026b5ad5c3151c314d136fd67415a17660b4aaddacf1b10"}, + {file = "nh3-0.2.17-cp37-abi3-win32.whl", hash = "sha256:1a814dd7bba1cb0aba5bcb9bebcc88fd801b63e21e2450ae6c52d3b3336bc911"}, + {file = "nh3-0.2.17-cp37-abi3-win_amd64.whl", hash = "sha256:1aa52a7def528297f256de0844e8dd680ee279e79583c76d6fa73a978186ddfb"}, + {file = "nh3-0.2.17.tar.gz", hash = "sha256:40d0741a19c3d645e54efba71cb0d8c475b59135c1e3c580f879ad5514cbf028"}, ] [[package]] @@ -2221,13 +2273,13 @@ signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] [[package]] name = "packaging" -version = "23.2" +version = "24.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" files = [ - {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, - {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, ] [[package]] @@ -2256,18 +2308,18 @@ dev = ["jinja2"] [[package]] name = "parso" -version = "0.8.3" +version = "0.8.4" description = "A Python Parser" optional = false python-versions = ">=3.6" files = [ - {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, - {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, + {file = "parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18"}, + {file = "parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d"}, ] [package.extras] -qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] -testing = ["docopt", "pytest (<6.0.0)"] +qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] +testing = ["docopt", "pytest"] [[package]] name = "pathspec" @@ -2504,7 +2556,6 @@ files = [ {file = "psycopg2_binary-2.9.9-cp311-cp311-win32.whl", hash = "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d"}, {file = "psycopg2_binary-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996"}, @@ -2513,8 +2564,6 @@ files = [ {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-win32.whl", hash = "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab"}, {file = "psycopg2_binary-2.9.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a"}, {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9"}, {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77"}, @@ -2579,24 +2628,24 @@ tests = ["pytest"] [[package]] name = "pycodestyle" -version = "2.9.1" +version = "2.11.1" description = "Python style guide checker" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, - {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, + {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, + {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, ] [[package]] name = "pycparser" -version = "2.21" +version = "2.22" description = "C parser in Python" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.8" files = [ - {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, - {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] [[package]] @@ -2618,13 +2667,13 @@ toml = ["tomli (>=1.2.3)"] [[package]] name = "pyflakes" -version = "2.5.0" +version = "3.2.0" description = "passive checker of Python programs" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, - {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, + {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, + {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, ] [[package]] @@ -2661,23 +2710,23 @@ tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] [[package]] name = "pylint" -version = "2.17.7" +version = "3.1.0" description = "python code static checker" optional = false -python-versions = ">=3.7.2" +python-versions = ">=3.8.0" files = [ - {file = "pylint-2.17.7-py3-none-any.whl", hash = "sha256:27a8d4c7ddc8c2f8c18aa0050148f89ffc09838142193fdbe98f172781a3ff87"}, - {file = "pylint-2.17.7.tar.gz", hash = "sha256:f4fcac7ae74cfe36bc8451e931d8438e4a476c20314b1101c458ad0f05191fad"}, + {file = "pylint-3.1.0-py3-none-any.whl", hash = "sha256:507a5b60953874766d8a366e8e8c7af63e058b26345cfcb5f91f89d987fd6b74"}, + {file = "pylint-3.1.0.tar.gz", hash = "sha256:6a69beb4a6f63debebaab0a3477ecd0f559aa726af4954fc948c51f7a2549e23"}, ] [package.dependencies] -astroid = ">=2.15.8,<=2.17.0-dev0" +astroid = ">=3.1.0,<=3.2.0-dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = [ {version = ">=0.2", markers = "python_version < \"3.11\""}, {version = ">=0.3.6", markers = "python_version >= \"3.11\""}, ] -isort = ">=4.2.5,<6" +isort = ">=4.2.5,<5.13.0 || >5.13.0,<6" mccabe = ">=0.6,<0.8" platformdirs = ">=2.2.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} @@ -2708,20 +2757,20 @@ with-django = ["Django (>=2.2)"] [[package]] name = "pylint-nautobot" -version = "0.2.1" +version = "0.3.0" description = "Custom Pylint Rules for Nautobot" optional = false -python-versions = ">=3.7,<4.0" +python-versions = ">=3.8,<3.12" files = [ - {file = "pylint_nautobot-0.2.1-py3-none-any.whl", hash = "sha256:6656cd571d6e997e6d7e37631308f1de25949a596a8309ab6d47a2e387c892c6"}, - {file = "pylint_nautobot-0.2.1.tar.gz", hash = "sha256:2872106a29236b0e31293efe4a2d02a66527c67f33437f3e2345251c4cf71b4d"}, + {file = "pylint_nautobot-0.3.0-py3-none-any.whl", hash = "sha256:91fed48d9a9f565c6aa46c679b930d64b06d014061f6e7e802e6de8b6b8e3f87"}, + {file = "pylint_nautobot-0.3.0.tar.gz", hash = "sha256:387a1d73b49186a7b325b6c1a3634e2c57ec0f2350e93494304c47073400099b"}, ] [package.dependencies] -importlib-resources = ">=5.12.0,<6.0.0" -pylint = ">=2.13,<3.0" -pyyaml = ">=6.0,<7.0" -tomli = ">=2.0.1,<3.0.0" +importlib-resources = ">=5.12.0" +pylint = ">=2.17.5" +pyyaml = ">=6.0.1" +toml = ">=0.10.2" [[package]] name = "pylint-plugin-utils" @@ -2739,17 +2788,17 @@ pylint = ">=1.7" [[package]] name = "pymdown-extensions" -version = "10.4" +version = "10.7.1" description = "Extension pack for Python Markdown." optional = false python-versions = ">=3.8" files = [ - {file = "pymdown_extensions-10.4-py3-none-any.whl", hash = "sha256:cfc28d6a09d19448bcbf8eee3ce098c7d17ff99f7bd3069db4819af181212037"}, - {file = "pymdown_extensions-10.4.tar.gz", hash = "sha256:bc46f11749ecd4d6b71cf62396104b4a200bad3498cb0f5dad1b8502fe461a35"}, + {file = "pymdown_extensions-10.7.1-py3-none-any.whl", hash = "sha256:f5cc7000d7ff0d1ce9395d216017fa4df3dde800afb1fb72d1c7d3fd35e710f4"}, + {file = "pymdown_extensions-10.7.1.tar.gz", hash = "sha256:c70e146bdd83c744ffc766b4671999796aba18842b268510a329f7f64700d584"}, ] [package.dependencies] -markdown = ">=3.2" +markdown = ">=3.5" pyyaml = "*" [package.extras] @@ -2793,13 +2842,13 @@ cron-schedule = ["croniter"] [[package]] name = "python-dateutil" -version = "2.8.2" +version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ - {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, - {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, ] [package.dependencies] @@ -2915,7 +2964,6 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -2923,15 +2971,8 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -2948,7 +2989,6 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -2956,7 +2996,6 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -2993,17 +3032,17 @@ pyquery = ">=1.2" [[package]] name = "redis" -version = "5.0.1" +version = "5.0.3" description = "Python client for Redis database and key-value store" optional = false python-versions = ">=3.7" files = [ - {file = "redis-5.0.1-py3-none-any.whl", hash = "sha256:ed4802971884ae19d640775ba3b03aa2e7bd5e8fb8dfaed2decce4d0fc48391f"}, - {file = "redis-5.0.1.tar.gz", hash = "sha256:0dab495cd5753069d3bc650a0dde8a8f9edde16fc5691b689a566eda58100d0f"}, + {file = "redis-5.0.3-py3-none-any.whl", hash = "sha256:5da9b8fe9e1254293756c16c008e8620b3d15fcc6dde6babde9541850e72a32d"}, + {file = "redis-5.0.3.tar.gz", hash = "sha256:4973bae7444c0fbed64a06b87446f79361cb7e4ec1538c022d696ed7a5015580"}, ] [package.dependencies] -async-timeout = {version = ">=4.0.2", markers = "python_full_version <= \"3.11.2\""} +async-timeout = {version = ">=4.0.3", markers = "python_full_version < \"3.11.3\""} [package.extras] hiredis = ["hiredis (>=1.0.0)"] @@ -3011,13 +3050,13 @@ ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)" [[package]] name = "referencing" -version = "0.33.0" +version = "0.34.0" description = "JSON Referencing + Python" optional = false python-versions = ">=3.8" files = [ - {file = "referencing-0.33.0-py3-none-any.whl", hash = "sha256:39240f2ecc770258f28b642dd47fd74bc8b02484de54e1882b74b35ebd779bd5"}, - {file = "referencing-0.33.0.tar.gz", hash = "sha256:c775fedf74bc0f9189c2a3be1c12fd03e8c23f4d371dce795df44e06c5b412f7"}, + {file = "referencing-0.34.0-py3-none-any.whl", hash = "sha256:d53ae300ceddd3169f1ffa9caf2cb7b769e92657e4fafb23d34b93679116dfd4"}, + {file = "referencing-0.34.0.tar.gz", hash = "sha256:5773bd84ef41799a5a8ca72dc34590c041eb01bf9aa02632b4a973fb0181a844"}, ] [package.dependencies] @@ -3149,13 +3188,13 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "requests-oauthlib" -version = "1.3.1" +version = "2.0.0" description = "OAuthlib authentication support for Requests." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.4" files = [ - {file = "requests-oauthlib-1.3.1.tar.gz", hash = "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a"}, - {file = "requests_oauthlib-1.3.1-py2.py3-none-any.whl", hash = "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5"}, + {file = "requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9"}, + {file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"}, ] [package.dependencies] @@ -3167,13 +3206,13 @@ rsa = ["oauthlib[signedtoken] (>=3.0.0)"] [[package]] name = "rich" -version = "13.7.0" +version = "13.7.1" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.7.0" files = [ - {file = "rich-13.7.0-py3-none-any.whl", hash = "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235"}, - {file = "rich-13.7.0.tar.gz", hash = "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa"}, + {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, + {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, ] [package.dependencies] @@ -3339,6 +3378,20 @@ files = [ {file = "smmap-5.0.1.tar.gz", hash = "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62"}, ] +[[package]] +name = "snakeviz" +version = "2.2.0" +description = "A web-based viewer for Python profiler output" +optional = false +python-versions = ">=3.7" +files = [ + {file = "snakeviz-2.2.0-py2.py3-none-any.whl", hash = "sha256:569e2d71c47f80a886aa6e70d6405cb6d30aa3520969ad956b06f824c5f02b8e"}, + {file = "snakeviz-2.2.0.tar.gz", hash = "sha256:7bfd00be7ae147eb4a170a471578e1cd3f41f803238958b6b8efcf2c698a6aa9"}, +] + +[package.dependencies] +tornado = ">=2.0" + [[package]] name = "snowballstemmer" version = "2.2.0" @@ -3439,13 +3492,13 @@ tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] [[package]] name = "stevedore" -version = "5.1.0" +version = "5.2.0" description = "Manage dynamic plugins for Python applications" optional = false python-versions = ">=3.8" files = [ - {file = "stevedore-5.1.0-py3-none-any.whl", hash = "sha256:8cc040628f3cea5d7128f2e76cf486b2251a4e543c7b938f58d9a377f6694a2d"}, - {file = "stevedore-5.1.0.tar.gz", hash = "sha256:a54534acf9b89bc7ed264807013b505bf07f74dbe4bcfa37d32bd063870b087c"}, + {file = "stevedore-5.2.0-py3-none-any.whl", hash = "sha256:1c15d95766ca0569cad14cb6272d4d31dae66b011a929d7c18219c176ea1b5c9"}, + {file = "stevedore-5.2.0.tar.gz", hash = "sha256:46b93ca40e1114cea93d738a6c1e365396981bb6bb78c27045b7587c9473544d"}, ] [package.dependencies] @@ -3497,39 +3550,59 @@ files = [ [[package]] name = "tomlkit" -version = "0.12.3" +version = "0.12.4" description = "Style preserving TOML library" optional = false python-versions = ">=3.7" files = [ - {file = "tomlkit-0.12.3-py3-none-any.whl", hash = "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba"}, - {file = "tomlkit-0.12.3.tar.gz", hash = "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4"}, + {file = "tomlkit-0.12.4-py3-none-any.whl", hash = "sha256:5cd82d48a3dd89dee1f9d64420aa20ae65cfbd00668d6f094d7578a78efbb77b"}, + {file = "tomlkit-0.12.4.tar.gz", hash = "sha256:7ca1cfc12232806517a8515047ba66a19369e71edf2439d0f5824f91032b6cc3"}, +] + +[[package]] +name = "tornado" +version = "6.4" +description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +optional = false +python-versions = ">= 3.8" +files = [ + {file = "tornado-6.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:02ccefc7d8211e5a7f9e8bc3f9e5b0ad6262ba2fbb683a6443ecc804e5224ce0"}, + {file = "tornado-6.4-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:27787de946a9cffd63ce5814c33f734c627a87072ec7eed71f7fc4417bb16263"}, + {file = "tornado-6.4-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7894c581ecdcf91666a0912f18ce5e757213999e183ebfc2c3fdbf4d5bd764e"}, + {file = "tornado-6.4-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e43bc2e5370a6a8e413e1e1cd0c91bedc5bd62a74a532371042a18ef19e10579"}, + {file = "tornado-6.4-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0251554cdd50b4b44362f73ad5ba7126fc5b2c2895cc62b14a1c2d7ea32f212"}, + {file = "tornado-6.4-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fd03192e287fbd0899dd8f81c6fb9cbbc69194d2074b38f384cb6fa72b80e9c2"}, + {file = "tornado-6.4-cp38-abi3-musllinux_1_1_i686.whl", hash = "sha256:88b84956273fbd73420e6d4b8d5ccbe913c65d31351b4c004ae362eba06e1f78"}, + {file = "tornado-6.4-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:71ddfc23a0e03ef2df1c1397d859868d158c8276a0603b96cf86892bff58149f"}, + {file = "tornado-6.4-cp38-abi3-win32.whl", hash = "sha256:6f8a6c77900f5ae93d8b4ae1196472d0ccc2775cc1dfdc9e7727889145c45052"}, + {file = "tornado-6.4-cp38-abi3-win_amd64.whl", hash = "sha256:10aeaa8006333433da48dec9fe417877f8bcc21f48dda8d661ae79da357b2a63"}, + {file = "tornado-6.4.tar.gz", hash = "sha256:72291fa6e6bc84e626589f1c29d90a5a6d593ef5ae68052ee2ef000dfd273dee"}, ] [[package]] name = "traitlets" -version = "5.14.1" +version = "5.14.2" description = "Traitlets Python configuration system" optional = false python-versions = ">=3.8" files = [ - {file = "traitlets-5.14.1-py3-none-any.whl", hash = "sha256:2e5a030e6eff91737c643231bfcf04a65b0132078dad75e4936700b213652e74"}, - {file = "traitlets-5.14.1.tar.gz", hash = "sha256:8585105b371a04b8316a43d5ce29c098575c2e477850b62b848b964f1444527e"}, + {file = "traitlets-5.14.2-py3-none-any.whl", hash = "sha256:fcdf85684a772ddeba87db2f398ce00b40ff550d1528c03c14dbf6a02003cd80"}, + {file = "traitlets-5.14.2.tar.gz", hash = "sha256:8cdd83c040dab7d1dee822678e5f5d100b514f7b72b01615b26fc5718916fdf9"}, ] [package.extras] docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] -test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<7.5)", "pytest-mock", "pytest-mypy-testing"] +test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.1)", "pytest-mock", "pytest-mypy-testing"] [[package]] name = "typing-extensions" -version = "4.9.0" +version = "4.11.0" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, - {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, + {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, + {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, ] [[package]] @@ -3556,13 +3629,13 @@ files = [ [[package]] name = "urllib3" -version = "2.2.0" +version = "2.2.1" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ - {file = "urllib3-2.2.0-py3-none-any.whl", hash = "sha256:ce3711610ddce217e6d113a2732fafad960a03fd0318c91faa79481e35c11224"}, - {file = "urllib3-2.2.0.tar.gz", hash = "sha256:051d961ad0c62a94e50ecf1af379c3aba230c66c710493493560c0c223c49f20"}, + {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, + {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, ] [package.extras] @@ -3635,93 +3708,28 @@ files = [ ] [[package]] -name = "wrapt" -version = "1.16.0" -description = "Module for decorators, wrappers and monkey patching." +name = "wheel" +version = "0.43.0" +description = "A built-package format for Python" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, - {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, - {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, - {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, - {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, - {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, - {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, - {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, - {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, - {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, - {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, - {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, - {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, - {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, - {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, - {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, - {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, - {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, - {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, - {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, - {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, - {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, - {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, - {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, - {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, - {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, - {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, - {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, - {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, - {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, - {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"}, - {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"}, - {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"}, - {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"}, - {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"}, - {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"}, - {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"}, - {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"}, - {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"}, - {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"}, - {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"}, - {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"}, - {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"}, - {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"}, - {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"}, - {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"}, - {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"}, - {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"}, - {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"}, - {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"}, - {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"}, - {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"}, - {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"}, - {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"}, - {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"}, - {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"}, - {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"}, - {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"}, - {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, - {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, - {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, - {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, - {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"}, - {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, - {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, - {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, - {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, - {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, - {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, - {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, + {file = "wheel-0.43.0-py3-none-any.whl", hash = "sha256:55c570405f142630c6b9f72fe09d9b67cf1477fcf543ae5b8dcb1f5b7377da81"}, + {file = "wheel-0.43.0.tar.gz", hash = "sha256:465ef92c69fa5c5da2d1cf8ac40559a8c940886afcef87dcf14b9470862f1d85"}, ] +[package.extras] +test = ["pytest (>=6.0.0)", "setuptools (>=65)"] + [[package]] name = "yamllint" -version = "1.35.0" +version = "1.35.1" description = "A linter for YAML files." optional = false python-versions = ">=3.8" files = [ - {file = "yamllint-1.35.0-py3-none-any.whl", hash = "sha256:601b0adaaac6d9bacb16a2e612e7ee8d23caf941ceebf9bfe2cff0f196266004"}, - {file = "yamllint-1.35.0.tar.gz", hash = "sha256:9bc99c3e9fe89b4c6ee26e17aa817cf2d14390de6577cb6e2e6ed5f72120c835"}, + {file = "yamllint-1.35.1-py3-none-any.whl", hash = "sha256:2e16e504bb129ff515b37823b472750b36b6de07963bd74b307341ef5ad8bdc3"}, + {file = "yamllint-1.35.1.tar.gz", hash = "sha256:7a003809f88324fd2c877734f2d575ee7881dd9043360657cc8049c809eba6cd"}, ] [package.dependencies] @@ -3733,18 +3741,18 @@ dev = ["doc8", "flake8", "flake8-import-order", "rstcheck[sphinx]", "sphinx"] [[package]] name = "zipp" -version = "3.17.0" +version = "3.18.1" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" files = [ - {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, - {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, + {file = "zipp-3.18.1-py3-none-any.whl", hash = "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b"}, + {file = "zipp-3.18.1.tar.gz", hash = "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] [extras] contrib = ["nautobot-bgp-models"] @@ -3752,5 +3760,5 @@ nautobot = ["nautobot"] [metadata] lock-version = "2.0" -python-versions = ">=3.8,<3.12" -content-hash = "3535a436c6dcd2aa57df4ba773c9707e6177161b2d6d4bbc34910c05330f8622" +python-versions = ">=3.8.1,<3.12" +content-hash = "c8415924cb148d2fe28616c682ddc4bcba054fe8bc4b40af85e879870a3ca133" diff --git a/pyproject.toml b/pyproject.toml index 2dbce4ab..d8e57ad4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "nautobot-design-builder" -version = "1.0.0" +version = "1.1.0" description = "Nautobot app that uses design templates to easily create data objects in Nautobot with minimal input from a user." authors = ["Network to Code, LLC "] license = "Apache-2.0" @@ -26,7 +26,7 @@ packages = [ ] [tool.poetry.dependencies] -python = ">=3.8,<3.12" +python = ">=3.8.1,<3.12" # Used for local development nautobot = ">=1.6.0,<=2.9999" nautobot-bgp-models = { version = "*", optional = true } @@ -58,6 +58,7 @@ mkdocs-version-annotations = "1.0.0" mkdocstrings = "0.22.0" mkdocstrings-python = "1.5.2" gitpython = "^3.1.41" +snakeviz = "^2.2.0" [tool.poetry.extras] nautobot = ["nautobot"] @@ -103,8 +104,12 @@ disable = """, duplicate-code, too-many-lines, too-many-ancestors, - line-too-long - """ + line-too-long, + nb-replaced-site, + nb-replaced-device-role, + nb-code-location-changed, + nb-code-location-changed-object, +""" [tool.pylint.miscellaneous] # Don't flag TODO as a failure, let us commit with things that still need to be done in the code @@ -115,7 +120,7 @@ notes = """, [tool.pylint-nautobot] supported_nautobot_versions = [ - "1.6.0" + "1" ] [tool.pydocstyle]