From 6d19a8f451728fe3aaae07565f90fd46124c25dd Mon Sep 17 00:00:00 2001 From: James Williams Date: Thu, 28 May 2026 16:20:45 -0500 Subject: [PATCH 1/3] Bump version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 978fdc61..118397d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pyntc" -version = "3.0.2" +version = "3.0.3a0" description = "Python library focused on tasks related to device level and OS management." authors = ["Network to Code, LLC "] readme = "README.md" From 44ac9d9c030d95bc8036ac343150beeb8e86ab35 Mon Sep 17 00:00:00 2001 From: James Williams Date: Mon, 29 Jun 2026 16:45:57 -0500 Subject: [PATCH 2/3] NAPPS-1175 | Adds IOS-XR OS Upgrades Support (#398) --- changes/398.added | 1 + docs/user/lib_overview.md | 52 ++ mkdocs.yml | 1 + pyntc/devices/__init__.py | 2 + pyntc/devices/iosxr_device.py | 931 +++++++++++++++++++ pyproject.toml | 2 +- tests/unit/conftest.py | 11 +- tests/unit/test_devices/test_f5_device.py | 6 + tests/unit/test_devices/test_iosxr_device.py | 612 ++++++++++++ tests/unit/test_infra.py | 14 +- 10 files changed, 1618 insertions(+), 14 deletions(-) create mode 100644 changes/398.added create mode 100644 pyntc/devices/iosxr_device.py create mode 100644 tests/unit/test_devices/test_iosxr_device.py diff --git a/changes/398.added b/changes/398.added new file mode 100644 index 00000000..e01c13de --- /dev/null +++ b/changes/398.added @@ -0,0 +1 @@ +Added native Cisco IOS-XR (eXR / 64-bit) support via the `cisco_iosxr_ssh` driver (`IOSXRDevice`), including `remote_file_copy` (FTP/TFTP/SCP/HTTP/HTTPS) and OS upgrades from a single golden ISO (built with Cisco's gisobuild tool) through the asynchronous `install add` / `install activate` / `install commit` workflow. diff --git a/docs/user/lib_overview.md b/docs/user/lib_overview.md index 30f29070..6a7770e4 100644 --- a/docs/user/lib_overview.md +++ b/docs/user/lib_overview.md @@ -15,7 +15,59 @@ It's main purpose is to simplify the execution of common tasks including: - Cisco AireOS - uses netmiko (SSH) - Cisco ASA - uses netmiko (SSH) - Cisco IOS platforms - uses netmiko (SSH) +- Cisco IOS-XR (eXR / 64-bit) - uses netmiko (SSH) - Cisco NX-OS - uses pynxos (NX-API) - Arista EOS - uses pyeapi (eAPI) - Juniper Junos - uses PyEz (NETCONF) - F5 Networks - uses f5-sdk (ReST) + +!!! note "IOS-XR upgrades require a golden ISO" + The `cisco_iosxr_ssh` driver (`IOSXRDevice`) performs eXR OS upgrades using the + asynchronous native install workflow (`install add` → poll → `install activate` → + poll → reload → `install commit` → verify). + + The driver upgrades from a single **golden ISO**. On eXR a bare base ISO + **cannot be activated on its own** when optional feature packages (IS-IS, OSPF, + MPLS, multicast, etc.) are active: `install activate` aborts demanding the + matching-version RPMs be activated in the same operation. A golden ISO solves + this by bundling the base XR image and the matching feature RPMs into one file, + so it activates cleanly without supplying separate packages: + + ```python + device.install_os("ncs5k-golden-x-7.11.2-NTC7112.iso") + ``` + + The golden ISO must already be staged on `harddisk:` (use `remote_file_copy`). + Installing a base ISO plus separate feature RPMs is **not** supported by this + driver. + +!!! tip "Building a golden ISO with gisobuild" + Build a golden ISO with Cisco's [gisobuild](https://github.com/ios-xr/gisobuild) + tool. Provide the platform's base (`mini`) ISO, a repository of the matching + feature RPMs you want bundled, a label (which becomes part of the resulting + filename), and an output directory: + + ```bash + ntc@linux-server:~/xr/gisobuild$ mkdir -p /home/ntc/xr/7.11.1/giso_out + + ./src/gisobuild.py \ + --iso /home/ntc/xr/7.11.1/ncs5k-mini-x-7.11.1.iso \ + --repo /home/ntc/xr/7.11.1/ \ + --pkglist ncs5k-isis-1.0.0.0-r7111.x86_64.rpm \ + ncs5k-ospf-1.0.0.0-r7111.x86_64.rpm \ + ncs5k-mpls-1.0.0.0-r7111.x86_64.rpm \ + ncs5k-mpls-te-rsvp-1.0.0.0-r7111.x86_64.rpm \ + ncs5k-mcast-1.0.0.0-r7111.x86_64.rpm \ + ncs5k-mgbl-1.0.0.0-r7111.x86_64.rpm \ + ncs5k-m2m-1.0.0.0-r7111.x86_64.rpm \ + --label NTC711 \ + --out-directory /home/ntc/xr/7.11.1/giso_out \ + --create-checksum \ + --clean \ + --docker + ``` + + On success gisobuild writes the golden ISO (e.g. + `ncs5k-golden-x-7.11.1-NTC711.iso`) into the output directory. Publish that file + to the server `remote_file_copy` pulls from, then pass its filename to + `install_os`. diff --git a/mkdocs.yml b/mkdocs.yml index de0ef850..9fec2fcd 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -163,6 +163,7 @@ nav: - pyntc.devices.f5_device: "code-reference/pyntc/devices/f5_device.md" - pyntc.devices.ios_device: "code-reference/pyntc/devices/ios_device.md" - pyntc.devices.iosxewlc_device: "code-reference/pyntc/devices/iosxewlc_device.md" + - pyntc.devices.iosxr_device: "code-reference/pyntc/devices/iosxr_device.md" - pyntc.devices.jnpr_device: "code-reference/pyntc/devices/jnpr_device.md" - pyntc.devices.nxos_device: "code-reference/pyntc/devices/nxos_device.md" - pyntc.devices.system_features: "code-reference/pyntc/devices/system_features/__init__.md" diff --git a/pyntc/devices/__init__.py b/pyntc/devices/__init__.py index a9dcead4..3756dd33 100644 --- a/pyntc/devices/__init__.py +++ b/pyntc/devices/__init__.py @@ -6,6 +6,7 @@ from .f5_device import F5Device from .ios_device import IOSDevice from .iosxewlc_device import IOSXEWLCDevice +from .iosxr_device import IOSXRDevice from .jnpr_device import JunosDevice from .nxos_device import NXOSDevice @@ -14,6 +15,7 @@ "arista_eos_eapi": EOSDevice, "f5_tmos_icontrol": F5Device, "cisco_ios_ssh": IOSDevice, + "cisco_iosxr_ssh": IOSXRDevice, "juniper_junos_netconf": JunosDevice, "cisco_nxos_nxapi": NXOSDevice, "cisco_aireos_ssh": AIREOSDevice, diff --git a/pyntc/devices/iosxr_device.py b/pyntc/devices/iosxr_device.py new file mode 100644 index 00000000..5cdc7778 --- /dev/null +++ b/pyntc/devices/iosxr_device.py @@ -0,0 +1,931 @@ +"""Module for using a Cisco IOS-XR (eXR / 64-bit) device over SSH. + +This driver targets 64-bit IOS-XR (eXR) platforms (initial target: NCS5000 / +NCS-5011) and implements the asynchronous OS upgrade workflow: + + install add -> poll for completion -> install activate -> reload -> install commit -> verify + +The driver upgrades from a single **golden ISO** image. A golden ISO bundles the +base XR image together with the matching-version feature RPMs (IS-IS, OSPF, MPLS, +multicast, etc.) into one file, so it can be added and activated on its own — eXR +will not abort the activation demanding separate feature RPMs. Build a golden ISO +with Cisco's gisobuild tool (https://github.com/ios-xr/gisobuild). Installing a +bare base ISO plus separate feature RPMs is not supported by this driver. +""" + +import re +import time + +from netmiko import ConnectHandler +from netmiko.exceptions import AuthenticationException, ReadTimeout, SSHException + +from pyntc import log +from pyntc.devices.base_device import BaseDevice, fix_docs +from pyntc.errors import ( + CommandError, + CommandListError, + FileSystemNotFoundError, + FileTransferError, + OSInstallError, + RebootTimeoutError, +) +from pyntc.utils.models import FileCopyModel + +# A freshly reloaded eXR node — or one hit by the upgrade workflow's rapid, short-lived +# sessions — refuses new SSH connections once it exceeds its `ssh server rate-limit`, +# closing the socket before the version exchange (the client sees "Error reading SSH +# protocol banner" or a connection timeout). These failures are transient: waiting a few +# seconds lets the per-minute rate-limit window drain, so connections are retried with a +# backoff. Override per device via the `ssh_connect_attempts` / `ssh_connect_retry_delay` +# kwargs. +DEFAULT_SSH_CONNECT_ATTEMPTS = 5 +DEFAULT_SSH_CONNECT_RETRY_DELAY = 15 + +# Parse the running version from "show version", e.g. "Version 7.11.2". +RE_XR_VERSION = re.compile(r"Version\s+(\d+\.\d+\.\d+\w*)") + + +@fix_docs +class IOSXRDevice(BaseDevice): + """Cisco IOS-XR (eXR / 64-bit) Device Implementation.""" + + vendor = "cisco" + + # pylint: disable=too-many-arguments, too-many-positional-arguments + def __init__( + self, + host, + username, + password, + secret="", + port=None, + read_timeout_override=None, + ssh_connect_attempts=DEFAULT_SSH_CONNECT_ATTEMPTS, + ssh_connect_retry_delay=DEFAULT_SSH_CONNECT_RETRY_DELAY, + **kwargs, + ): # noqa: D403 # nosec + """PyNTC Device implementation for Cisco IOS-XR (eXR). + + Args: + host (str): The address of the network device. + username (str): The username to authenticate with the device. + password (str): The password to authenticate with the device. + secret (str, optional): The password to escalate privilege on the device. + port (int, optional): The port to use to establish the connection. Defaults to 22. + read_timeout_override (int, optional): If supplied, overrides all timeouts for netmiko send_command calls. + ssh_connect_attempts (int, optional): Number of times to try to connect to the device before giving up. Defaults to 5. + ssh_connect_retry_delay (int, optional): Number of seconds to wait between retries when ssh_connect_attempts is >1. Defaults to 15. + kwargs (dict): Additional arguments to pass to the Netmiko ConnectHandler. + """ + super().__init__(host, username, password, device_type="cisco_iosxr_ssh") + + self.native = None + self.secret = secret + self.port = int(port) if port else 22 + self.read_timeout_override = read_timeout_override + self._connect_attempts = ssh_connect_attempts + self._connect_retry_delay = ssh_connect_retry_delay + self._connected = False + self.open() + log.init(host=host) + + def _send_command(self, command, expect_string=None, **kwargs): + command_args = {"command_string": command} + if expect_string is not None: + command_args["expect_string"] = expect_string + command_args.update(kwargs) + + response = self.native.send_command(**command_args) + + if re.search(r"^\s*%", response, flags=re.MULTILINE) or "Error:" in response: + log.error("Host %s: Error in %s with response: %s", self.host, command, response) + raise CommandError(command, response) + + log.info("Host %s: Command %s was executed successfully.", self.host, command) + return response + + def _get_file_system(self): + """Determine the default file system or directory for device. + + Returns: + (str): The name of the default file system or directory for the device. + + Raises: + FileSystemNotFound: When the module is unable to determine the default file system. + """ + raw_data = self.show("show filesystem location all") + + try: + found_filesystems = set() + fs_list = raw_data.split("File Systems:")[1].strip().split("\n")[1:] + for fs in fs_list: + _size_bytes, _free_bytes, fs_type, _fs_flags, fs_name = fs.split() + if "disk" in fs_type: + found_filesystems.add(fs_name) + log.debug("Host %s: Found filesystem %s.", self.host, fs_name) + + # Prefer harddisk: then disk0: + # TODO: Do we need to support more than these? + for fs in ["harddisk:", "disk0:"]: + if fs in found_filesystems: + return fs + except (AttributeError, IndexError, ValueError): + pass + + log.error("host %s: Unable to determine the device's default filesystem.") + raise FileSystemNotFoundError(hostname=self.hostname, command="show filesystem location all") + + def _uptime_components(self, uptime_full_string): + match_weeks = re.search(r"(\d+) weeks?", uptime_full_string) + match_days = re.search(r"(\d+) days?", uptime_full_string) + match_hours = re.search(r"(\d+) hours?", uptime_full_string) + match_minutes = re.search(r"(\d+) minutes?", uptime_full_string) + + weeks = int(match_weeks.group(1)) if match_weeks else 0 + days = int(match_days.group(1)) if match_days else 0 + hours = int(match_hours.group(1)) if match_hours else 0 + minutes = int(match_minutes.group(1)) if match_minutes else 0 + + return weeks, days, hours, minutes + + def _uptime_to_seconds(self, uptime_full_string): + weeks, days, hours, minutes = self._uptime_components(uptime_full_string) + seconds = weeks * 7 * 24 * 60 * 60 + seconds += days * 24 * 60 * 60 + seconds += hours * 60 * 60 + seconds += minutes * 60 + return seconds + + def _uptime_to_string(self, uptime_full_string): + weeks, days, hours, minutes = self._uptime_components(uptime_full_string) + days = days + weeks * 7 + return f"{days:02d}:{hours:02d}:{minutes:02d}:00" + + def _install_add(self, source, image_name): + """Stage the golden ISO into the install repository. + + ``install add`` returns immediately and continues asynchronously in the + background, printing an operation id that subsequent steps reference. + + Args: + source (str): The on-device source path, e.g. ``harddisk:/``. + image_name (str): The golden ISO filename to add. + + Returns: + (int): The install operation id parsed from the device response. + + Raises: + OSInstallError: When the device response does not contain an operation id. + """ + command = f"install add source {source} {image_name}" + response = self.native.send_command(command, read_timeout=120) + + # Parse the operation id from the response, e.g. "Install operation 17 started". + match = re.search(r"[Ii]nstall operation (\d+)", response) + if match is None: + log.error("Host %s: Unable to parse install operation id from response: %s", self.host, response) + raise OSInstallError(hostname=self.host, desired_boot=image_name) + + operation_id = int(match.group(1)) + log.info("Host %s: install add started operation %s.", self.host, operation_id) + return operation_id + + def _wait_for_install_operation(self, operation_id, timeout=3600, interval=30): + """Poll ``show install log `` until the operation reaches a terminal state. + + Args: + operation_id (int): The install operation id to track. + timeout (int): Maximum seconds to wait for a terminal state. Defaults to 3600. + interval (int): Seconds to wait between polls. Defaults to 30. + + Raises: + OSInstallError: When the operation aborts/fails or the timeout is exceeded. + """ + success = re.compile( + rf"operation\s+{operation_id}\b.*(completed successfully|succeeded)", re.IGNORECASE | re.DOTALL + ) + failure = re.compile(rf"operation\s+{operation_id}\b.*(aborted|failed)", re.IGNORECASE | re.DOTALL) + + start = time.time() + while time.time() - start < timeout: + output = self.native.send_command(f"show install log {operation_id}", read_timeout=120) + if failure.search(output): + log.error("Host %s: install operation %s aborted/failed.", self.host, operation_id) + raise OSInstallError(hostname=self.host, desired_boot=f"operation {operation_id}") + if success.search(output): + log.info("Host %s: install operation %s completed successfully.", self.host, operation_id) + return + time.sleep(interval) + + log.error("Host %s: install operation %s timed out after %s seconds.", self.host, operation_id, timeout) + raise OSInstallError(hostname=self.host, desired_boot=f"operation {operation_id}") + + def _install_activate(self, operation_id, poll_interval=60, timeout=3600): + """Activate a staged install operation and track it to completion. + + The activation is issued with ``noprompt`` (so eXR does not wait on the interactive + reload confirmation) and runs **asynchronously** so the SSH session stays free to poll + the install status. A synchronous activate would hold the session and, when the reload + tore the device down, trap the read on a half-open socket until ``read_timeout``. + + For an ISO upgrade the activation always ends in a reload, so "success" manifests as + ``show install request`` reporting *completed, pending reload* (or the SSH session + dropping as the reload starts) — not a committed ``State : Success`` (the device + reloads before that appears). This method polls ``show install request`` once per + ``poll_interval`` — logging each poll — and returns when it sees the pending-reload + marker or the session drops, and raises if the operation reports an abort/error. + + Args: + operation_id (int): The staged ``install add`` operation id to activate. + poll_interval (int): Seconds between status polls. Defaults to 60. + timeout (int): Maximum seconds to wait for the activation to finish. Defaults to 3600. + + Raises: + OSInstallError: When the activation operation aborts/fails or does not finish in time. + """ + command = f"install activate id {operation_id} noprompt" + log.info("Host %s: issuing activation: %s", self.host, command) + try: + self.native.send_command_timing(command, read_timeout=180) + except Exception as exc: # noqa: BLE001 # pylint: disable=broad-exception-caught + log.info("Host %s: activation issue dropped the session (reload already underway): %s", self.host, exc) + return + + start = time.time() + while time.time() - start < timeout: + time.sleep(poll_interval) + try: + request = self.native.send_command("show install request", read_timeout=120) + except Exception as exc: # noqa: BLE001 # pylint: disable=broad-exception-caught + log.info("Host %s: session dropped while polling activation (reload underway): %s", self.host, exc) + return + log.info("Host %s: polled activation status.", self.host) + if re.search(r"abort|Error[:!]", request, re.IGNORECASE): + log.error("Host %s: activation of operation %s failed: %s", self.host, operation_id, request) + raise OSInstallError(hostname=self.host, desired_boot=f"operation {operation_id}") + if re.search( + r"completed, pending reload|finished successfully|completed successfully", request, re.IGNORECASE + ): + log.info("Host %s: activation completed; reload imminent.", self.host) + return + + log.error("Host %s: activation of operation %s did not finish within %ss.", self.host, operation_id, timeout) + raise OSInstallError(hostname=self.host, desired_boot=f"operation {operation_id}") + + def _install_commit(self, retries=3, retry_delay=30, read_timeout=120): + """Persist the activated software so it survives future reloads. + + Issued immediately after the activation reload, where the install manager can be slow + to respond, so the command uses a generous ``read_timeout`` and is retried a few times + on failure. Re-issuing ``install commit`` when there is nothing left to commit is a + harmless no-op, so retrying is safe even if a prior attempt actually committed but the + prompt was slow to return. + + Args: + retries (int): Number of attempts before giving up. Defaults to 3. + retry_delay (int): Seconds to wait between attempts. Defaults to 30. + read_timeout (int): Per-attempt Netmiko read timeout. Defaults to 120. + + Raises: + OSInstallError: When the commit does not complete cleanly after ``retries`` attempts. + """ + for attempt in range(1, retries + 1): + try: + self.native.send_command("install commit", read_timeout=read_timeout) + log.info("Host %s: install commit issued.", self.host) + return + except Exception as exc: # noqa: BLE001 # pylint: disable=broad-exception-caught + log.warning( + "Host %s: install commit attempt %s/%s did not return cleanly (%s).", + self.host, + attempt, + retries, + exc, + ) + if attempt < retries: + time.sleep(retry_delay) + try: + self.open() + except Exception as open_exc: # noqa: BLE001 # pylint: disable=broad-exception-caught + log.debug("Host %s: reopen before commit retry failed (%s).", self.host, open_exc) + + log.error("Host %s: install commit did not complete after %s attempts.", self.host, retries) + raise OSInstallError(hostname=self.host, desired_boot="install commit") + + def _wait_for_device_reboot(self, timeout=3600, interval=60, previous_uptime=1200): + """Wait for the activation reload by watching for the device uptime to drop. + + After a successful activation the device reloads. This method probes the device once per + ``interval`` — logging each poll — on fresh short-lived connections, which avoids the + half-open-socket hang a long-lived read would hit. On each successful probe it reads the + device uptime: once the reported uptime drops below ``previous_uptime`` the reload is + treated as complete and the method returns. + + Args: + timeout (int): Maximum seconds to wait for the device to return. Defaults to 3600. + interval (int): Seconds between probes. Defaults to 60. + previous_uptime (int, optional): Device will be considered rebooted if the current + uptime (in seconds) is less than this value. Defaults to 1200. + + Raises: + RebootTimeoutError: When the device does not return within ``timeout``. + """ + # Drop any existing session so each probe is a fresh connection. + try: + self.close() + except Exception as close_exc: # noqa: BLE001 # pylint: disable=broad-exception-caught + log.debug("Host %s: pre-reboot disconnect raised %s (ignored).", self.host, close_exc) + self.native = None + self._connected = False + + start = time.time() + while time.time() - start < timeout: + try: + # This loop is itself the retry mechanism, so each probe fails fast + # (retry=False) rather than paying the connect backoff on every poll. + self.open(retry=False) + self._uptime = None + uptime = self.uptime + if uptime is not None and uptime < previous_uptime: + log.info("Host %s: device is back up after reload.", self.host) + return + if uptime is not None: + previous_uptime = uptime + log.info("Host %s: device still reachable; waiting for the reload to drop the session...", self.host) + except Exception as exc: # noqa: BLE001 # pylint: disable=broad-exception-caught + self.native = None + self._connected = False + log.info("Host %s: device is down (%s); polling again in %ss.", self.host, exc, interval) + time.sleep(interval) + + log.error("Host %s: device did not return within %ss while rebooting.", self.host, timeout) + raise RebootTimeoutError(hostname=self.host, wait_time=timeout) + + @property + def boot_options(self): + """Get the current boot image and version from ``show install active``. + + Returns: + (dict): ``{"sys": , "version": }``; + both values are ``None`` when the output cannot be parsed. + """ + show_install_active = self.show("show install active") + + # Parse the active boot package and its version, e.g. "ncs5k-xr-7.11.2". + match = re.search(r"(?P\S*xr-(?P\d+\.\d+\.\d+\w*))", show_install_active) + + # The regex's named groups are exactly "sys" and "version". + boot_options = match.groupdict() if match else {"sys": None, "version": None} + + log.debug("Host %s: boot options %s.", self.host, boot_options) + return boot_options + + def close(self): + """Disconnect from the device.""" + if self.connected: + self.native.disconnect() + self._connected = False + log.debug("Host %s: Connection closed.", self.host) + + @property + def connected(self): # noqa: D401 + """Get the connection status of the device. + + Returns: + (bool): True if the device is connected, else False. + """ + return self._connected + + @connected.setter + def connected(self, value): + self._connected = value + + def enable(self): + """No-op for IOS-XR. + + IOS-XR EXEC mode is already privileged, so there is no enable step + analogous to IOS. Provided for API compatibility. + """ + log.debug("Host %s: enable() is a no-op on IOS-XR.", self.host) + + def get_remote_checksum(self, filename, hashing_algorithm="md5", file_system=None): + """Get the checksum of a remote file. + + Args: + filename (str): The name of the file to check for on the remote device. + hashing_algorithm (str): The hashing algorithm to use. Valid choices are "md5", "sha1", "sha256" and "sha512" (default: "md5"). + file_system (str): The file system for the remote file. + If no file_system is provided, then the ``get_file_system`` + method is used to determine the correct file system to use. + + Returns: + (str): The checksum of the remote file. + + Raises: + ValueError: If an unsupported hashing algorithm is provided. + CommandError: If there is an error in executing the command to get the remote checksum. + """ + if hashing_algorithm not in {"md5", "sha1", "sha256", "sha512"}: + raise ValueError( + "hashing_algorithm must be either 'md5', 'sha1', 'sha256' or 'sha512' for Cisco IOS-XR devices." + ) + if file_system is None: + file_system = self._get_file_system() + if not file_system.startswith("/"): + file_system = "/" + file_system + cmd = f"run {hashing_algorithm}sum {file_system}/{filename}" + result = self._send_command(cmd, read_timeout=300) + + match = re.search(r"^([a-fA-F0-9]+)\s", result, flags=re.MULTILINE) + if match: + log.debug( + "Host %s: Remote checksum for file %s with hashing algorithm %s is %s.", + self.host, + filename, + hashing_algorithm, + match[1], + ) + return match[1] + + log.error( + "Host %s: Unable to get remote checksum for file %s with hashing algorithm %s", + self.host, + filename, + hashing_algorithm, + ) + raise CommandError( + cmd, f"Unable to get remote checksum for file {filename} with hashing algorithm {hashing_algorithm}" + ) + + def check_file_exists(self, filename, file_system=None): + """Check whether a file exists on the device filesystem. + + Args: + filename (str): The filename to look for. + file_system (str, optional): Filesystem to inspect. Automatically retrieves the default filesystem if not provided. + + Returns: + (bool): True if the file is present, False otherwise. + """ + if file_system is None: + file_system = self._get_file_system() + + result = self.native.send_command(f"dir {file_system}/{filename}", read_timeout=30) + if re.search(r"No such file|No files matched|not found|Path does not exist|Error", result, re.IGNORECASE): + log.debug("Host %s: File %s does not exist on %s.", self.host, filename, file_system) + return False + if re.search(re.escape(filename), result): + log.debug("Host %s: File %s exists on %s.", self.host, filename, file_system) + return True + + log.debug("Host %s: File %s not found in 'dir' output on %s.", self.host, filename, file_system) + return False + + def verify_file(self, checksum, filename, hashing_algorithm="md5", file_system=None): + """Verify a file on the remote device exists and its checksum matches. + + Args: + checksum (str): The expected checksum of the file. + filename (str): The name of the file to check for on the remote device. + hashing_algorithm (str): The hashing algorithm to use (default: "md5"). + file_system (str): The file system for the remote file. If no file_system + is provided, then the ``_get_file_system`` method is used to determine + the correct file system to use. + + Returns: + (bool): True if the file is verified successfully, False otherwise. + """ + return self.check_file_exists(filename, file_system=file_system) and self.compare_file_checksum( + checksum, filename, hashing_algorithm, file_system=file_system + ) + + def remote_file_copy(self, src: FileCopyModel, dest=None, file_system=None, **kwargs): + """Copy a file from a remote URL onto the device filesystem. + + Pulls the file specified by ``src`` from a remote server (FTP/TFTP/SCP/HTTP/HTTPS) + using the IOS-XR ``copy`` command and saves it to ``file_system``. The transfer is + verified by confirming the file exists and matches the ``checksum`` after copy. + + Args: + src (FileCopyModel): The source specification (URL, credentials, timeout). + dest (str, optional): Destination filename. Defaults to ``src.file_name``. + file_system (str, optional): Target filesystem. Automatically retrieves the default filesystem if not provided. + kwargs (dict): Additional keyword arguments (unused). + + Raises: + TypeError: When ``src`` is not a ``FileCopyModel``. + FileTransferError: When the transfer fails or the file does not match the checksum afterward. + """ + if not isinstance(src, FileCopyModel): + raise TypeError("src must be an instance of FileCopyModel") + if file_system is None: + file_system = self._get_file_system() + if dest is None: + dest = src.file_name + + if self.verify_file(src.checksum, dest, hashing_algorithm=src.hashing_algorithm, file_system=file_system): + log.info("Host %s: File %s already present on %s; skipping copy.", self.host, dest, file_system) + return + + self._pre_transfer_space_check(src, file_system) + current_prompt = self.native.find_prompt() + + # Prompts IOS-XR may emit during an interactive copy. + prompt_answers = { + r"Destination filename": "", + r"Host name or IP address": "", + r"Source username|[Uu]sername": src.username or "", + r"[Pp]assword": src.token or "", + r"yes/no|\[confirm\]|Are you sure": "", + } + keys = list(prompt_answers.keys()) + [re.escape(current_prompt)] + expect_regex = f"({'|'.join(keys)})" + + command = f"copy {src.clean_url} {file_system}/{dest}" + if src.vrf and src.scheme not in {"http", "https"}: + command = f"{command} vrf {src.vrf}" + + # Bypass _send_command: a copy may emit benign "%" lines that are not failures. + output = self.native.send_command(command, expect_string=expect_regex, read_timeout=src.timeout) + + # Walk any interactive prompts. Netmiko strips the trailing prompt from the output, + # so the post-copy existence check (below) is the authoritative success signal; this + # loop only answers prompts and surfaces explicit error markers early. + for _ in range(10): + if re.search( + r"Successfully copied|Copy operation success|bytes copied|copied in|\[OK\]|Download Complete|transfer successful", + output, + flags=re.IGNORECASE, + ): + log.info("Host %s: File %s transfer reported success.", self.host, dest) + break + if re.search( + r"%Error|Error opening|Invalid input|Failed|Aborted|denied|No such file|Connection refused|timed out|could not", + output, + flags=re.IGNORECASE, + ): + log.error("Host %s: File transfer error for %s: %s", self.host, dest, output) + raise FileTransferError + for prompt, answer in prompt_answers.items(): + if re.search(prompt, output, re.IGNORECASE): + is_password = "password" in output.lower() + output = self.native.send_command( + answer, expect_string=expect_regex, read_timeout=src.timeout, cmd_verify=not is_password + ) + break + else: + # No recognised prompt and no explicit marker; defer to the existence check. + break + + if not self.verify_file(src.checksum, dest, hashing_algorithm=src.hashing_algorithm, file_system=file_system): + log.error( + "Host %s: File %s could not be verified after transfer (missing or checksum mismatch). %s", + self.host, + dest, + FileTransferError.default_message, + ) + raise FileTransferError + + log.info("Host %s: File %s copied to %s and checksum verified.", self.host, dest, file_system) + + @property + def hostname(self): + """Get the hostname of the device. + + Returns: + (str): The device hostname derived from the CLI prompt. + """ + if self._hostname is None: + prompt = self.native.find_prompt() + self._hostname = re.sub(r"^RP/\S+/CPU\d+:", "", prompt).strip().rstrip("#>") + return self._hostname + + def _image_booted(self, image_name, image_pattern=r"(\d+\.\d+\.\d+\w*)", **vendor_specifics): + image_match = re.search(image_pattern, image_name) + if image_match is None: + log.info("Host %s: Unable to parse a version from image %s.", self.host, image_name) + return False + image_version = image_match.group(1) + + booted_version = self.boot_options.get("version") + if booted_version is None: + version_data = self.show("show version") + version_match = RE_XR_VERSION.search(version_data) + booted_version = version_match.group(1) if version_match else None + + booted = booted_version == image_version + if booted: + log.info("Host %s: Image %s booted successfully.", self.host, image_name) + else: + log.info("Host %s: Image %s not booted (running %s).", self.host, image_name, booted_version) + return booted + + @property + def install_mode(self): + """Indicate whether the device is operating in install mode. + + eXR is always install-mode (there is no legacy boot-from-image state), + so this always returns ``True``. Provided for ``BaseDevice`` parity. + + Returns: + (bool): Always ``True``. + """ + return True + + def _get_free_space(self, file_system=None): + """Return free bytes on ``file_system`` as reported by ``dir`` output. + + Args: + file_system (str, optional): Target filesystem. Automatically retrieves the default filesystem if not provided. + + Returns: + (int): Free bytes available on ``file_system``. + + Raises: + CommandError: When the free space cannot be parsed from ``dir`` output. + """ + if file_system is None: + file_system = self._get_file_system() + + raw_data = self.show(f"dir {file_system}") + # eXR reports the trailer in kbytes (e.g. "9948012 kbytes total (9396256 kbytes free)"); + # other contexts may use plain bytes. Capture the unit and normalise to bytes. + match = re.search(r"\((\d+)\s+(k|m|g)?bytes\s+free", raw_data, re.IGNORECASE) + if match is None: + log.error("Host %s: could not parse free space from 'dir %s'.", self.host, file_system) + raise CommandError(command=f"dir {file_system}", message="Unable to parse free space from dir output.") + + multipliers = {"": 1, "k": 1024, "m": 1024**2, "g": 1024**3} + unit = (match.group(2) or "").lower() + free_bytes = int(match.group(1)) * multipliers[unit] + log.debug("Host %s: %s bytes free on %s.", self.host, free_bytes, file_system) + return free_bytes + + def install_os(self, image_name, reboot=True, **vendor_specifics): + """Install a golden IOS-XR ISO and verify the device boots into it. + + Orchestrates the eXR install workflow over the native primitives: + ``install add`` (golden ISO) -> poll for completion -> ``install activate`` -> + poll the activation operation -> wait for reboot -> ``install commit`` -> verify. + + ``image_name`` must be a **golden ISO** that already bundles the base XR image + and the matching-version feature RPMs (IS-IS, OSPF, MPLS, multicast, etc.). A + bare base ISO cannot be activated on its own when feature packages are active — + eXR aborts the activation demanding the matching RPMs — so build a golden ISO + with Cisco's gisobuild tool (https://github.com/ios-xr/gisobuild) and stage that + single file. Installing a base ISO plus separate feature RPMs is not supported. + + Args: + image_name (str): The golden ISO filename already staged on the device. + reboot (bool): Must be ``True``; activation reloads the device automatically. + vendor_specifics (dict, optional): Supports ``timeout`` (default 3600) for the + install-operation and reboot waits. + + Returns: + (bool): True if the install ran and succeeded, False if the device was + already running ``image_name``. + + Raises: + ValueError: When ``reboot`` is False (eXR always reloads during activation). + OSInstallError: When activation aborts or the device does not boot into + ``image_name`` after install. + """ + timeout = vendor_specifics.get("timeout", 3600) + + if self._image_booted(image_name): + log.info("Host %s: OS image %s already booted; nothing to install.", self.host, image_name) + return False + + if not reboot: + raise ValueError( + "IOS-XR devices reload automatically during 'install activate'; " + "the reboot argument cannot be set to False." + ) + + self._uptime = None + uptime = self.uptime + add_id = self._install_add(f"{self._get_file_system()}/", image_name) + self._wait_for_install_operation(add_id, timeout=timeout) + self._install_activate(add_id, timeout=timeout) + self._wait_for_device_reboot(timeout=timeout, previous_uptime=uptime) + self._install_commit() + + if not self._image_booted(image_name): + log.error("Host %s: OS install error for image %s.", self.host, image_name) + raise OSInstallError(hostname=self.host, desired_boot=image_name) + + log.info("Host %s: OS image %s installed successfully.", self.host, image_name) + return True + + def _connect(self, attempts): + """Establish a Netmiko connection, retrying transient SSH failures with backoff. + + eXR refuses new SSH sessions once its ``ssh server rate-limit`` is exceeded — and + the upgrade workflow opens many short-lived sessions in quick succession, so a fresh + connection (e.g. the post-reboot verification) can be rejected: the device closes the + socket before the version exchange, surfacing as ``Error reading SSH protocol banner`` + or a connection timeout. These are transient, so connect is retried with a backoff + long enough to let the per-minute window drain. Authentication failures are not + transient and are re-raised immediately. + + Args: + attempts (int): Maximum number of connection attempts. + + Returns: + ConnectHandler: A live Netmiko connection. + + Raises: + AuthenticationException: On a genuine auth failure (never retried). + SSHException: When every attempt fails to connect (last error re-raised). + """ + last_exc = None + for attempt in range(1, attempts + 1): + try: + return ConnectHandler( + device_type="cisco_xr", + ip=self.host, + username=self.username, + password=self.password, + port=self.port, + read_timeout_override=self.read_timeout_override, + secret=self.secret, + # Keepalives let the status polls notice a dropped session promptly. + keepalive=30, + verbose=False, + ) + except AuthenticationException: + # Bad credentials are not transient — fail fast. + raise + except (SSHException, OSError, EOFError) as exc: + # SSHException covers Netmiko's banner/timeout wrappers and raw paramiko + # banner errors; OSError/EOFError cover the rate-limited socket close. + last_exc = exc + if attempt < attempts: + log.info( + "Host %s: SSH connect attempt %s/%s failed (%s); retrying in %ss.", + self.host, + attempt, + attempts, + exc, + self._connect_retry_delay, + ) + time.sleep(self._connect_retry_delay) + log.error("Host %s: SSH connect failed after %s attempts.", self.host, attempts) + raise last_exc + + def open(self, retry=True): + """Open a connection to the network device. + + Args: + retry (bool): Retry transient SSH failures (rate-limit / banner) with backoff. + Defaults to True. Callers that run their own polling loop (e.g. + ``_wait_for_device_reboot``) pass False so each probe fails fast. + """ + if self.connected: + try: + self.native.find_prompt() + except Exception: # noqa: BLE001 # pylint: disable=broad-exception-caught + self._connected = False + + if not self.connected: + self.native = self._connect(self._connect_attempts if retry else 1) + self._connected = True + + log.debug("Host %s: Connection to controller was opened successfully.", self.host) + + @property + def os_version(self): + """Get the running OS version from ``show version``. + + Returns: + (str): The version string (e.g. ``7.11.2``), or ``None`` if unparsable. + """ + if self._os_version is None: + version_data = self.show("show version") + match = RE_XR_VERSION.search(version_data) + self._os_version = match.group(1) if match else None + + log.debug("Host %s: OS version %s.", self.host, self._os_version) + return self._os_version + + def reboot(self, wait_for_reload=False, **kwargs): + """Reboot the device. + + Args: + wait_for_reload (bool): Whether to also run ``_wait_for_device_reboot``. Defaults to False. + kwargs (dict): Additional arguments to pass to Netmiko. + """ + if kwargs.get("confirm"): + log.warning("Passing 'confirm' to reboot method is deprecated.") + + try: + self.native.send_command_timing("reload") + # IOS-XR prompts "Proceed with reload?" — confirm with a newline. + try: + self.native.send_command_timing("\n", read_timeout=10) + except ReadTimeout as expected_exception: + log.info("Host %s: Device rebooted.", self.host) + log.info("Hit expected exception during reload: %s", expected_exception.__class__) + if wait_for_reload: + time.sleep(10) + self._wait_for_device_reboot() + except Exception as err: # noqa: BLE001 # pylint: disable=broad-exception-caught + log.error(err) + log.error(err.__class__) + + def set_boot_options(self, image_name, **vendor_specifics): + """Not supported on IOS-XR. + + eXR has no separate set-boot step: boot selection is performed atomically + by ``install_os`` via ``install activate`` + ``install commit``. + + Raises: + NotImplementedError: Always. + """ + raise NotImplementedError( + "IOS-XR has no separate set-boot step; boot selection is performed by install_os " + "via 'install activate' + 'install commit'." + ) + + def config(self, command, **netmiko_args): + """Not implemented for IOS-XR. + + Configuration management is out of scope for this OS-upgrade driver in the current + release; only the upgrade workflow is supported. + + Raises: + NotImplementedError: Always. + """ + raise NotImplementedError("config() is not implemented for the IOS-XR driver in this release.") + + def save(self, filename=None): + """Not supported on IOS-XR. + + eXR software state is committed atomically by ``install_os`` (via ``install commit``); + there is no standalone save step. + + Raises: + NotImplementedError: Always. + """ + raise NotImplementedError( + "IOS-XR has no standalone save; software is committed by install_os via 'install commit'." + ) + + def show(self, command, expect_string=None, **netmiko_args): + """Run a command on the device. + + IOS-XR EXEC mode is already privileged, so no enable step is performed. + + Args: + command (str|list): Command(s) to run. + expect_string (str, optional): Expected prompt string. Defaults to None. + netmiko_args (dict): Additional arguments passed to Netmiko's send_command. + + Returns: + (str|list): Command output; a list when ``command`` is a list. + + Raises: + CommandListError: When ``command`` is a list and one of the commands fails. + """ + if isinstance(command, list): + responses = [] + entered_commands = [] + for command_instance in command: + entered_commands.append(command_instance) + try: + responses.append(self._send_command(command_instance)) + except CommandError as e: + raise CommandListError(entered_commands, command_instance, e.cli_error_msg) + return responses + return self._send_command(command, expect_string=expect_string, **netmiko_args) + + @property + def uptime(self): + """Get uptime from the device. + + Returns: + (int): Uptime in seconds. + """ + if self._uptime is None: + version_data = self.show("show version") + match = re.search(r"uptime is (.+)", version_data) + uptime_full_string = match.group(1) if match else "" + self._uptime = self._uptime_to_seconds(uptime_full_string) + + log.debug("Host %s: Uptime %s.", self.host, self._uptime) + return self._uptime + + @property + def uptime_string(self): + """Get uptime in ``dd:hh:mm:ss`` format. + + Returns: + (str): Uptime of the device. + """ + if self._uptime_string is None: + version_data = self.show("show version") + match = re.search(r"uptime is (.+)", version_data) + uptime_full_string = match.group(1) if match else "" + self._uptime_string = self._uptime_to_string(uptime_full_string) + + return self._uptime_string diff --git a/pyproject.toml b/pyproject.toml index 118397d2..af9e003f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -167,7 +167,7 @@ python_paths = "./" testpaths = [ "tests/" ] -addopts = "-vv --doctest-modules -p no:warnings --ignore-glob='*mock*'" +addopts = "-vv --doctest-modules -p no:warnings -p no:f5sdk_fixtures --ignore-glob='*mock*'" [tool.towncrier] package = "pyntc" diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index f3f974df..fe9fbede 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -3,7 +3,7 @@ import pytest -from pyntc.devices import AIREOSDevice, ASADevice, EOSDevice, IOSDevice, IOSXEWLCDevice, supported_devices +from pyntc.devices import AIREOSDevice, ASADevice, EOSDevice, IOSDevice, IOSXEWLCDevice def get_side_effects(mock_path, side_effects): @@ -59,15 +59,6 @@ def _mock(side_effects, existing_device=None, device=eos_device): return _mock -def pytest_generate_tests(metafunc): - if metafunc.function.__name__ == "test_device_creation": - metafunc.parametrize( - "device_type,expected", - ((device_type, device_class) for device_type, device_class in supported_devices.items()), - ids=(device_type for device_type in supported_devices), - ) - - @pytest.fixture def aireos_boot_image(): return "8.2.170.0" diff --git a/tests/unit/test_devices/test_f5_device.py b/tests/unit/test_devices/test_f5_device.py index 6822d805..f533b2ea 100644 --- a/tests/unit/test_devices/test_f5_device.py +++ b/tests/unit/test_devices/test_f5_device.py @@ -1,3 +1,4 @@ +import sys from unittest import mock import pytest @@ -6,6 +7,11 @@ from pyntc.devices.f5_device import F5Device, FileTransferError from pyntc.errors import NTCFileNotFoundError +pytestmark = pytest.mark.skipif( + sys.version_info >= (3, 12), + reason="The F5 SDK is only compatible with Python 3.11 or lower", +) + BOOT_IMAGE = "BIGIP-11.3.0.2806.0.iso" VOLUME = "HD1.1" diff --git a/tests/unit/test_devices/test_iosxr_device.py b/tests/unit/test_devices/test_iosxr_device.py new file mode 100644 index 00000000..8eeb41c9 --- /dev/null +++ b/tests/unit/test_devices/test_iosxr_device.py @@ -0,0 +1,612 @@ +import unittest + +import mock +from netmiko.exceptions import AuthenticationException, SSHException + +from pyntc.devices import IOSXRDevice, supported_devices +from pyntc.devices import iosxr_device as iosxr_module +from pyntc.errors import FileTransferError +from pyntc.utils.models import FileCopyModel + +ISO = "ncs5k-golden-x-7.11.2-NTC7112.iso" +ACTIVE_VERSION = "7.11.2" +ISO_URL = "http://10.1.100.220/IOS-XR/7.11.2/{ISO}" +PROMPT = "RP/0/RP0/CPU0:ncs#" + +DIR_FILE_PRESENT = f"Mon Jun 15 12:00:00.000 UTC\n\nDirectory of harddisk:\n15 -rw- 1500000000 Jun 15 12:00 {ISO}\n" +DIR_FILE_ABSENT = f"Mon Jun 15 12:00:00.000 UTC\n%Error: dir: '/harddisk:/{ISO}': No such file\n" +COPY_FTP_PROMPTS = [ + "Source username: [anonymous]?", + "Source password:", + "Destination filename [%s]?", + "Accessing %s", +] + +COPY_SUCCESS = ( + "Mon Jun 15 12:00:00.000 UTC\n" + f"Destination filename [/harddisk:/{ISO}]?\n" + f"Accessing http://10.1.100.220/IOS-XR/7.11.2/{ISO}\n" + "1500000000 bytes copied in 42 secs (35714285 bytes/sec)\n" + "RP/0/RP0/CPU0:ncs#" +) +COPY_ERROR = ( + f"Mon Jun 15 12:00:00.000 UTC\n%Error opening http://10.1.100.220/IOS-XR/7.11.2/{ISO}: Connection refused\n" +) +# Real eXR (NCS5011) copy output: netmiko strips the trailing prompt, and the +# success markers are "Successfully copied ... Bytes" / "Copy operation success". +COPY_SUCCESS_EXR = ( + f"\nAccessing http://10.1.100.220/IOS-XR/7.11.2/{ISO}\n" + + ("!" * 60) + + "\nSuccessfully copied 1432756224 Bytes\n\n\nCopy operation success\n" +) + +SHOW_INSTALL_ACTIVE_SINGLE = ( + "Node 0/RP0/CPU0 [RP]\n" + " Boot Partition: xr_lv0\n" + " Active Packages: 1\n" + " ncs5k-xr-7.11.2 version=7.11.2 [Boot image]\n" +) + +SHOW_INSTALL_ACTIVE_MULTI = ( + "Node 0/RP0/CPU0 [RP]\n" + " Active Packages: 8\n" + " ncs5k-xr-7.11.2 version=7.11.2 [Boot image]\n" + " ncs5k-isis-7.11.2\n" + " ncs5k-ospf-7.11.2\n" + " ncs5k-mpls-7.11.2\n" + " ncs5k-mpls-te-rsvp-7.11.2\n" + " ncs5k-mcast-7.11.2\n" + " ncs5k-m2m-7.11.2\n" + " ncs5k-mgbl-7.11.2\n" +) + +INSTALL_ADD_RESPONSE = ( + "Mon Jun 15 12:00:00.000 UTC\n" + "Install operation 17 started by admin:\n" + f" install add source harddisk:/ {ISO}\n" + "This operation will continue asynchronously.\n" + "Install add operation 17 will continue in the background.\n" +) + +INSTALL_ADD_NO_OP_ID = "Mon Jun 15 12:00:00.000 UTC\n% Unexpected output without an operation id\n" + +SHOW_FILESYSTEM_LOCATION_ALL = """ +Tue Jun 23 21:22:51.023 UTC + + node: node0_RP0_CPU0 +------------------------------------------------------------------ +File Systems: + + Size(b) Free(b) Type Flags Prefixes + 2358312960 2347773952 flash-disk rw disk0: + 480907264 479154176 flash rw /misc/config + 10186764288 7176835072 harddisk rw harddisk: + 0 0 network rw ftp: + 0 0 network rw tftp: + 3962216448 3926454272 flash-disk rw apphost: +""" + +SHOW_INSTALL_LOG_INPROGRESS = ( + "Install operation 17: 'install add source harddisk:/ ...' started\nAction 17 in progress\n" +) + +SHOW_INSTALL_LOG_SUCCESS = ( + "Install operation 17: 'install add source harddisk:/ ...' started\nInstall operation 17 completed successfully\n" +) + +SHOW_INSTALL_LOG_ABORT = ( + "Install operation 17: 'install add source harddisk:/ ...' started\nInstall operation 17 aborted\n" +) + +SHOW_VERSION = ( + "Cisco IOS XR Software, Version 7.11.2\n" + "Copyright (c) 2013-2024 by Cisco Systems, Inc.\n\n" + "cisco NCS-5011 () processor\n" + "System uptime is 1 week, 2 days, 3 hours, 4 minutes\n" +) + +DIR_HARDDISK = ( + "Mon Jun 15 12:00:00.000 UTC\n\n" + "Directory of harddisk:\n" + f"15 -rw- 1500000000 Jun 15 12:00 {ISO}\n\n" + "3000000000 bytes total (2000000000 bytes free)\n" +) + +DIR_HARDDISK_LOW = "Mon Jun 15 12:00:00.000 UTC\n\nDirectory of harddisk:\n3000000000 bytes total (1000 bytes free)\n" + +# Real eXR (NCS5011) trailer reports kbytes, not bytes. +DIR_HARDDISK_KBYTES = ( + "Mon Jun 15 23:43:01.321 UTC\n\n" + "Directory of harddisk:\n" + " 13 drwxr-xr-x. 2 4096 Jun 15 20:22 .tmp\n" + " 12 -rw-r--r--. 1 382788 Jun 15 23:35 nvgen_bkup.log\n\n" + "9948012 kbytes total (9396256 kbytes free)\n" +) + +# 'show install request' states observed on real eXR during an activation. +SHOW_INSTALL_REQUEST_IN_PROGRESS = ( + "Tue Jun 16 03:31:04.338 UTC\n" + "User ntc, Op Id 26\ninstall activate\nncs5k-golden-x-7.11.2-NTC7112\n" + "install operation 26 is in progress\n" + "Install prepare operation 26 is in progress\n" + "0/RP0 In Progress Partition preparation in progress\n" +) +SHOW_INSTALL_REQUEST_PENDING_RELOAD = "Tue Jun 16 03:35:12.838 UTC\nInstall operation completed, pending reload\n" +SHOW_INSTALL_REQUEST_ACTIVATE_FAILURE = ( + "Tue Jun 16 00:29:55.000 UTC\n" + "Error: An exception is hit while executing the install operation.\n" + "Install operation 26 aborted\n" +) + +# RP/0/RP0/CPU0:NCS5011-LAB#run sha512sum /harddisk:/ncs5k-golden-x-7.11.1-NTC711.iso +SHA512SUM = "30221afa665814ab68cb4dd2f114a9da2b2285b8d7b2b2854b63db7bd0a5ea7980e3d4295b8cb6bfaa3d7c4f4d8d8f635562541d727e1dd66566c4895780c47b" +RUN_SHA512SUM = f""" +Wed Jun 24 22:17:02.779 UTC +{SHA512SUM} /harddisk:/ncs5k-golden-x-7.11.1-NTC711.iso +""" + +# RP/0/RP0/CPU0:NCS5011-LAB#run sha256sum /harddisk:/ncs5k-golden-x-7.11.1-NTC711.iso +SHA256SUM = "71e68c6b7ff7eac595f09d34d184adf31181b16bf7d986a474277119493df8bb" +RUN_SHA256SUM = f""" +Wed Jun 24 22:17:28.194 UTC +{SHA256SUM} /harddisk:/ncs5k-golden-x-7.11.1-NTC711.iso +""" + +# RP/0/RP0/CPU0:NCS5011-LAB#run sha1sum /harddisk:/ncs5k-golden-x-7.11.1-NTC711.iso +SHA1SUM = "6c62c6322c796036b18fbbb819f25e6558467c51" +RUN_SHA1SUM = f""" +Wed Jun 24 22:17:47.916 UTC +{SHA1SUM} /harddisk:/ncs5k-golden-x-7.11.1-NTC711.iso +""" + +# RP/0/RP0/CPU0:NCS5011-LAB#run md5sum /harddisk:/ncs5k-golden-x-7.11.1-NTC711.iso +MD5SUM = "c12d35ae63203202304f0c4d5a49f0e6" +RUN_MD5SUM = f""" +Wed Jun 24 22:18:00.782 UTC +{MD5SUM} /harddisk:/ncs5k-golden-x-7.11.1-NTC711.iso +""" + + +def _fake_clock(values): + """Return a time.time() stand-in that yields ``values`` then a large constant. + + The standard ``logging`` module also calls ``time.time()``, so a finite + ``side_effect`` list raises StopIteration unpredictably. This helper returns + a huge value once exhausted, keeping the polling-loop timeout logic + deterministic regardless of interleaved log calls. + """ + seq = list(values) + + def _inner(*args, **kwargs): + return seq.pop(0) if seq else 1e12 + + return _inner + + +class TestIOSXRDevice(unittest.TestCase): + @mock.patch.object(IOSXRDevice, "open") + @mock.patch.object(IOSXRDevice, "close") + def setUp(self, mock_close, mock_open): # pylint: disable=arguments-differ + self.device = IOSXRDevice("host", "user", "pass") + self.device.native = mock.MagicMock() + + def tearDown(self): + if self.device.native is not None: + self.device.native.reset_mock() + + # --- basics / registration --- + + def test_port(self): + self.assertEqual(self.device.port, 22) + + def test_device_type(self): + self.assertEqual(self.device.device_type, "cisco_iosxr_ssh") + + def test_registration(self): + self.assertIs(supported_devices["cisco_iosxr_ssh"], IOSXRDevice) + + # --- facts --- + + def test_os_version(self): + self.device.native.send_command.return_value = SHOW_VERSION + self.assertEqual(self.device.os_version, ACTIVE_VERSION) + + def test_uptime_parses_weeks(self): + self.device.native.send_command.return_value = SHOW_VERSION + # 1 week + 2 days + 3 hours + 4 minutes + expected = (7 * 86400) + (2 * 86400) + (3 * 3600) + (4 * 60) + self.assertEqual(self.device.uptime, expected) + + def test_uptime_string_folds_weeks_into_days(self): + self.device.native.send_command.return_value = SHOW_VERSION + # 1 week + 2 days -> 9 days, 3 hours, 4 minutes -> dd:hh:mm:ss + self.assertEqual(self.device.uptime_string, "09:03:04:00") + + def test_hostname_strips_rp_prefix(self): + self.device.native.find_prompt.return_value = "RP/0/RP0/CPU0:NCS5011-LAB#" + self.assertEqual(self.device.hostname, "NCS5011-LAB") + + # --- show / config / save --- + + def test_show_list_returns_list(self): + self.device.native.send_command.side_effect = ["out-a", "out-b"] + self.assertEqual(self.device.show(["show foo", "show bar"]), ["out-a", "out-b"]) + + def test_show_raises_command_error_on_error_response(self): + self.device.native.send_command.return_value = "% Invalid input detected" + with self.assertRaises(iosxr_module.CommandError): + self.device.show("show bogus") + + def test_show_list_raises_command_list_error(self): + self.device.native.send_command.side_effect = ["% Invalid input detected", "ok"] + with self.assertRaises(iosxr_module.CommandListError): + self.device.show(["show bad", "show good"]) + + def test_config_not_implemented(self): + with self.assertRaises(NotImplementedError): + self.device.config("hostname FOO") + + def test_save_not_implemented(self): + with self.assertRaises(NotImplementedError): + self.device.save() + + # --- boot_options / install_mode / set_boot_options --- + + def test_boot_options(self): + self.device.native.send_command.return_value = SHOW_INSTALL_ACTIVE_SINGLE + self.assertEqual(self.device.boot_options, {"sys": "ncs5k-xr-7.11.2", "version": "7.11.2"}) + + def test_boot_options_multi_package(self): + self.device.native.send_command.return_value = SHOW_INSTALL_ACTIVE_MULTI + self.assertEqual(self.device.boot_options, {"sys": "ncs5k-xr-7.11.2", "version": "7.11.2"}) + + def test_boot_options_none_when_unmatched(self): + self.device.native.send_command.return_value = "No active packages found" + self.assertEqual(self.device.boot_options, {"sys": None, "version": None}) + + def test_install_mode_always_true(self): + self.assertTrue(self.device.install_mode) + + def test_set_boot_options_not_implemented(self): + with self.assertRaises(NotImplementedError): + self.device.set_boot_options(ISO) + + # --- _image_booted --- + + def test_image_booted_true(self): + self.device.native.send_command.return_value = SHOW_INSTALL_ACTIVE_SINGLE + self.assertTrue(self.device._image_booted(ISO)) + + def test_image_booted_false(self): + self.device.native.send_command.return_value = SHOW_INSTALL_ACTIVE_SINGLE + self.assertFalse(self.device._image_booted("ncs5k-mini-x-7.10.1.iso")) + + # --- _get_free_space --- + + @mock.patch.object(IOSXRDevice, "_get_file_system", return_value="harddisk:") + def test_get_free_space(self, *_mocks): + self.device.native.send_command.return_value = DIR_HARDDISK + self.assertEqual(self.device._get_free_space(), 2000000000) + + @mock.patch.object(IOSXRDevice, "_get_file_system", return_value="harddisk:") + def test_get_free_space_kbytes_units(self, *_mocks): + self.device.native.send_command.return_value = DIR_HARDDISK_KBYTES + self.assertEqual(self.device._get_free_space(), 9396256 * 1024) + + @mock.patch.object(IOSXRDevice, "_get_file_system", return_value="harddisk:") + def test_get_free_space_unparsable_raises(self, *_mocks): + self.device.native.send_command.return_value = "garbage output" + with self.assertRaises(iosxr_module.CommandError): + self.device._get_free_space() + + # --- async install primitives --- + + def test_install_add_parses_op_id(self): + self.device.native.send_command.return_value = INSTALL_ADD_RESPONSE + self.assertEqual(self.device._install_add("harddisk:/", ISO), 17) + + def test_install_add_no_op_id_raises(self): + self.device.native.send_command.return_value = INSTALL_ADD_NO_OP_ID + with self.assertRaises(iosxr_module.OSInstallError): + self.device._install_add("harddisk:/", ISO) + + @mock.patch("pyntc.devices.iosxr_device.time.sleep") + def test_wait_for_install_operation_success(self, mock_sleep): + self.device.native.send_command.side_effect = [ + SHOW_INSTALL_LOG_INPROGRESS, + SHOW_INSTALL_LOG_INPROGRESS, + SHOW_INSTALL_LOG_SUCCESS, + ] + self.device._wait_for_install_operation(17) + self.assertEqual(self.device.native.send_command.call_count, 3) + + @mock.patch("pyntc.devices.iosxr_device.time.sleep") + def test_wait_for_install_operation_abort_raises(self, mock_sleep): + self.device.native.send_command.return_value = SHOW_INSTALL_LOG_ABORT + with self.assertRaises(iosxr_module.OSInstallError): + self.device._wait_for_install_operation(17) + + @mock.patch("pyntc.devices.iosxr_device.time.sleep") + @mock.patch("pyntc.devices.iosxr_device.time.time", side_effect=_fake_clock([0, 0])) + def test_wait_for_install_operation_timeout_raises(self, mock_time, mock_sleep): + self.device.native.send_command.return_value = SHOW_INSTALL_LOG_INPROGRESS + with self.assertRaises(iosxr_module.OSInstallError): + self.device._wait_for_install_operation(17, timeout=3600) + + @mock.patch("pyntc.devices.iosxr_device.time.sleep") + def test_install_activate_issues_async_and_returns_on_pending_reload(self, mock_sleep): + self.device.native.send_command_timing.return_value = "Install operation 26 started by ntc" + self.device.native.send_command.return_value = SHOW_INSTALL_REQUEST_PENDING_RELOAD + self.device._install_activate(25) + self.device.native.send_command_timing.assert_any_call("install activate id 25 noprompt", read_timeout=180) + + @mock.patch("pyntc.devices.iosxr_device.time.sleep") + def test_install_activate_raises_on_failure(self, mock_sleep): + self.device.native.send_command_timing.return_value = "Install operation 26 started by ntc" + self.device.native.send_command.return_value = SHOW_INSTALL_REQUEST_ACTIVATE_FAILURE + with self.assertRaises(iosxr_module.OSInstallError): + self.device._install_activate(25) + + @mock.patch("pyntc.devices.iosxr_device.time.sleep") + def test_install_activate_tolerates_session_drop(self, mock_sleep): + # The reload drops the session while polling: that is the success signal, not an error. + self.device.native.send_command_timing.return_value = "Install operation 26 started by ntc" + self.device.native.send_command.side_effect = OSError("socket closed") + self.device._install_activate(25) # must not raise + + @mock.patch("pyntc.devices.iosxr_device.time.sleep") + @mock.patch("pyntc.devices.iosxr_device.time.time", side_effect=_fake_clock([0, 0])) + def test_install_activate_timeout_raises(self, mock_time, mock_sleep): + # Activation never reaches pending-reload (status stays in progress) -> timeout -> raise. + self.device.native.send_command_timing.return_value = "Install operation 26 started by ntc" + self.device.native.send_command.return_value = SHOW_INSTALL_REQUEST_IN_PROGRESS + with self.assertRaises(iosxr_module.OSInstallError): + self.device._install_activate(25, timeout=3600) + + def test_install_commit(self): + self.device.native.send_command.return_value = "Install operation 18 completed successfully" + self.device._install_commit() + self.device.native.send_command.assert_any_call("install commit", read_timeout=120) + + @mock.patch("pyntc.devices.iosxr_device.time.sleep") + @mock.patch.object(IOSXRDevice, "open") + def test_install_commit_retries_then_succeeds(self, mock_open, mock_sleep): + # The install manager can be slow right after the reload; the first attempt fails. + self.device.native.send_command.side_effect = [Exception("read timeout"), "ok"] + self.device._install_commit() + self.assertEqual(self.device.native.send_command.call_count, 2) + + @mock.patch("pyntc.devices.iosxr_device.time.sleep") + @mock.patch.object(IOSXRDevice, "open") + def test_install_commit_raises_after_exhausting_retries(self, mock_open, mock_sleep): + self.device.native.send_command.side_effect = Exception("read timeout") + with self.assertRaises(iosxr_module.OSInstallError): + self.device._install_commit(retries=2) + + # --- reboot / _wait_for_device_reboot --- + + def test_reboot(self): + self.device.reboot() + self.device.native.send_command_timing.assert_any_call("reload") + + @mock.patch("pyntc.devices.iosxr_device.time.sleep") + @mock.patch.object(IOSXRDevice, "show") + @mock.patch.object(IOSXRDevice, "uptime", new_callable=mock.PropertyMock, side_effect=[3600, 30]) + @mock.patch.object(IOSXRDevice, "open", side_effect=[None, OSError("down"), None]) + @mock.patch.object(IOSXRDevice, "close") + def test_wait_for_device_reboot(self, mock_close, mock_open, mock_uptime, mock_show, mock_sleep): + # Reachable -> disconnect (reload) -> back up: requires the drop-then-recover transition. + self.device._wait_for_device_reboot(timeout=10, previous_uptime=3500) + self.assertEqual(mock_open.call_count, 3) + + @mock.patch("pyntc.devices.iosxr_device.time.sleep") + @mock.patch.object(IOSXRDevice, "show") + @mock.patch.object(IOSXRDevice, "uptime", new_callable=mock.PropertyMock, return_value=30) + @mock.patch.object(IOSXRDevice, "open", side_effect=[OSError("down"), OSError("down"), None]) + @mock.patch.object(IOSXRDevice, "close") + def test_wait_for_device_reboot_no_previous_uptime(self, mock_close, mock_open, mock_uptime, mock_show, mock_sleep): + # Reachable -> disconnect (reload) -> back up: requires the drop-then-recover transition. + self.device._wait_for_device_reboot(timeout=10, previous_uptime=3500) + self.assertEqual(mock_open.call_count, 3) + + @mock.patch("pyntc.devices.iosxr_device.time.sleep") + @mock.patch("pyntc.devices.iosxr_device.time.time", side_effect=_fake_clock([0, 0])) + @mock.patch.object(IOSXRDevice, "uptime", new_callable=mock.PropertyMock, return_value=3601) + @mock.patch.object(IOSXRDevice, "open", side_effect=OSError("down")) + @mock.patch.object(IOSXRDevice, "close") + def test_wait_for_device_reboot_timeout(self, mock_close, mock_open, mock_uptime, mock_time, mock_sleep): + with self.assertRaises(iosxr_module.RebootTimeoutError): + self.device._wait_for_device_reboot(timeout=10, previous_uptime=3600) + + # --- connection retry (eXR SSH rate-limit / banner race) --- + + @mock.patch("pyntc.devices.iosxr_device.time.sleep") + @mock.patch("pyntc.devices.iosxr_device.ConnectHandler") + def test_connect_retries_transient_then_succeeds(self, mock_connect, mock_sleep): + conn = mock.MagicMock() + mock_connect.side_effect = [SSHException("Error reading SSH protocol banner"), conn] + self.assertIs(self.device._connect(self.device._connect_attempts), conn) + self.assertEqual(mock_connect.call_count, 2) + mock_sleep.assert_called_once_with(self.device._connect_retry_delay) + + @mock.patch("pyntc.devices.iosxr_device.time.sleep") + @mock.patch("pyntc.devices.iosxr_device.ConnectHandler") + def test_connect_does_not_retry_auth_failure(self, mock_connect, mock_sleep): + mock_connect.side_effect = AuthenticationException("bad creds") + with self.assertRaises(AuthenticationException): + self.device._connect(self.device._connect_attempts) + self.assertEqual(mock_connect.call_count, 1) + mock_sleep.assert_not_called() + + @mock.patch("pyntc.devices.iosxr_device.time.sleep") + @mock.patch("pyntc.devices.iosxr_device.ConnectHandler") + def test_connect_raises_after_exhausting_attempts(self, mock_connect, mock_sleep): + mock_connect.side_effect = SSHException("Error reading SSH protocol banner") + with self.assertRaises(SSHException): + self.device._connect(3) + self.assertEqual(mock_connect.call_count, 3) + self.assertEqual(mock_sleep.call_count, 2) + + @mock.patch("pyntc.devices.iosxr_device.time.sleep") + @mock.patch("pyntc.devices.iosxr_device.ConnectHandler") + def test_open_retry_false_makes_single_attempt(self, mock_connect, mock_sleep): + # The reboot-wait loop is its own retry; each probe must fail fast. + mock_connect.side_effect = OSError("down") + self.device._connected = False + with self.assertRaises(OSError): + self.device.open(retry=False) + self.assertEqual(mock_connect.call_count, 1) + mock_sleep.assert_not_called() + + # --- install_os orchestration --- + + @mock.patch.object(IOSXRDevice, "uptime", new_callable=mock.PropertyMock, return_value=1000) + @mock.patch.object(IOSXRDevice, "_image_booted", side_effect=[False, True]) + @mock.patch.object(IOSXRDevice, "_get_file_system", return_value="harddisk:") + @mock.patch.object(IOSXRDevice, "_install_commit") + @mock.patch.object(IOSXRDevice, "_wait_for_device_reboot") + @mock.patch.object(IOSXRDevice, "_install_activate", return_value=18) + @mock.patch.object(IOSXRDevice, "_wait_for_install_operation") + @mock.patch.object(IOSXRDevice, "_install_add", return_value=17) + def test_install_os(self, mock_add, mock_wait_op, mock_activate, mock_wait_reboot, mock_commit, *_mocks): + result = self.device.install_os(ISO) + self.assertTrue(result) + mock_add.assert_called_once_with("harddisk:/", ISO) + mock_wait_op.assert_called_once_with(17, timeout=3600) # add op only + mock_activate.assert_called_once_with(17, timeout=3600) + mock_wait_reboot.assert_called_once_with(timeout=3600, previous_uptime=1000) + mock_commit.assert_called_once() + + @mock.patch.object(IOSXRDevice, "_install_add") + @mock.patch.object(IOSXRDevice, "_image_booted", return_value=True) + def test_install_os_already_installed(self, mock_booted, mock_add): + result = self.device.install_os(ISO) + self.assertFalse(result) + mock_add.assert_not_called() + + @mock.patch.object(IOSXRDevice, "_image_booted", return_value=False) + def test_install_os_reboot_false_raises(self, mock_booted): + with self.assertRaises(ValueError): + self.device.install_os(ISO, reboot=False) + + @mock.patch.object(IOSXRDevice, "_install_commit") + @mock.patch.object(IOSXRDevice, "_wait_for_device_reboot") + @mock.patch.object(IOSXRDevice, "_install_activate", return_value=18) + @mock.patch.object(IOSXRDevice, "_wait_for_install_operation") + @mock.patch.object(IOSXRDevice, "_install_add", return_value=17) + @mock.patch.object(IOSXRDevice, "uptime", new_callable=mock.PropertyMock, return_value=1000) + @mock.patch.object(IOSXRDevice, "_image_booted", side_effect=[False, False]) + @mock.patch.object(IOSXRDevice, "_get_file_system", return_value="harddisk:") + def test_install_os_verify_failure_raises(self, *_mocks): + with self.assertRaises(iosxr_module.OSInstallError): + self.device.install_os(ISO) + + # --- check_file_exists --- + + @mock.patch.object(IOSXRDevice, "_get_file_system", return_value="harddisk:") + def test_check_file_exists_true(self, *_mocks): + self.device.native.send_command.return_value = DIR_FILE_PRESENT + self.assertTrue(self.device.check_file_exists(ISO)) + + @mock.patch.object(IOSXRDevice, "_get_file_system", return_value="harddisk:") + def test_check_file_exists_false(self, *_mocks): + self.device.native.send_command.return_value = DIR_FILE_ABSENT + self.assertFalse(self.device.check_file_exists(ISO)) + + # --- remote_file_copy --- + + def test_remote_file_copy_requires_model(self): + with self.assertRaises(TypeError): + self.device.remote_file_copy(ISO_URL) + + @mock.patch.object(IOSXRDevice, "verify_file", side_effect=[False, True]) + @mock.patch.object(IOSXRDevice, "_get_file_system", return_value="harddisk:") + def test_remote_file_copy_success(self, *_mocks): + self.device.native.find_prompt.return_value = PROMPT + self.device.native.send_command.return_value = COPY_SUCCESS + src = FileCopyModel(download_url=ISO_URL, checksum="", file_name=ISO) + + self.device.remote_file_copy(src) + + copy_calls = [ + call + for call in self.device.native.send_command.call_args_list + if call.args and call.args[0].startswith("copy ") + ] + self.assertTrue(copy_calls) + self.assertEqual(copy_calls[0].args[0], f"copy {ISO_URL} harddisk:/{ISO}") + + @mock.patch.object(IOSXRDevice, "_get_file_system", return_value="harddisk:") + @mock.patch.object(IOSXRDevice, "verify_file", return_value=True) + def test_remote_file_copy_idempotent_when_present(self, *_mocks): + self.device.native.find_prompt.return_value = PROMPT + src = FileCopyModel(download_url=ISO_URL, checksum="", file_name=ISO) + + self.device.remote_file_copy(src) + + copy_calls = [ + call + for call in self.device.native.send_command.call_args_list + if call.args and call.args[0].startswith("copy ") + ] + self.assertEqual(copy_calls, []) + + @mock.patch.object(IOSXRDevice, "_get_file_system", return_value="harddisk:") + @mock.patch.object(IOSXRDevice, "verify_file", side_effect=[False, True]) + def test_remote_file_copy_success_exr_output(self, mock_verify_file, *_mocks): + # Real eXR success output (no trailing prompt, "Successfully copied"/"Copy operation success"). + self.device.native.find_prompt.return_value = PROMPT + self.device.native.send_command.return_value = COPY_SUCCESS_EXR + src = FileCopyModel(download_url=ISO_URL, checksum="", file_name=ISO) + + self.device.remote_file_copy(src) # must not raise + + self.assertEqual(mock_verify_file.call_count, 2) # idempotency check + post-copy verify + + @mock.patch.object(IOSXRDevice, "check_file_exists", side_effect=[False]) + @mock.patch.object(IOSXRDevice, "_get_file_system", return_value="harddisk:") + def test_remote_file_copy_error_raises(self, *_mocks): + self.device.native.find_prompt.return_value = PROMPT + self.device.native.send_command.return_value = COPY_ERROR + src = FileCopyModel(download_url=ISO_URL, checksum="", file_name=ISO) + + with self.assertRaises(FileTransferError): + self.device.remote_file_copy(src) + + @mock.patch.object(IOSXRDevice, "_get_file_system", return_value="harddisk:") + def test_get_remote_checksum_md5(self, *_mocks): + self.device.native.send_command.return_value = RUN_MD5SUM + self.assertEqual(self.device.get_remote_checksum(ISO, hashing_algorithm="md5"), MD5SUM) + + @mock.patch.object(IOSXRDevice, "_get_file_system", return_value="harddisk:") + def test_get_remote_checksum_sha1(self, *_mocks): + self.device.native.send_command.return_value = RUN_SHA1SUM + self.assertEqual(self.device.get_remote_checksum(ISO, hashing_algorithm="sha1"), SHA1SUM) + + @mock.patch.object(IOSXRDevice, "_get_file_system", return_value="harddisk:") + def test_get_remote_checksum_sha256(self, *_mocks): + self.device.native.send_command.return_value = RUN_SHA256SUM + self.assertEqual(self.device.get_remote_checksum(ISO, hashing_algorithm="sha256"), SHA256SUM) + + @mock.patch.object(IOSXRDevice, "_get_file_system", return_value="harddisk:") + def test_get_remote_checksum_sha512(self, *_mocks): + self.device.native.send_command.return_value = RUN_SHA512SUM + self.assertEqual(self.device.get_remote_checksum(ISO, hashing_algorithm="sha512"), SHA512SUM) + + @mock.patch.object(IOSXRDevice, "_get_file_system", return_value="harddisk:") + @mock.patch.object(IOSXRDevice, "get_remote_checksum", return_value=SHA512SUM) + def test_verify_file_checksum_matches(self, *_mocks): + self.device.native.send_command.return_value = DIR_FILE_PRESENT + self.assertTrue(self.device.verify_file(filename=ISO, checksum=SHA512SUM, hashing_algorithm="sha512")) + + @mock.patch.object(IOSXRDevice, "_get_file_system", return_value="harddisk:") + @mock.patch.object(IOSXRDevice, "get_remote_checksum", return_value=MD5SUM) + def test_verify_file_checksum_failure(self, mock_get_remote_checksum, *_mocks): + self.device.native.send_command.return_value = DIR_FILE_PRESENT + self.assertFalse(self.device.verify_file(filename=ISO, checksum=SHA512SUM, hashing_algorithm="md5")) + mock_get_remote_checksum.assert_called() + + @mock.patch.object(IOSXRDevice, "_get_file_system", return_value="harddisk:") + def test_verify_file_file_not_found(self, *_mocks): + self.device.native.send_command.return_value = DIR_FILE_ABSENT + self.assertFalse(self.device.verify_file(filename=ISO, checksum=MD5SUM)) diff --git a/tests/unit/test_infra.py b/tests/unit/test_infra.py index 881bd12a..0be3ff1a 100644 --- a/tests/unit/test_infra.py +++ b/tests/unit/test_infra.py @@ -1,10 +1,11 @@ import os +import sys import mock import pytest from pyntc import ntc_device, ntc_device_by_name -from pyntc.devices import EOSDevice, IOSDevice, NXOSDevice +from pyntc.devices import EOSDevice, IOSDevice, NXOSDevice, supported_devices from pyntc.errors import ConfFileNotFoundError, UnsupportedDeviceError BAD_DEVICE_TYPE = "238nzsvkn3981" @@ -12,14 +13,21 @@ @mock.patch("pyntc.devices.aireos_device.AIREOSDevice.open") -@mock.patch("pyntc.devices.f5_device.ManagementRoot") +@mock.patch("pyntc.devices.f5_device.ManagementRoot", create=True) @mock.patch("pyntc.devices.asa_device.ASADevice.open") @mock.patch("pyntc.devices.ios_device.IOSDevice.open") +@mock.patch("pyntc.devices.iosxr_device.IOSXRDevice.open") @mock.patch("pyntc.devices.nxos_device.NXOSDevice.open") @mock.patch("pyntc.devices.jnpr_device.JunosNativeSW") @mock.patch("pyntc.devices.jnpr_device.JunosNativeDevice.open") @mock.patch("pyntc.devices.jnpr_device.JunosNativeDevice.timeout") -def test_device_creation(j_timeout, j_open, j_nsw, nx_open, i_open, a_open, f_mr, air_open, device_type, expected): +@pytest.mark.parametrize("device_type,expected", supported_devices.items(), ids=list(supported_devices)) +def test_device_creation( + j_timeout, j_open, j_nsw, nx_open, xr_open, i_open, a_open, f_mr, air_open, device_type, expected +): + # Skip f5 on python >3.11 + if sys.version_info >= (3, 12) and device_type == "f5_tmos_icontrol": + pytest.skip(f"F5 not supported in Python {sys.version}") device = ntc_device(device_type, "host", "user", "pass") assert isinstance(device, expected) From 5ca57002dfa2f6137983acffddefe14e656c7aac Mon Sep 17 00:00:00 2001 From: Gary Snider <75227981+gsnider2195@users.noreply.github.com> Date: Wed, 1 Jul 2026 15:50:18 -0700 Subject: [PATCH 3/3] release v3.1.0 --- changes/398.added | 1 - docs/admin/release_notes/version_3.1.md | 14 + mkdocs.yml | 1 + poetry.lock | 391 +++++++++++------------- pyproject.toml | 4 +- 5 files changed, 200 insertions(+), 211 deletions(-) delete mode 100644 changes/398.added create mode 100644 docs/admin/release_notes/version_3.1.md diff --git a/changes/398.added b/changes/398.added deleted file mode 100644 index e01c13de..00000000 --- a/changes/398.added +++ /dev/null @@ -1 +0,0 @@ -Added native Cisco IOS-XR (eXR / 64-bit) support via the `cisco_iosxr_ssh` driver (`IOSXRDevice`), including `remote_file_copy` (FTP/TFTP/SCP/HTTP/HTTPS) and OS upgrades from a single golden ISO (built with Cisco's gisobuild tool) through the asynchronous `install add` / `install activate` / `install commit` workflow. diff --git a/docs/admin/release_notes/version_3.1.md b/docs/admin/release_notes/version_3.1.md new file mode 100644 index 00000000..9b9a21db --- /dev/null +++ b/docs/admin/release_notes/version_3.1.md @@ -0,0 +1,14 @@ +# v3.1 Release Notes + +This document describes all new features and changes in the release. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Release Overview + +- Added support for Cisco IOS-XR. + + +## [v3.1.0 (2026-07-02)](https://github.com/networktocode/pyntc/releases/tag/v3.1.0) + +### Added + +- [#398](https://github.com/networktocode/pyntc/issues/398) - Added native Cisco IOS-XR (eXR / 64-bit) support via the `cisco_iosxr_ssh` driver (`IOSXRDevice`), including `remote_file_copy` (FTP/TFTP/SCP/HTTP/HTTPS) and OS upgrades from a single golden ISO (built with Cisco's gisobuild tool) through the asynchronous `install add` / `install activate` / `install commit` workflow. diff --git a/mkdocs.yml b/mkdocs.yml index 9fec2fcd..d80c5303 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -132,6 +132,7 @@ nav: - Uninstall: "admin/uninstall.md" - Release Notes: - "admin/release_notes/index.md" + - v3.1: "admin/release_notes/version_3.1.md" - v3.0: "admin/release_notes/version_3.0.md" - v2.4: "admin/release_notes/version_2.4.md" - v2.3: "admin/release_notes/version_2.3.md" diff --git a/poetry.lock b/poetry.lock index fbcf7862..3e2ddc73 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.1 and should not be changed by hand. [[package]] name = "astroid" @@ -149,14 +149,14 @@ typecheck = ["mypy"] [[package]] name = "certifi" -version = "2026.5.20" +version = "2026.6.17" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" groups = ["main", "dev", "docs"] files = [ - {file = "certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897"}, - {file = "certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d"}, + {file = "certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db"}, + {file = "certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432"}, ] [[package]] @@ -398,14 +398,14 @@ files = [ [[package]] name = "click" -version = "8.4.1" +version = "8.4.2" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.10" groups = ["dev", "docs"] files = [ - {file = "click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2"}, - {file = "click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96"}, + {file = "click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76"}, + {file = "click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6"}, ] [package.dependencies] @@ -425,118 +425,103 @@ files = [ [[package]] name = "coverage" -version = "7.14.0" +version = "7.14.3" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "coverage-7.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:84c32d90bf4537f0e7b4dec9aaa9a938fb8205136b9d2ecf4d7629d5262dc075"}, - {file = "coverage-7.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7c843572c605ab51cfdb5c6b5f2586e2a8467c0d28eca4bdef4ec70c5fecbd82"}, - {file = "coverage-7.14.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0c451757d3fa2603354fdc789b5e58a0e327a117c370a40e3476ba4eabab228c"}, - {file = "coverage-7.14.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3fd43f0616e765ab78d069cf8358def7363957a45cee446d65c502dcfeea7893"}, - {file = "coverage-7.14.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:731e535b1498b27d13594a0527a79b0510867b0ad891532be41cb883f2128e20"}, - {file = "coverage-7.14.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c7492f2d493b976941c7ca050f273cbda2f43c381124f7586a3e3c16d1804fec"}, - {file = "coverage-7.14.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc38367eaa2abb1b766ac333142bce7655335a73537f5c8b75aaa89c2b987757"}, - {file = "coverage-7.14.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0a951308cde22cf77f953955a754d04dccb57fe3bb8e345d685778ed9fc1632a"}, - {file = "coverage-7.14.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fab3877e4ebb06bd9d4d4d00ee53309ee5478e66873c66a382272e3ee33eb7ea"}, - {file = "coverage-7.14.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b812eb847b19876ebf33fb6c4f11819af05ab6050b0bfa1bc53412ae81779adb"}, - {file = "coverage-7.14.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d9c8ef6ed820c433de075657d72dda1f89a2984955e58b8a75feb3f184250218"}, - {file = "coverage-7.14.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d128b1bba9361fbaaf6a19e179e6cfd6a9103ce0c0555876f72780acc93efd85"}, - {file = "coverage-7.14.0-cp310-cp310-win32.whl", hash = "sha256:65f267ca1370726ec2c1aa38bbe4df9a71a740f22878d2d4bf59d71a4cd8d323"}, - {file = "coverage-7.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:b34ece8065914f938ed7f2c5872bb865336977a52919149846eac3744327267a"}, - {file = "coverage-7.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a78e2a9d9c5e3b8d4ab9b9d28c985ea66fced0a7d7c2aec1f216e03a2011480"}, - {file = "coverage-7.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1816c505187592dcd1c5a5f226601a549f70365fbd00930ac88b0c225b76bb4"}, - {file = "coverage-7.14.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d8e1762f0e9cbc26ec315471e7b47855218e833cd5a032d706fbf43845d878c7"}, - {file = "coverage-7.14.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9336e23e8bb3a3925398261385e2a1533957d3e760e91070dcb0e98bfa514eed"}, - {file = "coverage-7.14.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd1169b2230f9cbe9c638ba38022ed7a2b1e641cc07f7cea0365e4be2a74980"}, - {file = "coverage-7.14.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d1bb3543b58fea74d2cd1abc4054cc927e4724687cb4560cd2ed88d2c7d820c0"}, - {file = "coverage-7.14.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a93bac2cb577ef60074999ed56d8a1535894398e2ed920d4185c3ec0c8864742"}, - {file = "coverage-7.14.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5904abf7e18cddc463219b17552229650c6b79e061d31a1059283051169cf7d5"}, - {file = "coverage-7.14.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:741f57cddc9004a8c81b084660215f33a6b597dbe62c31386b983ee26310e327"}, - {file = "coverage-7.14.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:664123feb0929d7affc135717dbd70d61d98688a08ab1e5ba464739620c6252d"}, - {file = "coverage-7.14.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:c83d2399a51bbec8429266905d33616f04bc5726b1138c35844d5fcd896b2e20"}, - {file = "coverage-7.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb2e855b87321259a037429288ae85216d191c74de3e79bf57cd2bc0761992c"}, - {file = "coverage-7.14.0-cp311-cp311-win32.whl", hash = "sha256:731dc15b385ac52289743d476245b61e1a2927e803bef655b52bc3b2a75a21f3"}, - {file = "coverage-7.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:bfb0ed8ec5d25e93face268115d7964db9df8b9aae8edcde9ec6b16c726a7cc1"}, - {file = "coverage-7.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:7ebb1c6df9f78046a1b1e0a89674cd4bf73b7c648914eebcf976a57fd99a5627"}, - {file = "coverage-7.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7ffd19fc8aed057fd686a17a4935eef5f9859d69208f96310e893e64b9b6ccf5"}, - {file = "coverage-7.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:829994cfe1aeb773ca27bf246d4badc1e764893e3bfb98fff820fcecd1ca4662"}, - {file = "coverage-7.14.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b4f07cf7edcb7ec39431a5074d7ea83b29a9f71fcfc494f0f40af4e65180420f"}, - {file = "coverage-7.14.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca3d9cf2c32b521bd9518385608787fa86f38daf993695307531822c3430ed67"}, - {file = "coverage-7.14.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92af52828e7f29d827346b0294e5a0853fa206db77db0395b282918d41e28db9"}, - {file = "coverage-7.14.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7b2bb6c9d7e769360d0f20a0f219603fd64f0c8f97de17ab25853261602be0fb"}, - {file = "coverage-7.14.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1c9ed6ef99f88fb8c14aa8e2bf8eb0fe55fa2edfea68f8675d78741df1a5ac0e"}, - {file = "coverage-7.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8231ade007f37959fbf58acc677f26b922c02eda6f0428ea307da0fd39681bf3"}, - {file = "coverage-7.14.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d8b013632cc1ce1d09dbe4f32667b4d320ec2f54fc326ebeffcd0b0bcc2bb6c4"}, - {file = "coverage-7.14.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1733198802d71ec4c524f322e2867ee05c62e9e75df86bdca545407a221827d1"}, - {file = "coverage-7.14.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:72a305291fa8ee01332f1aaf38b348ca34097f6aa0b0ef627eef2837e57bbba5"}, - {file = "coverage-7.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcaba850dd317c65423a9d63d88f9573c53b00354d6dd95724576cc98a131595"}, - {file = "coverage-7.14.0-cp312-cp312-win32.whl", hash = "sha256:5ac83957a80d0701310e96d8bec68cdcf4f90a7674b7d13f15a344315b41ab27"}, - {file = "coverage-7.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:70390b0da32cb90b501953716302906e8bcce087cb283e70d8c97729f22e92b2"}, - {file = "coverage-7.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:91b993743d959b8be85b4abf9d5478216a69329c321efe5be0433c1a841d691d"}, - {file = "coverage-7.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f2bbb8254370eb4c628ff3d6fa8a7f74ddc40565394d4f7ab791d1fe568e37ef"}, - {file = "coverage-7.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:23b81107f46d3f21d0cbce30664fcec0f5d9f585638a67081750f99738f6bf66"}, - {file = "coverage-7.14.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:22a7e06a5f11a757cdfe79018e9095f9f69ae283c5cd8123774c788deec8717b"}, - {file = "coverage-7.14.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9d1aa57a1dc8e05bdc42e81c5d671d849577aeedf279f4c449d6d286f9ed88ca"}, - {file = "coverage-7.14.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90c1a51bcfddf645b3bb7ec333d9e94393a8e94f55642380fa8a9a5a9e636cb7"}, - {file = "coverage-7.14.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a841fae2fadcae4f438d43b6ccc4aac2ad609f47cdb6cfdce60cbb3fe5ca7bc2"}, - {file = "coverage-7.14.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c79d2319cabef1fe8e86df73371126931550804738f78ad7d31e3aad85a67367"}, - {file = "coverage-7.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b23b0c6f0b1db6ad769b7050c8b641c0bf215ded26c1816955b17b7f26edfa9"}, - {file = "coverage-7.14.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:55d3089079ce181a4566b1065ab28d2575eb76d8ac8f81f4fcda2bf037fee087"}, - {file = "coverage-7.14.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:49c005cba1e2f9677fb2845dcdf9a2e72a52a17d63e8231aaaae35d9f50215ef"}, - {file = "coverage-7.14.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9117377b823daa28aa8635fbb08cda1cd6be3d7143257345459559aeef852d52"}, - {file = "coverage-7.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7b79d646cf46d5cf9a9f40281d4441df5849e445726e369006d2b117710b33fe"}, - {file = "coverage-7.14.0-cp313-cp313-win32.whl", hash = "sha256:fb609b3658479e33f9516d46f1a89dbb9b6c261366e3a11844a96ec487533dae"}, - {file = "coverage-7.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0773d8329cf32b6fd222e4b52622c61fe8d503eb966cfc8d3c3c10c96266d50e"}, - {file = "coverage-7.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:b4e26a0f1b696faf283bffe5b8569e44e336c582439df5d53281ab89ee0cba96"}, - {file = "coverage-7.14.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:953f521ca9445300397e65fda3dca58b2dbd68fee983777420b57ac3c77e9f90"}, - {file = "coverage-7.14.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:98af83fd65ae24b1fdd03aaead967a9f523bcd2f1aab2d4f3ffda65bb568a6f1"}, - {file = "coverage-7.14.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:668b92e6958c4db7cf92e81caac328dfbbdbb215db2850ad28f0cbe1eea0bfbd"}, - {file = "coverage-7.14.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9fbd898551762dea00d3fef2b1c4f99afd2c6a3ff952ea07d60a9bd5ed4f34bc"}, - {file = "coverage-7.14.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68af363c07ecd8d4b7d4043d85cb376d7d227eceb54e5323ee45da73dbd3e426"}, - {file = "coverage-7.14.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e57054a583da8ac55edf24117ea4c9133032cfc4cf72aa2d48c1e5d4b52f899"}, - {file = "coverage-7.14.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc3499459bbcdd51a65b64c35ab7ed2764eaf3cba826e0df3f1d7fe2e102b70b"}, - {file = "coverage-7.14.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:45899ec2138a4346ed34d601dedf5076fb74edf2d1dd9dc76a78e82397edee90"}, - {file = "coverage-7.14.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8767486808c436f05b23ab98eb963fb29185e32a9357a166971685cb3459900f"}, - {file = "coverage-7.14.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a3b5ddfd6aa7ddad53ee3edb231e88a2151507a43229b7d71b953916deca127d"}, - {file = "coverage-7.14.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:63df0fe568e698e1045792399f8ab6da3a6c2dce3182813fb92afa2641087b47"}, - {file = "coverage-7.14.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:827d6397dbd95144939b18f89edf31f63e1f99633e8d5f32f22ba8bdda567477"}, - {file = "coverage-7.14.0-cp313-cp313t-win32.whl", hash = "sha256:7bf43e000d24012599b879791cff41589af90674722421ef11b11a5431920bab"}, - {file = "coverage-7.14.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3f5549365af25d770e06b1f8f5682d9a5637d06eb494db91c6fa75d3950cc917"}, - {file = "coverage-7.14.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6d160217ec6fe890f16ad3a9531761589443749e448f91986c972714fad361c8"}, - {file = "coverage-7.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9aed9fa983514ca032790f3fe0d1c0e42ca7e16b42432af1706b50a9a46bef5d"}, - {file = "coverage-7.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ba3b8390db29296dbbf49e91b6fe08f990743a90c8f447ba4c2ffc29670dfa63"}, - {file = "coverage-7.14.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3a5d8e876dfa2f102e970b183863d6dedd023d3c0eeca1fe7a9787bc5f28b212"}, - {file = "coverage-7.14.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ebb8f4614a3787d567e610bbfdf96a4798dd69a1afb1bd8ad228d4111fe6ff3"}, - {file = "coverage-7.14.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b9bf47223dd8db3d4c4b2e443b02bace480d428f0822c3f991600448a176c97"}, - {file = "coverage-7.14.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3485a836550b303d006d57cc06e3d5afaabc642c77050b7c985a97b13e3776b8"}, - {file = "coverage-7.14.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e7e88110bae996d199d1693ca8ec3fd52441d426401ae963437598667b4c5eb"}, - {file = "coverage-7.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15228a6800ce7bdf1b74800595e56db7138cecb338fdbf044806e10dcf182dfe"}, - {file = "coverage-7.14.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9d26ac7f5398bafc5b57421ad994e8a4749e8a7a0e62d05ec7d53014d5963bfa"}, - {file = "coverage-7.14.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2fb73254ff43c911c967a899e1359bc5049b4b115d6e8fbdde4937d0a2246cd5"}, - {file = "coverage-7.14.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:454a380af72c6adada298ed270d38c7a391288198dbfb8467f786f588751a90c"}, - {file = "coverage-7.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:65c86fb646d2bd2972e96bd1a8b45817ed907cee68655d6295fe7ec031d04cca"}, - {file = "coverage-7.14.0-cp314-cp314-win32.whl", hash = "sha256:6a6516b02a6101398e19a3f44820f69bab2590697f7def4331f668b14adaf828"}, - {file = "coverage-7.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:45e0f79d8351fa76e256716df91eab12890d32678b9590df7ae1042e4bd4cf5d"}, - {file = "coverage-7.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:4b899594a8b2d81e5cc064a0d7f9cac2081fed91049456cae7676787e41549c9"}, - {file = "coverage-7.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f580f8c80acd94ac72e863efe2cab791d8c38d153e0b463b92dfa000d5c84cd1"}, - {file = "coverage-7.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a2bd259c442cd43c49b30fbafc51776eb19ea396faf159d26a83e6a0a5f13b0c"}, - {file = "coverage-7.14.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a706b908dfa85538863504c624b237a3cc34232bf403c057414ebfdb3b4d9f84"}, - {file = "coverage-7.14.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7333cd944ee4393b9b3d3c1b598c936d4fc8d70573a4c7dacfec5590dd50e436"}, - {file = "coverage-7.14.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f162bc9a15b82d947b02651b0c7e1609d6f7a8735ca330cfadec8481dd97d5a"}, - {file = "coverage-7.14.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:362cb78e01a5dc82009d88004cf60f2e6b6d6fcbfdec05b05af73b0abf40118f"}, - {file = "coverage-7.14.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:acebd068fca5512c3a6fde9c045f901613478781a73f0e82b307b214daef23fb"}, - {file = "coverage-7.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:29fe3da551dface75deb2ccbf87b6b66e2e7ef38f6d89050b428be94afff3490"}, - {file = "coverage-7.14.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b4cc4fce8672fffcb09b0eafc167b396b3ba53c4a7230f54b7aaffbf6c835fa9"}, - {file = "coverage-7.14.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5d4a51aad8ba8bdcd2b8bd8f03d4aca19693fa2327a3470e4718a25b03481020"}, - {file = "coverage-7.14.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9f323af3e1e4f68b60b7b247e37b8515563a61375518fa59de1af48ba28a3db6"}, - {file = "coverage-7.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1a0abc7342ea9711c469dd8b821c6c311e6bc6aac1442e5fbd6b27fae0a8f3db"}, - {file = "coverage-7.14.0-cp314-cp314t-win32.whl", hash = "sha256:a9f864ef57b7172e2db87a096642dd51e179e085ab6b2c371c29e885f65c8fb2"}, - {file = "coverage-7.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:29943e552fdc08e082eb51400fb2f58e118a83b5542bd06531214e084399b644"}, - {file = "coverage-7.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:742a73ea621953b012f2c4c2219b512180dd84489acf5b1596b0aafc55b9100b"}, - {file = "coverage-7.14.0-py3-none-any.whl", hash = "sha256:8de5b61163aee3d05c8a2beab6f47913df7981dad1baf82c414d99158c286ab1"}, - {file = "coverage-7.14.0.tar.gz", hash = "sha256:057a6af2f160a85384cde4ab36f0d2777bae1057bae255f95413cdd382aa5c74"}, + {file = "coverage-7.14.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:360bec1f58e7243e3405d3bdf7a1a8115aa9b448d54dc7cd6f7b7e0e9406b62e"}, + {file = "coverage-7.14.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed68faa5e85de2f3e400bc3f122e5c82735a58c8bb24b9f63a2215954ba17b2d"}, + {file = "coverage-7.14.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:830c1fca669c572dec37ce9c838224ee45aac5be0f6961edf871e82e49d6537c"}, + {file = "coverage-7.14.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a64caee2193563601dbaaa55fe2dcf597debef04a2f8f1fa8a07aa4bb7ac7a1e"}, + {file = "coverage-7.14.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0096fd7559178f0cc9cf088f2dbd2a02ef85bacaa69732c633517286b4494610"}, + {file = "coverage-7.14.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6197e5a00183c11a8ce7c6abd18be1a9189fd8399084ffc95196f4f0db4f2137"}, + {file = "coverage-7.14.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7dfe427045520d6abca33687dfef767b4f635015893a1816c5decb12eb72ce18"}, + {file = "coverage-7.14.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9a3f142070eb7b82fc4085a55d887396f9c4e21250bccebe2ba22502c45b9647"}, + {file = "coverage-7.14.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64b2055bb6e0dc945af35cdeceb3633e6ed9273475ef3af85592410fd6803803"}, + {file = "coverage-7.14.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1551b4caac3e3ec9f2bfcec6bf3776e01c0edbdd2e240431a50ca1a1aac72c27"}, + {file = "coverage-7.14.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:583d50d59142f8549470bd6390471d0fe8b8c8d69d6a0f28ac71e05380cef640"}, + {file = "coverage-7.14.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0bb8a6bc7015efdf8a928753b25da1b9ca2d6f24ef04d2ee0688e486f32aae7"}, + {file = "coverage-7.14.3-cp310-cp310-win32.whl", hash = "sha256:d48400185564042287dc487c1f016a3397f18ab4f4c5d5ec36edc218f7ffa35b"}, + {file = "coverage-7.14.3-cp310-cp310-win_amd64.whl", hash = "sha256:eadea7aba74e40adee867a8c0eec17b820b061d308a4b014f7a0e118c2b0aa61"}, + {file = "coverage-7.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e574801e1d643561594aa021206c46d80b257e9853087090ba97bed8b0a509d3"}, + {file = "coverage-7.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f82b6bb7d75a2613e85d07cefa3a8c973d0544a8993337f6e2728e4a1e94c305"}, + {file = "coverage-7.14.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a2335ea5fed26af2e831094964fa3f8fae60b45f7e37fcc2d3b615b2add3ad87"}, + {file = "coverage-7.14.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fbb8c3a98e779013786ae01d229662aeacbc77100efbd3f2f245219ace5af700"}, + {file = "coverage-7.14.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac082660de8f429ba0ea363595abb838998570b9a7546777c60f413ab902bbde"}, + {file = "coverage-7.14.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ac012839ff7e396030f1e94e10553a431d14e4de2ab65cb3acb72bbd5628ca2"}, + {file = "coverage-7.14.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5952f8c1bda2a5347154450379316e6dfa4d934d62ca35f6784451e6f55074fb"}, + {file = "coverage-7.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8cf0f2509acb4619e2471a1951089054dd58ebea7a912066d2ea56dd4c24ca4a"}, + {file = "coverage-7.14.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2e41fd3aab806770008279a93879b0924b16247e09ab537c043d08bbca53b4ab"}, + {file = "coverage-7.14.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f0a47095963cfe054e0df178daca95aec21e680d6076da807c3add28dfe920f7"}, + {file = "coverage-7.14.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a090cbf9521e78ffdb2fcf448b72902afe9f5923ff6a12d5c0d0120200348af9"}, + {file = "coverage-7.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4d310baf69a4fbe8a098ce727e4808a34866ac718a6f759ae659cbd3221358bc"}, + {file = "coverage-7.14.3-cp311-cp311-win32.whl", hash = "sha256:74fdd718d88fe144f4579b8747873a07ec3f04cb837d5faec5a25d9e22fa31a8"}, + {file = "coverage-7.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:cc96aa922e21d4bc5d5ed3c915cef27dfcbc13686f47d5e378d647fbfba655a2"}, + {file = "coverage-7.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:c66f9f9d4f1e9712eb9b1de5310f881d4e2188cfcba5065e1a8490f38687f2c4"}, + {file = "coverage-7.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3d74ff26299c4879ce3a4d826f9d3d4d556fd285fde7bbce3c0ef5a8ab1cec24"}, + {file = "coverage-7.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:96150a9cf3468ea20f0bc5d0e21b3df8972c31480ef90fa7614b773cc6429665"}, + {file = "coverage-7.14.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:27d07a46500ba23515b838dbcf52512026af04090755cf6cc64166d88c9b9a1a"}, + {file = "coverage-7.14.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:621e13c6108234d7960aaf5762ab5c3c00f33c30c15af06dcbff0c73bf112727"}, + {file = "coverage-7.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b60ca6d8af70473491a15a343cbabab2e8f9ea66a4376e81c7aa24876a6f977"}, + {file = "coverage-7.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c90a7cdd5e380e1ce02f19792e2ac2fbfbf177e35a27e69fd3e873b30d895c0c"}, + {file = "coverage-7.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5d788e5fd55347eef06ca0732c77d04a264de67e8ff24631270cdff3767a60cf"}, + {file = "coverage-7.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62c7f79db2851c95ef020e5d28b97afde3daf9f7febcd35b53e05638f729063f"}, + {file = "coverage-7.14.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:90f7608aeb5d9b60b523b9fb2a4ee1973867cc4865a3f26fe6c7577073b70205"}, + {file = "coverage-7.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1e3b91f9c4740aeb571ecf82e5e8d8e4ab62d34fcb5a5d4e5baa38c6f7d2857c"}, + {file = "coverage-7.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c946099774a7699de03cbd0ff0a64e21aed4525eed9d959adde4afe6d15758ef"}, + {file = "coverage-7.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:16b206e521feb8b7133a45754643dead0538489cf8b783b90cf5f4e3299625fd"}, + {file = "coverage-7.14.3-cp312-cp312-win32.whl", hash = "sha256:ea3169c7116eb6cdf7608c6c7da9ecfcb3da40688e3a510fac2d1d2bafd6dc35"}, + {file = "coverage-7.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:7ea52fc08f007bcc494d4bb3df3851e95843d881860ba38fe2c64dc100db5e7d"}, + {file = "coverage-7.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:8cec0ad652ec57790970d817490105bd917d783c2f7b38d6b58a0ca312e1a336"}, + {file = "coverage-7.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47968988b367990ae4ab17523790c38cd125e02c6bfd379b6022be2d40bdc38c"}, + {file = "coverage-7.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0ee68f5c34812780f3a7063382c0a9fcbb99985b7ddcdcaa626e4f3fb2e0783a"}, + {file = "coverage-7.14.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fa9e5c6857a7e80fa22ace5cf3550ae392bbfc322f1d8dd2d2d5a8be38cec027"}, + {file = "coverage-7.14.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:98a0859b0e98e43e1178a9402e19c8127766b14f7109a374d976e5a62c0e5c73"}, + {file = "coverage-7.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69918344541ed9c8368566c2adc03c0e33d4550d7faa87d1b35e49b6a3286ea9"}, + {file = "coverage-7.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b7f300ac92cd4b570724c8ffbbd0c130fee298d2447f41d5a3abf58976fae1de"}, + {file = "coverage-7.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11a7ec9f97ab950f4c5af62229befc7faf208fdbc0116d3902d7e306cf2c5abd"}, + {file = "coverage-7.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a571bd889cd36c5922ce8e42e059f9d37d02301531d11374afa4c87a578625d5"}, + {file = "coverage-7.14.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:de76caefc8deabb0dd1678b6a980be97d14c8d87e213ac194dbf8b09e96d63fb"}, + {file = "coverage-7.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d20a15c622194234161535459affa8f7905830391c9ccfa060d495dbfe3a1c7f"}, + {file = "coverage-7.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b488bd4b23397db62e7a9459129d01ff06a846582a732efd24834b24a6ada498"}, + {file = "coverage-7.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6a3693b4153394d265f44fb855fdc80e72403024d4d6f91c4871b334d028e4e0"}, + {file = "coverage-7.14.3-cp313-cp313-win32.whl", hash = "sha256:338b19131ab1a6b767b462bfcbaa692e7ae22f24463e39d49b02a83410ff6b37"}, + {file = "coverage-7.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:b3d77f7f196abdef7e01415de1bce09f216189e83e58159cfeef2b92d0464994"}, + {file = "coverage-7.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:e6230e688c7c3e65cedd41a774eb4ec221adc6bfee13768231015b702d5e4150"}, + {file = "coverage-7.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:605ab2b566a22bd94834529d66d295c364aba84afd3e5498285c7a524017b1fc"}, + {file = "coverage-7.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a3c2134809e80fac091bfed18a6991b5a5eb5df5ae32b17ac4f4f99864b73dd7"}, + {file = "coverage-7.14.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c02efd507227bde9969cab0db8f48890eb3b5dcad6afac57a4792df4133543ce"}, + {file = "coverage-7.14.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1bb93c2aa61d2a5b38f1526546d95cf4132cb681e541a337bf8dfd092be816e5"}, + {file = "coverage-7.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f502e948e03e866538048bba081c075caaa62e5bda6ea5b7432e45f587eb462a"}, + {file = "coverage-7.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9973ef2463f8e6cfb61a6324126bb3e17d67a85f22f58d856e583ea2e3ca6501"}, + {file = "coverage-7.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9be4e7d4c5ca0427889f8f9d614bd630c2be741b1de7699bca3b2b6c0e41003e"}, + {file = "coverage-7.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a574912f3bde4b0619f6e97d01aa590b70998859244793769eb3a6df78ee56d3"}, + {file = "coverage-7.14.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e343fb086c9cd780b38622fea7c369acd64c1a0724312149b5d769c387a2b1f5"}, + {file = "coverage-7.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:3c68df8e61f1e09633fefc7538297145623957a048534368c9d212782aa5e845"}, + {file = "coverage-7.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3e5b550a128419373c2f6cec28a244207013ef15f5cbcff6a5ca09d1dfaaf027"}, + {file = "coverage-7.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2bfc4dd0a912329eccc7484a7d0b2a38032b38c40663b1e1ac595f10c457954b"}, + {file = "coverage-7.14.3-cp314-cp314-win32.whl", hash = "sha256:0423d64c013057a06e70f070f073cec4b0cbc7d2b27f3c7007292f2ff1d52965"}, + {file = "coverage-7.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:92c22e19ce64ca3f2ad751f16f14df1468b4c231bd6af97185063a9c292a0cb3"}, + {file = "coverage-7.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:41de778bd41780586e2b04912079c73089ab5d839624e28db3bdb26de638da92"}, + {file = "coverage-7.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8427f370ca67db4c975d2a26acfc0e5783ca0b52444dbc50278ace0f35445949"}, + {file = "coverage-7.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d8e88f335544a47e22ae2e45b344772925ec65166555c958720d5ed971880891"}, + {file = "coverage-7.14.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:beaab199b9e5ceaf5a225e16a9d4df136f2a1eae0a5c20de1e277c8a5225f388"}, + {file = "coverage-7.14.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3ff255799f5a1676c71c1c32ec01fd043aa09d57b3d95764b24992757184784"}, + {file = "coverage-7.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:878832eaac515b62decfa76965aed558775f86bf1fc8cca76993c0c84ae31aed"}, + {file = "coverage-7.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:611e62cb9386096d81b63e0a05330750268617231e7bd598e1fe77482a2c58a5"}, + {file = "coverage-7.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:02c41de2a88011b893050fc9830267d927a50a215f7ad5ec17349db7090ccf26"}, + {file = "coverage-7.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:526ce9721116af23b1065089f0b75046fe521e7772ab94b641cd66b7a0421889"}, + {file = "coverage-7.14.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e4ed44705ca4bead6fc977a8b741f2145608289b33c8a9b42a95d0f15aedbf4d"}, + {file = "coverage-7.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2415902f385a23dcc4ccd26e0ba803249a169af6a930c003a4c715eeb9a5444e"}, + {file = "coverage-7.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b75ee850fc2d7c831e883220c445b035f2224de2ba6103f1e56dbd237ab913f7"}, + {file = "coverage-7.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dc9b4e35e7c3920e925ba7f14886fd5fbe481232754624e832ddba66c7535635"}, + {file = "coverage-7.14.3-cp314-cp314t-win32.whl", hash = "sha256:7b27c822a8161afbe48e99f1adfb098d270ae7e0f7d7b0555ce110529bdb69cc"}, + {file = "coverage-7.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:39e1dbbb6ff2c338e0196a482558a792a1de3aa64261196f5cdb3da016ad9cda"}, + {file = "coverage-7.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:68520c90babfa2d560eca6d497921ed3a4f469623bd709733124491b2aa8ef3f"}, + {file = "coverage-7.14.3-py3-none-any.whl", hash = "sha256:fb7e18afb6e903c1a92401a2f0501ac277dca527bb9ca6fe1f691a8a0026a0e8"}, + {file = "coverage-7.14.3.tar.gz", hash = "sha256:1a7563a443f3d53fdeb040ec8c9f7466aed7ca3dc5891aa09d3ca3625fa4387f"}, ] [package.extras] @@ -544,61 +529,58 @@ toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "cryptography" -version = "48.0.0" +version = "49.0.0" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = "!=3.9.0,!=3.9.1,>=3.9" groups = ["main"] files = [ - {file = "cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6"}, - {file = "cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c"}, - {file = "cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3"}, - {file = "cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5"}, - {file = "cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c"}, - {file = "cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f"}, - {file = "cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25"}, - {file = "cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602"}, - {file = "cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c"}, - {file = "cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5"}, - {file = "cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321"}, - {file = "cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74"}, - {file = "cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4"}, - {file = "cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7"}, - {file = "cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec"}, - {file = "cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18"}, - {file = "cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20"}, - {file = "cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff"}, - {file = "cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c"}, - {file = "cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db"}, - {file = "cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741"}, - {file = "cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166"}, - {file = "cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336"}, - {file = "cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057"}, - {file = "cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae"}, - {file = "cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c"}, - {file = "cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f"}, - {file = "cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12"}, - {file = "cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86"}, - {file = "cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e"}, - {file = "cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f"}, - {file = "cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7"}, - {file = "cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832"}, - {file = "cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c"}, - {file = "cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a"}, - {file = "cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a"}, - {file = "cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a"}, - {file = "cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239"}, - {file = "cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c"}, - {file = "cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4"}, - {file = "cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd"}, - {file = "cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8"}, - {file = "cryptography-48.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:84cf79f0dc8b36ac5da873481716e87aef31fcfa0444f9e1d8b4b2cece142855"}, - {file = "cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fdfef35d751d510fcef5252703621574364fec16418c4a1e5e1055248401054b"}, - {file = "cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0890f502ddf7d9c6426129c3f49f5c0a39278ed7cd6322c8755ffca6ee675a13"}, - {file = "cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:ecde28a596bead48b0cfd2a1b4416c3d43074c2d785e3a398d7ec1fc4d0f7fbb"}, - {file = "cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:4defde8685ae324a9eb9d818717e93b4638ef67070ac9bc15b8ca85f63048355"}, - {file = "cryptography-48.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:db63bf618e5dea46c07de12e900fe1cdd2541e6dc9dbae772a70b7d4d4765f6a"}, - {file = "cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920"}, + {file = "cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db"}, + {file = "cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db"}, + {file = "cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325"}, + {file = "cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2"}, + {file = "cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b"}, + {file = "cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6"}, + {file = "cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c"}, + {file = "cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7"}, + {file = "cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68"}, + {file = "cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9"}, + {file = "cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f"}, + {file = "cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459"}, + {file = "cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e"}, + {file = "cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7"}, + {file = "cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d"}, + {file = "cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa"}, + {file = "cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb"}, + {file = "cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d"}, + {file = "cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561"}, + {file = "cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122"}, + {file = "cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505"}, + {file = "cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866"}, + {file = "cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8"}, + {file = "cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3"}, + {file = "cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27"}, + {file = "cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61"}, + {file = "cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a"}, + {file = "cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4"}, + {file = "cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18"}, + {file = "cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69"}, + {file = "cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64"}, + {file = "cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21"}, + {file = "cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9"}, + {file = "cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc"}, + {file = "cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8"}, + {file = "cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36"}, + {file = "cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e"}, + {file = "cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b"}, + {file = "cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001"}, + {file = "cryptography-49.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b"}, + {file = "cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838"}, + {file = "cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5"}, + {file = "cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615"}, + {file = "cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6"}, + {file = "cryptography-49.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6"}, + {file = "cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493"}, ] [package.dependencies] @@ -719,14 +701,14 @@ files = [ [[package]] name = "hypothesis" -version = "6.153.0" +version = "6.155.7" description = "The property-based testing library for Python" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "hypothesis-6.153.0-py3-none-any.whl", hash = "sha256:2aeda9bbb44ae0ee0bfa67ef744a25be05c1f804dca4eb6479c63518dc9f2900"}, - {file = "hypothesis-6.153.0.tar.gz", hash = "sha256:11616e5158fc485d62bae19d9cc69333237faa8050ad44a45218254a1ef272bb"}, + {file = "hypothesis-6.155.7-py3-none-any.whl", hash = "sha256:9f634bdb1f9e9b8ab6ba09431cf2deedb750c96978125a6fb3c5a0f6c6db4131"}, + {file = "hypothesis-6.155.7.tar.gz", hash = "sha256:d8d6091753d0669db3c90c5e5b346cb37c72f3dd9378c8413acb1fd5da63f7ea"}, ] [package.dependencies] @@ -734,10 +716,10 @@ exceptiongroup = {version = ">=1.0.0", markers = "python_version < \"3.11\""} sortedcontainers = ">=2.1.0,<3.0.0" [package.extras] -all = ["black (>=20.8b0)", "click (>=7.0)", "crosshair-tool (>=0.0.104)", "django (>=5.2)", "dpcontracts (>=0.4)", "hypothesis-crosshair (>=0.0.28)", "lark (>=0.10.1)", "libcst (>=0.3.16)", "numpy (>=1.21.6)", "pandas (>=1.1)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2026.2) ; sys_platform == \"win32\" or sys_platform == \"emscripten\"", "watchdog (>=4.0.0)"] +all = ["black (>=20.8b0)", "click (>=7.0)", "crosshair-tool (>=0.0.106)", "django (>=5.2)", "dpcontracts (>=0.4)", "hypothesis-crosshair (>=0.0.28)", "lark (>=0.10.1)", "libcst (>=0.3.16)", "numpy (>=1.21.6)", "pandas (>=1.1)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2026.2) ; sys_platform == \"win32\" or sys_platform == \"emscripten\"", "watchdog (>=4.0.0)"] cli = ["black (>=20.8b0)", "click (>=7.0)", "rich (>=9.0.0)"] codemods = ["libcst (>=0.3.16)"] -crosshair = ["crosshair-tool (>=0.0.104)", "hypothesis-crosshair (>=0.0.28)"] +crosshair = ["crosshair-tool (>=0.0.106)", "hypothesis-crosshair (>=0.0.28)"] dateutil = ["python-dateutil (>=1.4)"] django = ["django (>=5.2)"] dpcontracts = ["dpcontracts (>=0.4)"] @@ -753,14 +735,14 @@ zoneinfo = ["tzdata (>=2026.2) ; sys_platform == \"win32\" or sys_platform == \" [[package]] name = "idna" -version = "3.16" +version = "3.18" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.9" groups = ["main", "dev", "docs"] files = [ - {file = "idna-3.16-py3-none-any.whl", hash = "sha256:cc246e3a3f89580c3a951b5ad298ca4638078b2cdd4f115654332b5c26daded5"}, - {file = "idna-3.16.tar.gz", hash = "sha256:d7a6da03db833450fca25d2358ac9ff06cd624577a4aea3a596d5c0f77b8e03d"}, + {file = "idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2"}, + {file = "idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848"}, ] [package.extras] @@ -826,18 +808,18 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "junos-eznc" -version = "2.8.0" +version = "2.8.2" description = "Junos 'EZ' automation for non-programmers" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "junos_eznc-2.8.0-py2.py3-none-any.whl", hash = "sha256:de34f4d857e897cd9e24196abc7bc96868f6596ef40722488c01bbe9c133f912"}, - {file = "junos_eznc-2.8.0.tar.gz", hash = "sha256:be7faf9edf3397f22fdd0ce1a544a77e63d2a8c4bf3cc8b8704ed78786fcb256"}, + {file = "junos_eznc-2.8.2-py2.py3-none-any.whl", hash = "sha256:2e0038910e8bcbc2eeb61f7221127e7cfcfab54a682462076534db56326157ce"}, + {file = "junos_eznc-2.8.2.tar.gz", hash = "sha256:bb0426e74bce4544405723a9047341ede91b29cd9b61f1be9e633a55c470fffc"}, ] [package.dependencies] -jinja2 = ">=2.7.1" +jinja2 = ">=3.1.6" lxml = ">=3.2.4" ncclient = "0.7.0" paramiko = ">=3.5.0" @@ -872,7 +854,6 @@ files = [ {file = "lxml-6.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37a58976370f36d9329d118ad0b953c5aeb9119ac9c6a4e258942a225d0573a1"}, {file = "lxml-6.1.1-cp310-cp310-win32.whl", hash = "sha256:cea3f4c1af79af13cdb2da0c028111d8f8522d4f22a000c82385535f24e5cf3a"}, {file = "lxml-6.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:3abf332af33a74288675d936fe861fd4344da0dd6622193fbc4f2bfbb35536b5"}, - {file = "lxml-6.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:8dadbe5b217ff35b6a8d16610dd710219b59b76d13f0e3f0d9f36786206e4485"}, {file = "lxml-6.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:53b7d2b7a10b1c35c0a5e21e9224accf60c1bbfba523990732e521b2b73adef2"}, {file = "lxml-6.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3f333630ab480244a1bff72043e511a91eb22e7595dead8653ee5612dd8f3d"}, {file = "lxml-6.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a4bbea04c97f6d78a48e3fbc1cb9116d2780b1b39e03a23f6eb9b603fd61f510"}, @@ -888,7 +869,6 @@ files = [ {file = "lxml-6.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:80c2dfadb855da477cf73373ad29a333535dedb9b12bad02c9814c8e2b43bf08"}, {file = "lxml-6.1.1-cp311-cp311-win32.whl", hash = "sha256:30a89d3ac8faec007453fb541f3f46807eeec88edd5826f6e3fe001752a2c621"}, {file = "lxml-6.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:abbefa31eee84842140f67acef1c828e28bba8bbf0c3bc6e5492a9af88152c28"}, - {file = "lxml-6.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:dcb292aa7fe485ceff7af4f92e46c5af397daec5dff64871a528f0fc47a3cc5b"}, {file = "lxml-6.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:104c09bda8d2a562824c0e319d0768ce26a779b7601e0931d33b09b53c392ef7"}, {file = "lxml-6.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:25c6997a9a534e016695a0ba06b2f07945de682731ff01065b6d5a4474179da1"}, {file = "lxml-6.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c921ba5c51e4e9f63b8b00267d06566e1f63407408a0496da2d1d0bfc819c7fc"}, @@ -906,7 +886,6 @@ files = [ {file = "lxml-6.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aa366a1e55b8ebfe8ca8ddc3cfe75c8ebade181aeb0f661d0cb05986b647f72a"}, {file = "lxml-6.1.1-cp312-cp312-win32.whl", hash = "sha256:126c93f7f56f0eda92f6d8c619edc463a4f23d9252f1c9d0405a76f25fa9f11a"}, {file = "lxml-6.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:26e6eda8d38c1fcab1090dd196ee87cbd13788e531937610e2589085de074e77"}, - {file = "lxml-6.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:6540377fbd53fe1b629172288c464fb18db11ce1fa7dc15891da10aa9dcc3e7f"}, {file = "lxml-6.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:68a9198d0fc122d14bb76837de9aa80cf84caed990b5b237f532ed87d3706736"}, {file = "lxml-6.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7d47866cb32fb503450b6edc9df355d10dc49836af2e89901bd6ac6b0896d9d9"}, {file = "lxml-6.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb7c9811bfaa8b1ed5ed319f5d370dfbcaa59d52ea64be2a5a85e18195930354"}, @@ -924,7 +903,6 @@ files = [ {file = "lxml-6.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:47402e62c52ff5988c1e8c6c63177f5708bccf48e366dea4e3dcf1e645e04947"}, {file = "lxml-6.1.1-cp313-cp313-win32.whl", hash = "sha256:3483644525531e1d5762b0c44a8e18b6efba321b6dcf8a8952de10b037618bca"}, {file = "lxml-6.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:a10bd2fd62e8ce916ececb342f348f190724a098c1faa056fdfb2a22ad5e8660"}, - {file = "lxml-6.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:424aa57aca0897eb922aef34395bd1289b3b6f04e6bae20ea123c0c7e333cffc"}, {file = "lxml-6.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:19b7ab10b210b0b3ad7985d9ac4eb66ab09a90b20fe6e2f7ba55d01a234345d0"}, {file = "lxml-6.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c08e5c694306507275f2290073350c4f32e383db15213b2c69e7ff39c1193840"}, {file = "lxml-6.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:74a9717fd0d82effef5c2854f0d917231d5324b5a3eb7275c43ac9fa32f97a14"}, @@ -942,7 +920,6 @@ files = [ {file = "lxml-6.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63876be28efefa04a1df615b46770e82042cce445cfdce55160522f57b231ccb"}, {file = "lxml-6.1.1-cp314-cp314-win32.whl", hash = "sha256:7f7a92e8583f06b1fd49d01158143b8461cfcd135dcb10ec807270a3051bd603"}, {file = "lxml-6.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:b2d444f2e66624d68e9c6b211e28a76e22fff5fcabcfff4deac18b529b7d4137"}, - {file = "lxml-6.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:3fd9728a2735fda14f4e8235830c86b539e9661e849665bf926d3f867943b4bf"}, {file = "lxml-6.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:787b2496d0dbe8cd180984e8d29e3a6f76e7ea34db781cb3bd55e4ba1ef8b4ee"}, {file = "lxml-6.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2c8daa471358dc2d6fcf02165e80ec68f77871a286df95bc5cc3816153b0fd2c"}, {file = "lxml-6.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:acd7d70b64c0aae0c7922cca83d288a16f5f6da523637697872253415269baef"}, @@ -960,7 +937,6 @@ files = [ {file = "lxml-6.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9e36f163528fc50cbef305f02a5fd66d404edf7049cdaff211dbc2cba5a7013e"}, {file = "lxml-6.1.1-cp314-cp314t-win32.whl", hash = "sha256:649dda677cf3bd6ac9ae14007ba0c824ded8ce5808b53fc7431d9140399118c1"}, {file = "lxml-6.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:793033d6c5cdf33a573f910d9bea14ef8f5771820411d118da8e1182edb53d5e"}, - {file = "lxml-6.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:58bb955caba94e467d2a96da17660d2d704e0675894cba21ab8a775b8621fd1c"}, {file = "lxml-6.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6689e828a94eee4f139408c337bb198e014724bb8a8c26d3cfac49d119ed69a6"}, {file = "lxml-6.1.1-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bdebcc8a75d38c7598dfb2c9ed852d7a9eb4a10d6e2d0764b919b802bf32ac88"}, {file = "lxml-6.1.1-cp38-cp38-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8be8ad51249698103d24b0571df35a10990fbe93dd043b6c024172189485f5e3"}, @@ -983,7 +959,6 @@ files = [ {file = "lxml-6.1.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c9a4b821dc7055bf9e05ff5719e18ec501f75c0f0bbfabd573b277559780833d"}, {file = "lxml-6.1.1-cp39-cp39-win32.whl", hash = "sha256:639f6c857d91d9be29bd7502348d6736dab168b54b5158cd899abf11684dc186"}, {file = "lxml-6.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:34c2d737beabfe35baada43941ed519251e9a12e779031496bcd5d539fcfd730"}, - {file = "lxml-6.1.1-cp39-cp39-win_arm64.whl", hash = "sha256:07a4a68e286ee7a1ed7dfb8af83e615757c0ccfe9f18c6b4ea6771388d9ba8c9"}, {file = "lxml-6.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:31033dc34636ea6b7d5cc11b1ddbda78a14de858ba9d3e1ed4b69a3085bc521e"}, {file = "lxml-6.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3893c14c4b6ac5b2d54ba8cf03e99fe5104e592de491f19bd6b82756c09f8004"}, {file = "lxml-6.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c07da4cebf6889f03ebac8d238f62318e29f495de0aa18a51ea14e61ae907e2e"}, @@ -1595,14 +1570,14 @@ re2 = ["google-re2 (>=1.1)"] [[package]] name = "platformdirs" -version = "4.9.6" +version = "4.10.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.10" groups = ["dev", "docs"] files = [ - {file = "platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917"}, - {file = "platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a"}, + {file = "platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a"}, + {file = "platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7"}, ] [[package]] @@ -1790,14 +1765,14 @@ cp2110 = ["hidapi"] [[package]] name = "pytest" -version = "9.0.3" +version = "9.1.1" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9"}, - {file = "pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c"}, + {file = "pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c"}, + {file = "pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313"}, ] [package.dependencies] @@ -2004,30 +1979,30 @@ oldlibyaml = ["ruamel.yaml.clib ; platform_python_implementation == \"CPython\"" [[package]] name = "ruff" -version = "0.15.14" +version = "0.15.20" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "ruff-0.15.14-py3-none-linux_armv6l.whl", hash = "sha256:8dd2db9416e487c8d4b01fa7056bb02c4d05969d4f8d17a08c229c2f4ff3c108"}, - {file = "ruff-0.15.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:be4ff55af755bd71a00ab3dc6bd7ffc467bd76e0df6881e286c2e3d23e8fb43b"}, - {file = "ruff-0.15.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:48d5909d7d06276ce7dde6d32bfa4b0d4cb2651145cd8ee4b440722cbc77832f"}, - {file = "ruff-0.15.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca8cbfa94c4f90984a67561978602746d4cd27103568f745fa90eee3f0d4107d"}, - {file = "ruff-0.15.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a6bbc0333f1ab053423bcbf6226477d266ca7cec7738c4c8e3f55647803f3c4"}, - {file = "ruff-0.15.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a24a4f7605d7003a6674d4387651effd939dead3fddd0f36561eb77a9a2e542"}, - {file = "ruff-0.15.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:049b5326e53ed80978f2fc041a280603f69dd6b0c95464342a2bb4572d9d9e2f"}, - {file = "ruff-0.15.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4ed42e6696c8dfa5f06728e6441993901f548eb92d73bc472cb5a38d1395fbf"}, - {file = "ruff-0.15.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:715c543cf450c4888251f91c52f1942a800541d9bddd7ac060aa4e6b77ae7cba"}, - {file = "ruff-0.15.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ebab6013ec887d439d8b7593737a0a4ffb06d45d209d4e4bf2e92813082d3f"}, - {file = "ruff-0.15.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:49072d36abdbe97a8dd7f480afe9c675699c0c495d4c84076e2c1203c4550581"}, - {file = "ruff-0.15.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:958522aee105068640c2c2ceae08f413ae44d922f52a1374ac13d6a96032fc93"}, - {file = "ruff-0.15.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f3707da619a143a2e8830e2abab8224478d69ace2d28cb6c20543ae97c36bf61"}, - {file = "ruff-0.15.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bb01d645694e3ec0102105d07ef2d53703970407d59c04e59d3ba0b7a1d53553"}, - {file = "ruff-0.15.14-py3-none-win32.whl", hash = "sha256:6d0c1ad2a0ab718d39b6d8fd2217981ce4d625cd96a720095f798fb47d8b13e6"}, - {file = "ruff-0.15.14-py3-none-win_amd64.whl", hash = "sha256:802342981e056db3851a7836e5b070f8f15f67d4a685ae2a6160939d364b2902"}, - {file = "ruff-0.15.14-py3-none-win_arm64.whl", hash = "sha256:ff47b90a9ef6a40c9e2f3b479c1fb78531adf055b94c1eba0a7ba04b31951826"}, - {file = "ruff-0.15.14.tar.gz", hash = "sha256:48e866b165be4a9bdbf310f7d3c9a07edef2fe8cd63ffeb4e00bb590506ebf9f"}, + {file = "ruff-0.15.20-py3-none-linux_armv6l.whl", hash = "sha256:00e188c53e499c3c1637f73c91dcf2fb56d576cab76ce1be50a27c4e80e37078"}, + {file = "ruff-0.15.20-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9ebd1fd9b9c95fc0bd7b2761aebec1f030013d2e193a2901b224af68fe47251b"}, + {file = "ruff-0.15.20-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5b16cdd67ca108185cd36dce98c576350c03b1660a751de725fb049193a0632"}, + {file = "ruff-0.15.20-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3413bb3c3d2ca6a8208f1f4809cd2dca3c6de6d0b491c0e70847672bde6e6efd"}, + {file = "ruff-0.15.20-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd7ec42b3bb3da066488db093308a69c4ac5ee6d2af333a86ba6e2eb2e7dd44b"}, + {file = "ruff-0.15.20-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1a36ad0eb77fba9aabfb69ede54de6f376d04ac18ebea022847046d340a8267"}, + {file = "ruff-0.15.20-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6df3b1e4610432f0386dba04d853b5f08cbbc903410c6fcc02f620f05aff53c"}, + {file = "ruff-0.15.20-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e89f198a1ea6ef0d727c1cf16088bc91a6cb0ab947dedc966715691647186eae"}, + {file = "ruff-0.15.20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309809086c2acb67624950a3c8133e80f32d0d3e27106c0cd60ff26657c9f24b"}, + {file = "ruff-0.15.20-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2d2374caa2f2c2f9e2b7da0a50802cfb8b79f55a9b5e49379f564544fbf56487"}, + {file = "ruff-0.15.20-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a1ed17b65293e0c2f22fc387bc13198a5de94bf4429589b0ff6946b0feaf21a3"}, + {file = "ruff-0.15.20-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f701305e66b38ea6c91882490eb73459796808e4c6362a1b765255e0cdcd4053"}, + {file = "ruff-0.15.20-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b9c0c367ad8e5d0d5b5b8537864c469a0a0e55417aadfbeca41fa61333be9f4"}, + {file = "ruff-0.15.20-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:01cc00dd58f0df339d0e902219dd53990ea99996a0344e5d9cc8d45d5307e460"}, + {file = "ruff-0.15.20-py3-none-win32.whl", hash = "sha256:ed65ef510e43a137207e0f01cfcf998aeddb1aeeda5c9d35023e910284d7cf21"}, + {file = "ruff-0.15.20-py3-none-win_amd64.whl", hash = "sha256:a525c81c70fb0380344dd1d8745d8cc1c890b7fc94a58d5a07bd8eb9557b8415"}, + {file = "ruff-0.15.20-py3-none-win_arm64.whl", hash = "sha256:2f5b2a6d614e8700388806a14996c40fab2c47b819ef57d790a34878858ed9ca"}, + {file = "ruff-0.15.20.tar.gz", hash = "sha256:1416eb04349192646b54de98f146c4f59afe37d0decfc02c3cbbf396f3a28566"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index af9e003f..bbe1428d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pyntc" -version = "3.0.3a0" +version = "3.1.0" description = "Python library focused on tasks related to device level and OS management." authors = ["Network to Code, LLC "] readme = "README.md" @@ -172,7 +172,7 @@ addopts = "-vv --doctest-modules -p no:warnings -p no:f5sdk_fixtures --ignore-gl [tool.towncrier] package = "pyntc" directory = "changes" -filename = "docs/admin/release_notes/version_3.0.md" +filename = "docs/admin/release_notes/version_3.1.md" template = "towncrier_template.j2" start_string = "" issue_format = "[#{issue}](https://github.com/networktocode/pyntc/issues/{issue})"