diff --git a/nautobot_device_onboarding/diffsync/adapters/sync_network_data_adapters.py b/nautobot_device_onboarding/diffsync/adapters/sync_network_data_adapters.py index b4305509..b0d735b8 100644 --- a/nautobot_device_onboarding/diffsync/adapters/sync_network_data_adapters.py +++ b/nautobot_device_onboarding/diffsync/adapters/sync_network_data_adapters.py @@ -1,7 +1,10 @@ """DiffSync adapters.""" +import datetime + import diffsync from diffsync.enum import DiffSyncModelFlags +from django.conf import settings from django.core.exceptions import ValidationError from nautobot.dcim.models import Interface from nautobot.ipam.models import VLAN, VRF, IPAddress @@ -12,6 +15,8 @@ from nautobot_device_onboarding.nornir_plays.command_getter import sync_network_data_command_getter from nautobot_device_onboarding.utils import diffsync_utils +app_settings = settings.PLUGINS_CONFIG["nautobot_device_onboarding"] + class FilteredNautobotAdapter(NautobotAdapter): """ @@ -101,8 +106,6 @@ def load_ip_addresses(self): try: network_ip_address.model_flags = DiffSyncModelFlags.SKIP_UNMATCHED_DST self.add(network_ip_address) - if self.job.debug: - self.job.logger.debug(f"{network_ip_address} loaded.") except diffsync.exceptions.ObjectAlreadyExists: self.job.logger.warning( f"{network_ip_address} is already loaded to the DiffSync store. This is a duplicate IP Address." @@ -124,8 +127,6 @@ def load_vlans(self): try: network_vlan.model_flags = DiffSyncModelFlags.SKIP_UNMATCHED_DST self.add(network_vlan) - if self.job.debug: - self.job.logger.debug(f"Vlan {network_vlan} loaded.") except diffsync.exceptions.ObjectAlreadyExists: pass @@ -151,8 +152,6 @@ def load_tagged_vlans_to_interface(self): ) network_tagged_vlans_to_interface.model_flags = DiffSyncModelFlags.SKIP_UNMATCHED_DST self.add(network_tagged_vlans_to_interface) - if self.job.debug: - self.job.logger.debug(f"Tagged Vlan to interface: {network_tagged_vlans_to_interface} loaded.") def load_untagged_vlan_to_interface(self): """ @@ -174,8 +173,6 @@ def load_untagged_vlan_to_interface(self): ) network_untagged_vlan_to_interface.model_flags = DiffSyncModelFlags.SKIP_UNMATCHED_DST self.add(network_untagged_vlan_to_interface) - if self.job.debug: - self.job.logger.debug(f"Unagged Vlan to interface: {network_untagged_vlan_to_interface} loaded.") def load_lag_to_interface(self): """ @@ -192,8 +189,6 @@ def load_lag_to_interface(self): ) network_lag_to_interface.model_flags = DiffSyncModelFlags.SKIP_UNMATCHED_DST self.add(network_lag_to_interface) - if self.job.debug: - self.job.logger.debug(f"Lag to interface {network_lag_to_interface} loaded.") def load_vrfs(self): """ @@ -210,10 +205,8 @@ def load_vrfs(self): try: network_vrf.model_flags = DiffSyncModelFlags.SKIP_UNMATCHED_DST self.add(network_vrf) - if self.job.debug: - self.job.logger.debug(f"Vrf {network_vrf} loaded.") except diffsync.exceptions.ObjectAlreadyExists: - pass + continue def load_vrf_to_interface(self): """ @@ -234,8 +227,6 @@ def load_vrf_to_interface(self): ) network_vrf_to_interface.model_flags = DiffSyncModelFlags.SKIP_UNMATCHED_DST self.add(network_vrf_to_interface) - if self.job.debug: - self.job.logger.debug(f"Vrf to interface: {network_vrf_to_interface} loaded.") def load(self): """Generic implementation of the load function.""" @@ -373,13 +364,19 @@ def _handle_failed_devices(self, device_data): self.job.command_getter_result = device_data self.job.devices_to_load = diffsync_utils.generate_device_queryset_from_command_getter_result(device_data) + def _handle_general_load_exception(self, error, hostname, data, model_type): + """If a diffsync model fails to load, log the error.""" + self.job.logger.error(f"Failed to load {model_type} model for {hostname}. {error}, {error.args}") + if self.job.debug: + self.job.logger.debug(f"HOSTNAME: {hostname}, DATA: {data}") + def execute_command_getter(self): """Start the CommandGetterDO job to query devices for data.""" result = sync_network_data_command_getter( self.job.job_result, self.job.logger.getEffectiveLevel(), self.job.job_result.task_kwargs ) - if self.job.debug: - self.job.logger.debug(f"Command Getter Result: {result}") + # if self.job.debug: + # self.job.logger.debug(f"Command Getter Result: {result}") # verify data returned is a dict data_type_check = diffsync_utils.check_data_type(result) if self.job.debug: @@ -401,16 +398,21 @@ def _process_mac_address(self, mac_address): def load_devices(self): """Load devices into the DiffSync store.""" for hostname, device_data in self.job.command_getter_result.items(): - network_device = self.device(diffsync=self, name=hostname, serial=device_data["serial"]) - self.add(network_device) - if self.job.debug: - self.job.logger.debug(f"Device {network_device} loaded.") + try: + network_device = self.device( + diffsync=self, + name=hostname, + serial=device_data["serial"], + last_network_data_sync=datetime.datetime.now().date().isoformat(), + ) + self.add(network_device) + except Exception as err: + self._handle_general_load_exception(error=err, hostname=hostname, data=device_data, model_type="device") + continue # for interface in device_data["interfaces"]: for interface_name, interface_data in device_data["interfaces"].items(): network_interface = self.load_interface(hostname, interface_name, interface_data) network_device.add_child(network_interface) - if self.job.debug: - self.job.logger.debug(f"Interface {network_interface} loaded.") def _get_vlan_name(self, interface_data): """Given interface data returned from a device, process and return the vlan name.""" @@ -435,8 +437,6 @@ def load_interface(self, hostname, interface_name, interface_data): untagged_vlan__name=self._get_vlan_name(interface_data=interface_data), ) self.add(network_interface) - if self.job.debug: - self.job.logger.debug(f"Interface {network_interface} loaded.") return network_interface def load_ip_addresses(self): @@ -451,23 +451,27 @@ def load_ip_addresses(self): if ip_address["ip_address"]: # the ip_address and mask_length may be empty, skip these if self.job.debug: self.job.logger.debug(f"Loading {ip_address} from {interface_name} on {hostname}") - network_ip_address = self.ip_address( - diffsync=self, - host=ip_address["ip_address"], - mask_length=int(ip_address["prefix_length"]), - type="host", - ip_version=4, - status__name=self.job.ip_address_status.name, - ) try: + network_ip_address = self.ip_address( + diffsync=self, + host=ip_address["ip_address"], + mask_length=int(ip_address["prefix_length"]), + type="host", + ip_version=4, + status__name=self.job.ip_address_status.name, + ) self.add(network_ip_address) - if self.job.debug: - self.job.logger.debug(f"{network_ip_address} loaded.") except diffsync.exceptions.ObjectAlreadyExists: self.job.logger.warning( f"{network_ip_address} is already loaded to the " "DiffSync store. This is a duplicate IP Address." ) + continue + except Exception as err: + self._handle_general_load_exception( + error=err, hostname=hostname, data=device_data, model_type="ip_address" + ) + continue def load_vlans(self): """Load vlans into the Diffsync store.""" @@ -482,32 +486,38 @@ def load_vlans(self): for _, interface_data in device_data["interfaces"].items(): # add tagged vlans for tagged_vlan in interface_data["tagged_vlans"]: - network_vlan = self.vlan( - diffsync=self, - name=tagged_vlan["name"], - vid=tagged_vlan["id"], - location__name=location_names.get(hostname, ""), - ) try: + network_vlan = self.vlan( + diffsync=self, + name=tagged_vlan["name"], + vid=tagged_vlan["id"], + location__name=location_names.get(hostname, ""), + ) self.add(network_vlan) - if self.job.debug: - self.job.logger.debug(f"Tagged Vlan {network_vlan} loaded.") except diffsync.exceptions.ObjectAlreadyExists: - pass + continue + except Exception as err: + self._handle_general_load_exception( + error=err, hostname=hostname, data=device_data, model_type="vlan" + ) + continue # check for untagged vlan and add if necessary if interface_data["untagged_vlan"]: - network_vlan = self.vlan( - diffsync=self, - name=interface_data["untagged_vlan"]["name"], - vid=interface_data["untagged_vlan"]["id"], - location__name=location_names.get(hostname, ""), - ) try: + network_vlan = self.vlan( + diffsync=self, + name=interface_data["untagged_vlan"]["name"], + vid=interface_data["untagged_vlan"]["id"], + location__name=location_names.get(hostname, ""), + ) self.add(network_vlan) - if self.job.debug: - self.job.logger.debug(f"Untagged Vlan {network_vlan} loaded.") except diffsync.exceptions.ObjectAlreadyExists: - pass + continue + except Exception as err: + self._handle_general_load_exception( + error=err, hostname=hostname, data=device_data, model_type="vlan" + ) + continue def load_vrfs(self): """Load vrfs into the Diffsync store.""" @@ -517,97 +527,119 @@ def load_vrfs(self): # for interface in device_data["interfaces"]: for _, interface_data in device_data["interfaces"].items(): if interface_data["vrf"]: - network_vrf = self.vrf( - diffsync=self, - name=interface_data["vrf"]["name"], - namespace__name=self.job.namespace.name, - ) try: + network_vrf = self.vrf( + diffsync=self, + name=interface_data["vrf"]["name"], + namespace__name=self.job.namespace.name, + ) self.add(network_vrf) - if self.job.debug: - self.job.logger.debug(f"Vrf {network_vrf} loaded.") except diffsync.exceptions.ObjectAlreadyExists: - pass + continue + except Exception as err: + self._handle_general_load_exception( + error=err, hostname=hostname, data=device_data, model_type="vrf" + ) + continue def load_ip_address_to_interfaces(self): """Load ip address interface assignments into the Diffsync store.""" for hostname, device_data in self.job.command_getter_result.items(): # pylint: disable=too-many-nested-blocks - # for interface in device_data["interfaces"]: for interface_name, interface_data in device_data["interfaces"].items(): for ip_address in interface_data["ip_addresses"]: if ip_address["ip_address"]: # the ip_address and mask_length may be empty, skip these - network_ip_address_to_interface = self.ipaddress_to_interface( - diffsync=self, - interface__device__name=hostname, - interface__name=interface_name, - ip_address__host=ip_address["ip_address"], - ip_address__mask_length=( - int(ip_address["prefix_length"]) if ip_address["prefix_length"] else None - ), - ) - self.add(network_ip_address_to_interface) - if self.job.debug: - self.job.logger.debug(f"IP Address to interface {network_ip_address_to_interface} loaded.") + try: + network_ip_address_to_interface = self.ipaddress_to_interface( + diffsync=self, + interface__device__name=hostname, + interface__name=interface_name, + ip_address__host=ip_address["ip_address"], + ip_address__mask_length=( + int(ip_address["prefix_length"]) if ip_address["prefix_length"] else None + ), + ) + self.add(network_ip_address_to_interface) + except Exception as err: + self._handle_general_load_exception( + error=err, hostname=hostname, data=device_data, model_type="ip_address to interface" + ) + continue def load_tagged_vlans_to_interface(self): """Load tagged vlan to interface assignments into the Diffsync store.""" for hostname, device_data in self.job.command_getter_result.items(): # for interface in device_data["interfaces"]: for interface_name, interface_data in device_data["interfaces"].items(): - network_tagged_vlans_to_interface = self.tagged_vlans_to_interface( - diffsync=self, - device__name=hostname, - name=interface_name, - tagged_vlans=interface_data["tagged_vlans"], - ) - self.add(network_tagged_vlans_to_interface) - if self.job.debug: - self.job.logger.debug(f"Tagged Vlan to interface {network_tagged_vlans_to_interface} loaded.") + try: + network_tagged_vlans_to_interface = self.tagged_vlans_to_interface( + diffsync=self, + device__name=hostname, + name=interface_name, + tagged_vlans=interface_data["tagged_vlans"], + ) + self.add(network_tagged_vlans_to_interface) + except Exception as err: + self._handle_general_load_exception( + error=err, hostname=hostname, data=device_data, model_type="tagged vlan to interface" + ) + continue def load_untagged_vlan_to_interface(self): """Load untagged vlan to interface assignments into the Diffsync store.""" for hostname, device_data in self.job.command_getter_result.items(): # for interface in device_data["interfaces"]: for interface_name, interface_data in device_data["interfaces"].items(): - network_untagged_vlan_to_interface = self.untagged_vlan_to_interface( - diffsync=self, - device__name=hostname, - name=interface_name, - untagged_vlan=interface_data["untagged_vlan"], - ) - self.add(network_untagged_vlan_to_interface) - if self.job.debug: - self.job.logger.debug(f"Untagged Vlan to interface {network_untagged_vlan_to_interface} loaded.") + try: + network_untagged_vlan_to_interface = self.untagged_vlan_to_interface( + diffsync=self, + device__name=hostname, + name=interface_name, + untagged_vlan=interface_data["untagged_vlan"], + ) + self.add(network_untagged_vlan_to_interface) + except Exception as err: + self._handle_general_load_exception( + error=err, hostname=hostname, data=device_data, model_type="untagged vlan to interface" + ) + continue def load_lag_to_interface(self): """Load lag interface assignments into the Diffsync store.""" for hostname, device_data in self.job.command_getter_result.items(): # for interface in device_data["interfaces"]: for interface_name, interface_data in device_data["interfaces"].items(): - network_lag_to_interface = self.lag_to_interface( - diffsync=self, - device__name=hostname, - name=interface_name, - lag__interface__name=interface_data["lag"] if interface_data["lag"] else "", - ) - self.add(network_lag_to_interface) - if self.job.debug: - self.job.logger.debug(f"Lag to interface {network_lag_to_interface} loaded.") + try: + network_lag_to_interface = self.lag_to_interface( + diffsync=self, + device__name=hostname, + name=interface_name, + lag__interface__name=interface_data["lag"] if interface_data["lag"] else "", + ) + self.add(network_lag_to_interface) + except Exception as err: + self._handle_general_load_exception( + error=err, hostname=hostname, data=device_data, model_type="lag to interface" + ) + continue def load_vrf_to_interface(self): """Load Vrf to interface assignments into the Diffsync store.""" for hostname, device_data in self.job.command_getter_result.items(): # for interface in device_data["interfaces"]: for interface_name, interface_data in device_data["interfaces"].items(): - network_vrf_to_interface = self.vrf_to_interface( - diffsync=self, - device__name=hostname, - name=interface_name, - vrf=interface_data["vrf"], - ) - self.add(network_vrf_to_interface) - if self.job.debug: - self.job.logger.debug(f"Untagged Vlan to interface {network_vrf_to_interface} loaded.") + try: + network_vrf_to_interface = self.vrf_to_interface( + diffsync=self, + device__name=hostname, + name=interface_name, + vrf=interface_data["vrf"], + ) + self.add(network_vrf_to_interface) + except Exception as err: + self._handle_general_load_exception( + error=err, hostname=hostname, data=device_data, model_type="vrf to interface" + ) + continue def load(self): """Load network data.""" diff --git a/nautobot_device_onboarding/diffsync/models/sync_network_data_models.py b/nautobot_device_onboarding/diffsync/models/sync_network_data_models.py index da35ac22..c0403180 100644 --- a/nautobot_device_onboarding/diffsync/models/sync_network_data_models.py +++ b/nautobot_device_onboarding/diffsync/models/sync_network_data_models.py @@ -2,6 +2,11 @@ from typing import List, Optional +try: + from typing import Annotated # Python>=3.9 +except ModuleNotFoundError: + from typing_extensions import Annotated # Python<3.9 + from diffsync import DiffSync, DiffSyncModel from diffsync import exceptions as diffsync_exceptions from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist, ValidationError @@ -9,7 +14,7 @@ from nautobot.dcim.models import Device, Interface, Location from nautobot.extras.models import Status from nautobot.ipam.models import VLAN, VRF, IPAddress, IPAddressToInterface -from nautobot_ssot.contrib import NautobotModel +from nautobot_ssot.contrib import CustomFieldAnnotation, NautobotModel from nautobot_device_onboarding.utils import diffsync_utils @@ -48,11 +53,16 @@ class SyncNetworkDataDevice(FilteredNautobotModel): "name", "serial", ) + _attributes = ("last_network_data_sync",) _children = {"interface": "interfaces"} name: str serial: str + last_network_data_sync: Annotated[ + Optional[str], CustomFieldAnnotation(key="last_network_data_sync", name="last_network_data_sync") + ] + interfaces: List["SyncNetworkDataInterface"] = [] @classmethod diff --git a/nautobot_device_onboarding/jobs.py b/nautobot_device_onboarding/jobs.py index 73c1561e..0f76a5c5 100755 --- a/nautobot_device_onboarding/jobs.py +++ b/nautobot_device_onboarding/jobs.py @@ -7,13 +7,14 @@ from diffsync.enum import DiffSyncFlags from django.conf import settings +from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.forms import HiddenInput from nautobot.apps.jobs import BooleanVar, FileVar, IntegerVar, Job, MultiObjectVar, ObjectVar, StringVar from nautobot.core.celery import register_jobs from nautobot.dcim.models import Device, DeviceType, Location, Platform -from nautobot.extras.choices import SecretsGroupAccessTypeChoices, SecretsGroupSecretTypeChoices -from nautobot.extras.models import Role, SecretsGroup, SecretsGroupAssociation, Status +from nautobot.extras.choices import CustomFieldTypeChoices, SecretsGroupAccessTypeChoices, SecretsGroupSecretTypeChoices +from nautobot.extras.models import CustomField, Role, SecretsGroup, SecretsGroupAssociation, Status from nautobot.ipam.models import Namespace from nautobot_ssot.jobs.base import DataSource @@ -614,6 +615,24 @@ def run( self.sync_vlans = sync_vlans self.sync_vrfs = sync_vrfs + # Check for last_network_data_sync CustomField + if self.debug: + self.logger.debug("Checking for last_network_data_sync custom field") + try: + cf, _ = CustomField.objects.get_or_create( + label="Last Network Data Sync", + key="last_network_data_sync", + type=CustomFieldTypeChoices.TYPE_DATE, + required=False, + ) + + cf.content_types.add(ContentType.objects.get_for_model(Device)) + if self.debug: + self.logger.debug("Custom field found or created") + except Exception as err: + self.logger.error(f"Failed to get or create last_network_data_sync custom field, {err}") + return + # Filter devices based on form input device_filter = {} if self.devices: @@ -634,7 +653,7 @@ def run( self.logger.info("No devices returned based on filter selections.") return - # Log selected devices + # Log the devices that will be synced filtered_devices_names = list(self.filtered_devices.values_list("name", flat=True)) self.logger.info(f"{len(filtered_devices_names)} devices will be synced") if len(filtered_devices_names) <= 300: diff --git a/poetry.lock b/poetry.lock index a8214d7e..f68428fe 100755 --- a/poetry.lock +++ b/poetry.lock @@ -1930,13 +1930,9 @@ files = [ {file = "lxml-5.2.2-cp36-cp36m-win_amd64.whl", hash = "sha256:edcfa83e03370032a489430215c1e7783128808fd3e2e0a3225deee278585196"}, {file = "lxml-5.2.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:28bf95177400066596cdbcfc933312493799382879da504633d16cf60bba735b"}, {file = "lxml-5.2.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a745cc98d504d5bd2c19b10c79c61c7c3df9222629f1b6210c0368177589fb8"}, - {file = "lxml-5.2.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b590b39ef90c6b22ec0be925b211298e810b4856909c8ca60d27ffbca6c12e6"}, {file = "lxml-5.2.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b336b0416828022bfd5a2e3083e7f5ba54b96242159f83c7e3eebaec752f1716"}, - {file = "lxml-5.2.2-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:c2faf60c583af0d135e853c86ac2735ce178f0e338a3c7f9ae8f622fd2eb788c"}, {file = "lxml-5.2.2-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:4bc6cb140a7a0ad1f7bc37e018d0ed690b7b6520ade518285dc3171f7a117905"}, - {file = "lxml-5.2.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7ff762670cada8e05b32bf1e4dc50b140790909caa8303cfddc4d702b71ea184"}, {file = "lxml-5.2.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:57f0a0bbc9868e10ebe874e9f129d2917750adf008fe7b9c1598c0fbbfdde6a6"}, - {file = "lxml-5.2.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:a6d2092797b388342c1bc932077ad232f914351932353e2e8706851c870bca1f"}, {file = "lxml-5.2.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:60499fe961b21264e17a471ec296dcbf4365fbea611bf9e303ab69db7159ce61"}, {file = "lxml-5.2.2-cp37-cp37m-win32.whl", hash = "sha256:d9b342c76003c6b9336a80efcc766748a333573abf9350f4094ee46b006ec18f"}, {file = "lxml-5.2.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b16db2770517b8799c79aa80f4053cd6f8b716f21f8aca962725a9565ce3ee40"}, @@ -3343,13 +3339,12 @@ cp2110 = ["hidapi"] [[package]] name = "python-crontab" -version = "3.0.0" +version = "3.1.0" description = "Python Crontab API" optional = false python-versions = "*" files = [ - {file = "python-crontab-3.0.0.tar.gz", hash = "sha256:79fb7465039ddfd4fb93d072d6ee0d45c1ac8bf1597f0686ea14fd4361dba379"}, - {file = "python_crontab-3.0.0-py3-none-any.whl", hash = "sha256:6d5ba3c190ec76e4d252989a1644fcb233dbf53fbc8fceeb9febe1657b9fb1d4"}, + {file = "python-crontab-3.1.0.tar.gz", hash = "sha256:f4ea1605d24533b67fa7a634ef26cb59a5f2e7954f6e677d2d7a2229959a2fc8"}, ] [package.dependencies] @@ -3496,6 +3491,7 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -3910,28 +3906,28 @@ files = [ [[package]] name = "ruff" -version = "0.4.4" +version = "0.4.5" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.4.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:29d44ef5bb6a08e235c8249294fa8d431adc1426bfda99ed493119e6f9ea1bf6"}, - {file = "ruff-0.4.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c4efe62b5bbb24178c950732ddd40712b878a9b96b1d02b0ff0b08a090cbd891"}, - {file = "ruff-0.4.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c8e2f1e8fc12d07ab521a9005d68a969e167b589cbcaee354cb61e9d9de9c15"}, - {file = "ruff-0.4.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:60ed88b636a463214905c002fa3eaab19795679ed55529f91e488db3fe8976ab"}, - {file = "ruff-0.4.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b90fc5e170fc71c712cc4d9ab0e24ea505c6a9e4ebf346787a67e691dfb72e85"}, - {file = "ruff-0.4.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8e7e6ebc10ef16dcdc77fd5557ee60647512b400e4a60bdc4849468f076f6eef"}, - {file = "ruff-0.4.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9ddb2c494fb79fc208cd15ffe08f32b7682519e067413dbaf5f4b01a6087bcd"}, - {file = "ruff-0.4.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c51c928a14f9f0a871082603e25a1588059b7e08a920f2f9fa7157b5bf08cfe9"}, - {file = "ruff-0.4.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5eb0a4bfd6400b7d07c09a7725e1a98c3b838be557fee229ac0f84d9aa49c36"}, - {file = "ruff-0.4.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b1867ee9bf3acc21778dcb293db504692eda5f7a11a6e6cc40890182a9f9e595"}, - {file = "ruff-0.4.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1aecced1269481ef2894cc495647392a34b0bf3e28ff53ed95a385b13aa45768"}, - {file = "ruff-0.4.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9da73eb616b3241a307b837f32756dc20a0b07e2bcb694fec73699c93d04a69e"}, - {file = "ruff-0.4.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:958b4ea5589706a81065e2a776237de2ecc3e763342e5cc8e02a4a4d8a5e6f95"}, - {file = "ruff-0.4.4-py3-none-win32.whl", hash = "sha256:cb53473849f011bca6e754f2cdf47cafc9c4f4ff4570003a0dad0b9b6890e876"}, - {file = "ruff-0.4.4-py3-none-win_amd64.whl", hash = "sha256:424e5b72597482543b684c11def82669cc6b395aa8cc69acc1858b5ef3e5daae"}, - {file = "ruff-0.4.4-py3-none-win_arm64.whl", hash = "sha256:39df0537b47d3b597293edbb95baf54ff5b49589eb7ff41926d8243caa995ea6"}, - {file = "ruff-0.4.4.tar.gz", hash = "sha256:f87ea42d5cdebdc6a69761a9d0bc83ae9b3b30d0ad78952005ba6568d6c022af"}, + {file = "ruff-0.4.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8f58e615dec58b1a6b291769b559e12fdffb53cc4187160a2fc83250eaf54e96"}, + {file = "ruff-0.4.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:84dd157474e16e3a82745d2afa1016c17d27cb5d52b12e3d45d418bcc6d49264"}, + {file = "ruff-0.4.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25f483ad9d50b00e7fd577f6d0305aa18494c6af139bce7319c68a17180087f4"}, + {file = "ruff-0.4.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:63fde3bf6f3ad4e990357af1d30e8ba2730860a954ea9282c95fc0846f5f64af"}, + {file = "ruff-0.4.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78e3ba4620dee27f76bbcad97067766026c918ba0f2d035c2fc25cbdd04d9c97"}, + {file = "ruff-0.4.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:441dab55c568e38d02bbda68a926a3d0b54f5510095c9de7f95e47a39e0168aa"}, + {file = "ruff-0.4.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1169e47e9c4136c997f08f9857ae889d614c5035d87d38fda9b44b4338909cdf"}, + {file = "ruff-0.4.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:755ac9ac2598a941512fc36a9070a13c88d72ff874a9781493eb237ab02d75df"}, + {file = "ruff-0.4.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4b02a65985be2b34b170025a8b92449088ce61e33e69956ce4d316c0fe7cce0"}, + {file = "ruff-0.4.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:75a426506a183d9201e7e5664de3f6b414ad3850d7625764106f7b6d0486f0a1"}, + {file = "ruff-0.4.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6e1b139b45e2911419044237d90b60e472f57285950e1492c757dfc88259bb06"}, + {file = "ruff-0.4.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a6f29a8221d2e3d85ff0c7b4371c0e37b39c87732c969b4d90f3dad2e721c5b1"}, + {file = "ruff-0.4.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d6ef817124d72b54cc923f3444828ba24fa45c3164bc9e8f1813db2f3d3a8a11"}, + {file = "ruff-0.4.5-py3-none-win32.whl", hash = "sha256:aed8166c18b1a169a5d3ec28a49b43340949e400665555b51ee06f22813ef062"}, + {file = "ruff-0.4.5-py3-none-win_amd64.whl", hash = "sha256:b0b03c619d2b4350b4a27e34fd2ac64d0dabe1afbf43de57d0f9d8a05ecffa45"}, + {file = "ruff-0.4.5-py3-none-win_arm64.whl", hash = "sha256:9d15de3425f53161b3f5a5658d4522e4eee5ea002bf2ac7aa380743dd9ad5fba"}, + {file = "ruff-0.4.5.tar.gz", hash = "sha256:286eabd47e7d4d521d199cab84deca135557e6d1e0f0d01c29e757c3cb151b54"}, ] [[package]] @@ -4346,40 +4342,43 @@ files = [ [[package]] name = "watchdog" -version = "4.0.0" +version = "4.0.1" description = "Filesystem events monitoring" optional = false python-versions = ">=3.8" files = [ - {file = "watchdog-4.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:39cb34b1f1afbf23e9562501673e7146777efe95da24fab5707b88f7fb11649b"}, - {file = "watchdog-4.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c522392acc5e962bcac3b22b9592493ffd06d1fc5d755954e6be9f4990de932b"}, - {file = "watchdog-4.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6c47bdd680009b11c9ac382163e05ca43baf4127954c5f6d0250e7d772d2b80c"}, - {file = "watchdog-4.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8350d4055505412a426b6ad8c521bc7d367d1637a762c70fdd93a3a0d595990b"}, - {file = "watchdog-4.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c17d98799f32e3f55f181f19dd2021d762eb38fdd381b4a748b9f5a36738e935"}, - {file = "watchdog-4.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4986db5e8880b0e6b7cd52ba36255d4793bf5cdc95bd6264806c233173b1ec0b"}, - {file = "watchdog-4.0.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:11e12fafb13372e18ca1bbf12d50f593e7280646687463dd47730fd4f4d5d257"}, - {file = "watchdog-4.0.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5369136a6474678e02426bd984466343924d1df8e2fd94a9b443cb7e3aa20d19"}, - {file = "watchdog-4.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76ad8484379695f3fe46228962017a7e1337e9acadafed67eb20aabb175df98b"}, - {file = "watchdog-4.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:45cc09cc4c3b43fb10b59ef4d07318d9a3ecdbff03abd2e36e77b6dd9f9a5c85"}, - {file = "watchdog-4.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:eed82cdf79cd7f0232e2fdc1ad05b06a5e102a43e331f7d041e5f0e0a34a51c4"}, - {file = "watchdog-4.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ba30a896166f0fee83183cec913298151b73164160d965af2e93a20bbd2ab605"}, - {file = "watchdog-4.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d18d7f18a47de6863cd480734613502904611730f8def45fc52a5d97503e5101"}, - {file = "watchdog-4.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2895bf0518361a9728773083908801a376743bcc37dfa252b801af8fd281b1ca"}, - {file = "watchdog-4.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:87e9df830022488e235dd601478c15ad73a0389628588ba0b028cb74eb72fed8"}, - {file = "watchdog-4.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:6e949a8a94186bced05b6508faa61b7adacc911115664ccb1923b9ad1f1ccf7b"}, - {file = "watchdog-4.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:6a4db54edea37d1058b08947c789a2354ee02972ed5d1e0dca9b0b820f4c7f92"}, - {file = "watchdog-4.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d31481ccf4694a8416b681544c23bd271f5a123162ab603c7d7d2dd7dd901a07"}, - {file = "watchdog-4.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8fec441f5adcf81dd240a5fe78e3d83767999771630b5ddfc5867827a34fa3d3"}, - {file = "watchdog-4.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:6a9c71a0b02985b4b0b6d14b875a6c86ddea2fdbebd0c9a720a806a8bbffc69f"}, - {file = "watchdog-4.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:557ba04c816d23ce98a06e70af6abaa0485f6d94994ec78a42b05d1c03dcbd50"}, - {file = "watchdog-4.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:d0f9bd1fd919134d459d8abf954f63886745f4660ef66480b9d753a7c9d40927"}, - {file = "watchdog-4.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:f9b2fdca47dc855516b2d66eef3c39f2672cbf7e7a42e7e67ad2cbfcd6ba107d"}, - {file = "watchdog-4.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:73c7a935e62033bd5e8f0da33a4dcb763da2361921a69a5a95aaf6c93aa03a87"}, - {file = "watchdog-4.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6a80d5cae8c265842c7419c560b9961561556c4361b297b4c431903f8c33b269"}, - {file = "watchdog-4.0.0-py3-none-win32.whl", hash = "sha256:8f9a542c979df62098ae9c58b19e03ad3df1c9d8c6895d96c0d51da17b243b1c"}, - {file = "watchdog-4.0.0-py3-none-win_amd64.whl", hash = "sha256:f970663fa4f7e80401a7b0cbeec00fa801bf0287d93d48368fc3e6fa32716245"}, - {file = "watchdog-4.0.0-py3-none-win_ia64.whl", hash = "sha256:9a03e16e55465177d416699331b0f3564138f1807ecc5f2de9d55d8f188d08c7"}, - {file = "watchdog-4.0.0.tar.gz", hash = "sha256:e3e7065cbdabe6183ab82199d7a4f6b3ba0a438c5a512a68559846ccb76a78ec"}, + {file = "watchdog-4.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:da2dfdaa8006eb6a71051795856bedd97e5b03e57da96f98e375682c48850645"}, + {file = "watchdog-4.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e93f451f2dfa433d97765ca2634628b789b49ba8b504fdde5837cdcf25fdb53b"}, + {file = "watchdog-4.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ef0107bbb6a55f5be727cfc2ef945d5676b97bffb8425650dadbb184be9f9a2b"}, + {file = "watchdog-4.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:17e32f147d8bf9657e0922c0940bcde863b894cd871dbb694beb6704cfbd2fb5"}, + {file = "watchdog-4.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:03e70d2df2258fb6cb0e95bbdbe06c16e608af94a3ffbd2b90c3f1e83eb10767"}, + {file = "watchdog-4.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:123587af84260c991dc5f62a6e7ef3d1c57dfddc99faacee508c71d287248459"}, + {file = "watchdog-4.0.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:093b23e6906a8b97051191a4a0c73a77ecc958121d42346274c6af6520dec175"}, + {file = "watchdog-4.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:611be3904f9843f0529c35a3ff3fd617449463cb4b73b1633950b3d97fa4bfb7"}, + {file = "watchdog-4.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:62c613ad689ddcb11707f030e722fa929f322ef7e4f18f5335d2b73c61a85c28"}, + {file = "watchdog-4.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d4925e4bf7b9bddd1c3de13c9b8a2cdb89a468f640e66fbfabaf735bd85b3e35"}, + {file = "watchdog-4.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cad0bbd66cd59fc474b4a4376bc5ac3fc698723510cbb64091c2a793b18654db"}, + {file = "watchdog-4.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a3c2c317a8fb53e5b3d25790553796105501a235343f5d2bf23bb8649c2c8709"}, + {file = "watchdog-4.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c9904904b6564d4ee8a1ed820db76185a3c96e05560c776c79a6ce5ab71888ba"}, + {file = "watchdog-4.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:667f3c579e813fcbad1b784db7a1aaa96524bed53437e119f6a2f5de4db04235"}, + {file = "watchdog-4.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d10a681c9a1d5a77e75c48a3b8e1a9f2ae2928eda463e8d33660437705659682"}, + {file = "watchdog-4.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0144c0ea9997b92615af1d94afc0c217e07ce2c14912c7b1a5731776329fcfc7"}, + {file = "watchdog-4.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:998d2be6976a0ee3a81fb8e2777900c28641fb5bfbd0c84717d89bca0addcdc5"}, + {file = "watchdog-4.0.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e7921319fe4430b11278d924ef66d4daa469fafb1da679a2e48c935fa27af193"}, + {file = "watchdog-4.0.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:f0de0f284248ab40188f23380b03b59126d1479cd59940f2a34f8852db710625"}, + {file = "watchdog-4.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bca36be5707e81b9e6ce3208d92d95540d4ca244c006b61511753583c81c70dd"}, + {file = "watchdog-4.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:ab998f567ebdf6b1da7dc1e5accfaa7c6992244629c0fdaef062f43249bd8dee"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:dddba7ca1c807045323b6af4ff80f5ddc4d654c8bce8317dde1bd96b128ed253"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_armv7l.whl", hash = "sha256:4513ec234c68b14d4161440e07f995f231be21a09329051e67a2118a7a612d2d"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_i686.whl", hash = "sha256:4107ac5ab936a63952dea2a46a734a23230aa2f6f9db1291bf171dac3ebd53c6"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_ppc64.whl", hash = "sha256:6e8c70d2cd745daec2a08734d9f63092b793ad97612470a0ee4cbb8f5f705c57"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:f27279d060e2ab24c0aa98363ff906d2386aa6c4dc2f1a374655d4e02a6c5e5e"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_s390x.whl", hash = "sha256:f8affdf3c0f0466e69f5b3917cdd042f89c8c63aebdb9f7c078996f607cdb0f5"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ac7041b385f04c047fcc2951dc001671dee1b7e0615cde772e84b01fbf68ee84"}, + {file = "watchdog-4.0.1-py3-none-win32.whl", hash = "sha256:206afc3d964f9a233e6ad34618ec60b9837d0582b500b63687e34011e15bb429"}, + {file = "watchdog-4.0.1-py3-none-win_amd64.whl", hash = "sha256:7577b3c43e5909623149f76b099ac49a1a01ca4e167d1785c76eb52fa585745a"}, + {file = "watchdog-4.0.1-py3-none-win_ia64.whl", hash = "sha256:d7b9f5f3299e8dd230880b6c55504a1f69cf1e4316275d1b215ebdd8187ec88d"}, + {file = "watchdog-4.0.1.tar.gz", hash = "sha256:eebaacf674fa25511e8867028d281e602ee6500045b57f43b08778082f7f8b44"}, ] [package.extras]