From c115720c1f5d2f88b7afd8f30fa201b707a6baea Mon Sep 17 00:00:00 2001 From: Matt Miller Date: Fri, 22 May 2026 10:44:07 -0400 Subject: [PATCH 1/6] Bug Fix NXOS (#387) * Fix operations to use native_ssh instead of nx-api * Update NXOS so that both nx-api and ssh can co-exist * Updates to remove api_port * remove redundant line, update tests * changelog * minor tweak to f5 workaround * Updated the following NXOSDevice methods to use netmiko: - _image_booted - _wait_for_device_reboot - uptime - hostname - redundancy_state - reboot - set_boot_options Removed caching for the uptime and uptime_string properties Added an NXOSDevice.show_netmiko method and added a deprecation warning to the existing NXOSDevice.show method * added test mocks for most of the netmiko commands and updated tests * revert breaking property changes and fix pylint * revert breaking changes * Updated the NXOSDevice driver to use netmiko for the os_version * updated the nxos file transfer methods to resolve file verification * ruff ruff * add changelog fragments * update the NXOSDevice.save to use netmiko instead of NXAPI * Migrate NXOSDevice.show to netmiko from NXAPI * update _wait_for_device_reboot to reconnect via ssh * updates per CI failures * updates per CI failures --------- Co-authored-by: Gary Snider <75227981+gsnider2195@users.noreply.github.com> Co-authored-by: James Williams --- changes/387.added | 1 + changes/387.changed | 1 + changes/387.fixed | 8 + poetry.lock | 689 +++++++++--------- pyntc/devices/f5_device.py | 12 +- pyntc/devices/nxos_device.py | 198 +++-- tests/integration/conftest.py | 2 +- tests/integration/test_nxos_device.py | 290 ++++++++ .../device_mocks/nxos/__init__.py | 30 +- .../device_mocks/nxos/show_netmiko/dir | 9 + .../device_mocks/nxos/show_netmiko/reload | 1 + .../nxos/show_netmiko/show_cdp_neighbors | 18 + .../device_mocks/nxos/show_netmiko/show_clock | 10 + .../nxos/show_netmiko/show_hostname | 5 + .../nxos/show_netmiko/terminal_dont-ask | 1 + .../device_mocks/nxos/show_raw/dir | 11 + .../nxos/show_raw/terminal_dont-ask | 0 tests/unit/test_devices/test_nxos_device.py | 413 +++++++++-- 18 files changed, 1235 insertions(+), 464 deletions(-) create mode 100644 changes/387.added create mode 100644 changes/387.changed create mode 100644 changes/387.fixed create mode 100644 tests/integration/test_nxos_device.py create mode 100644 tests/unit/test_devices/device_mocks/nxos/show_netmiko/dir create mode 100644 tests/unit/test_devices/device_mocks/nxos/show_netmiko/reload create mode 100644 tests/unit/test_devices/device_mocks/nxos/show_netmiko/show_cdp_neighbors create mode 100644 tests/unit/test_devices/device_mocks/nxos/show_netmiko/show_clock create mode 100644 tests/unit/test_devices/device_mocks/nxos/show_netmiko/show_hostname create mode 100644 tests/unit/test_devices/device_mocks/nxos/show_netmiko/terminal_dont-ask create mode 100644 tests/unit/test_devices/device_mocks/nxos/show_raw/dir create mode 100644 tests/unit/test_devices/device_mocks/nxos/show_raw/terminal_dont-ask diff --git a/changes/387.added b/changes/387.added new file mode 100644 index 00000000..24934f42 --- /dev/null +++ b/changes/387.added @@ -0,0 +1 @@ +Added an `NXOSDevice.show_netmiko` method and deprecated the existing `NXOSDevice.show` method that uses pynxos. Developers should transition to the `show_netmiko` method to prepare for the eventual removal of pynxos. diff --git a/changes/387.changed b/changes/387.changed new file mode 100644 index 00000000..40851e39 --- /dev/null +++ b/changes/387.changed @@ -0,0 +1 @@ +Changed the following NXOSDevice methods/properties to use Netmiko instead of pynxos: `_image_booted`, `_wait_for_device_reboot`, `uptime`, `hostname`, `os_version`, `_get_file_system`, `_get_free_space`, `remote_file_copy`, `redundancy_state`, `reboot`, `set_boot_options`, and `startup_config`. diff --git a/changes/387.fixed b/changes/387.fixed new file mode 100644 index 00000000..5f1a8b4e --- /dev/null +++ b/changes/387.fixed @@ -0,0 +1,8 @@ +Fixed a bug in nxos where nx-api commands were mixed with ssh commands. +Fixed a bug in nxos `_build_url_copy_command_simple` returning the wrong type of data. +Fixed a bug in nxos failing to answer a prompt when using remote_file_copy. +Fixed NXOSDevice.os_version to use netmiko SSH instead of NX-API. +Fixed NXOSDevice.get_remote_checksum to use the correct `show file` command form and parse the digest out of the device output. +Fixed NXOSDevice.save to use netmiko SSH instead of NX-API. +Fixed NXOSDevice.show to use netmiko SSH instead of NX-API. Structured (non-`raw_text`) results are now TextFSM-parsed lists of dicts. +Fixed NXOSDevice._wait_for_device_reboot to drop the pre-reboot SSH session and reconnect each poll so it reliably detects when the device comes back from a reload. diff --git a/poetry.lock b/poetry.lock index b2ba1c43..3e9e9c6b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -398,14 +398,14 @@ files = [ [[package]] name = "click" -version = "8.3.3" +version = "8.4.0" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.10" groups = ["dev", "docs"] files = [ - {file = "click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613"}, - {file = "click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2"}, + {file = "click-8.4.0-py3-none-any.whl", hash = "sha256:40c50b7c6c6adac2823d411041ec84f3f103f1b280d5e9ce0d7f998995832f81"}, + {file = "click-8.4.0.tar.gz", hash = "sha256:638f1338fe1235c8f4e008e4a8a254fb5c5fbdcbb40ece3c9142ebb78e792973"}, ] [package.dependencies] @@ -425,118 +425,118 @@ files = [ [[package]] name = "coverage" -version = "7.13.5" +version = "7.14.0" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "coverage-7.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0723d2c96324561b9aa76fb982406e11d93cdb388a7a7da2b16e04719cf7ca5"}, - {file = "coverage-7.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52f444e86475992506b32d4e5ca55c24fc88d73bcbda0e9745095b28ef4dc0cf"}, - {file = "coverage-7.13.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:704de6328e3d612a8f6c07000a878ff38181ec3263d5a11da1db294fa6a9bdf8"}, - {file = "coverage-7.13.5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a1a6d79a14e1ec1832cabc833898636ad5f3754a678ef8bb4908515208bf84f4"}, - {file = "coverage-7.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79060214983769c7ba3f0cee10b54c97609dca4d478fa1aa32b914480fd5738d"}, - {file = "coverage-7.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:356e76b46783a98c2a2fe81ec79df4883a1e62895ea952968fb253c114e7f930"}, - {file = "coverage-7.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0cef0cdec915d11254a7f549c1170afecce708d30610c6abdded1f74e581666d"}, - {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dc022073d063b25a402454e5712ef9e007113e3a676b96c5f29b2bda29352f40"}, - {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9b74db26dfea4f4e50d48a4602207cd1e78be33182bc9cbf22da94f332f99878"}, - {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ad146744ca4fd09b50c482650e3c1b1f4dfa1d4792e0a04a369c7f23336f0400"}, - {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c555b48be1853fe3997c11c4bd521cdd9a9612352de01fa4508f16ec341e6fe0"}, - {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7034b5c56a58ae5e85f23949d52c14aca2cfc6848a31764995b7de88f13a1ea0"}, - {file = "coverage-7.13.5-cp310-cp310-win32.whl", hash = "sha256:eb7fdf1ef130660e7415e0253a01a7d5a88c9c4d158bcf75cbbd922fd65a5b58"}, - {file = "coverage-7.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:3e1bb5f6c78feeb1be3475789b14a0f0a5b47d505bfc7267126ccbd50289999e"}, - {file = "coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d"}, - {file = "coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587"}, - {file = "coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642"}, - {file = "coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b"}, - {file = "coverage-7.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686"}, - {file = "coverage-7.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743"}, - {file = "coverage-7.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75"}, - {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209"}, - {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a"}, - {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e"}, - {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd"}, - {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8"}, - {file = "coverage-7.13.5-cp311-cp311-win32.whl", hash = "sha256:259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf"}, - {file = "coverage-7.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9"}, - {file = "coverage-7.13.5-cp311-cp311-win_arm64.whl", hash = "sha256:bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028"}, - {file = "coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01"}, - {file = "coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422"}, - {file = "coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f"}, - {file = "coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5"}, - {file = "coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376"}, - {file = "coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256"}, - {file = "coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c"}, - {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5"}, - {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09"}, - {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9"}, - {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf"}, - {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c"}, - {file = "coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf"}, - {file = "coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810"}, - {file = "coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de"}, - {file = "coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1"}, - {file = "coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3"}, - {file = "coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26"}, - {file = "coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3"}, - {file = "coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b"}, - {file = "coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a"}, - {file = "coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969"}, - {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161"}, - {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15"}, - {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1"}, - {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6"}, - {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17"}, - {file = "coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85"}, - {file = "coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b"}, - {file = "coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664"}, - {file = "coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d"}, - {file = "coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0"}, - {file = "coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806"}, - {file = "coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3"}, - {file = "coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9"}, - {file = "coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd"}, - {file = "coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606"}, - {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e"}, - {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0"}, - {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87"}, - {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479"}, - {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2"}, - {file = "coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a"}, - {file = "coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819"}, - {file = "coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911"}, - {file = "coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f"}, - {file = "coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e"}, - {file = "coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a"}, - {file = "coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510"}, - {file = "coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247"}, - {file = "coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6"}, - {file = "coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0"}, - {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882"}, - {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740"}, - {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16"}, - {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0"}, - {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0"}, - {file = "coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc"}, - {file = "coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633"}, - {file = "coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8"}, - {file = "coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b"}, - {file = "coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c"}, - {file = "coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9"}, - {file = "coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29"}, - {file = "coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607"}, - {file = "coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90"}, - {file = "coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3"}, - {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab"}, - {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562"}, - {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2"}, - {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea"}, - {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a"}, - {file = "coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215"}, - {file = "coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43"}, - {file = "coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45"}, - {file = "coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61"}, - {file = "coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179"}, + {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"}, ] [package.extras] @@ -544,65 +544,65 @@ toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "cryptography" -version = "47.0.0" +version = "48.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.8" +python-versions = "!=3.9.0,!=3.9.1,>=3.9" groups = ["main"] files = [ - {file = "cryptography-47.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:160ad728f128972d362e714054f6ba0067cab7fb350c5202a9ae8ae4ce3ef1a0"}, - {file = "cryptography-47.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b9a8943e359b7615db1a3ba587994618e094ff3d6fa5a390c73d079ce18b3973"}, - {file = "cryptography-47.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f5c15764f261394b22aef6b00252f5195f46f2ca300bec57149474e2538b31f8"}, - {file = "cryptography-47.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9c59ab0e0fa3a180a5a9c59f3a5abe3ef90d474bc56d7fadfbe80359491b615b"}, - {file = "cryptography-47.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:34b4358b925a5ea3e14384ca781a2c0ef7ac219b57bb9eacc4457078e2b19f92"}, - {file = "cryptography-47.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0024b87d47ae2399165a6bfb20d24888881eeab83ae2566d62467c5ff0030ce7"}, - {file = "cryptography-47.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:1e47422b5557bb82d3fff997e8d92cff4e28b9789576984f08c248d2b3535d93"}, - {file = "cryptography-47.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:6f29f36582e6151d9686235e586dd35bb67491f024767d10b842e520dc6a07ac"}, - {file = "cryptography-47.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a9b761f012a943b7de0e828843c5688d0de94a0578d44d6c85a1bae32f87791f"}, - {file = "cryptography-47.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4e1de79e047e25d6e9f8cea71c86b4a53aced64134f0f003bbcbf3655fd172c8"}, - {file = "cryptography-47.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef6b3634087f18d2155b1e8ce264e5345a753da2c5fa9815e7d41315c90f8318"}, - {file = "cryptography-47.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:11dbb9f50a0f1bb9757b3d8c27c1101780efb8f0bdecfb12439c22a74d64c001"}, - {file = "cryptography-47.0.0-cp311-abi3-win32.whl", hash = "sha256:7fda2f02c9015db3f42bb8a22324a454516ed10a8c29ca6ece6cdbb5efe2a203"}, - {file = "cryptography-47.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:f5c3296dab66202f1b18a91fa266be93d6aa0c2806ea3d67762c69f60adc71aa"}, - {file = "cryptography-47.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:be12cb6a204f77ed968bcefe68086eb061695b540a3dd05edac507a3111b25f0"}, - {file = "cryptography-47.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2ebd84adf0728c039a3be2700289378e1c164afc6748df1a5ed456767bef9ba7"}, - {file = "cryptography-47.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f68d6fbc7fbbcfb0939fea72c3b96a9f9a6edfc0e1b1d29778a2066030418b1"}, - {file = "cryptography-47.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:6651d32eff255423503aa276739da98c30f26c40cbeffcc6048e0d54ef704c0c"}, - {file = "cryptography-47.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3fb8fa48075fad7193f2e5496135c6a76ac4b2aa5a38433df0a539296b377829"}, - {file = "cryptography-47.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11438c7518132d95f354fa01a4aa2f806d172a061a7bed18cf18cbdacdb204d7"}, - {file = "cryptography-47.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8c1a736bbb3288005796c3f7ccb9453360d7fed483b13b9f468aea5171432923"}, - {file = "cryptography-47.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:f1557695e5c2b86e204f6ce9470497848634100787935ab7adc5397c54abd7ab"}, - {file = "cryptography-47.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:f9a034b642b960767fb343766ae5ba6ad653f2e890ddd82955aef288ffea8736"}, - {file = "cryptography-47.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:b1c76fca783aa7698eb21eb14f9c4aa09452248ee54a627d125025a43f83e7a7"}, - {file = "cryptography-47.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4f7722c97826770bab8ae92959a2e7b20a5e9e9bf4deae68fd86c3ca457bab52"}, - {file = "cryptography-47.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:09f6d7bf6724f8db8b32f11eccf23efc8e759924bc5603800335cf8859a3ddbd"}, - {file = "cryptography-47.0.0-cp314-cp314t-win32.whl", hash = "sha256:6eebcaf0df1d21ce1f90605c9b432dd2c4f4ab665ac29a40d5e3fc68f51b5e63"}, - {file = "cryptography-47.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:51c9313e90bd1690ec5a75ed047c27c0b8e6c570029712943d6116ef9a90620b"}, - {file = "cryptography-47.0.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:14432c8a9bcb37009784f9594a62fae211a2ae9543e96c92b2a8e4c3cd5cd0c4"}, - {file = "cryptography-47.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:07efe86201817e7d3c18781ca9770bc0db04e1e48c994be384e4602bc38f8f27"}, - {file = "cryptography-47.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b45761c6ec22b7c726d6a829558777e32d0f1c8be7c3f3480f9c912d5ee8a10"}, - {file = "cryptography-47.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:edd4da498015da5b9f26d38d3bfc2e90257bfa9cbed1f6767c282a0025ae649b"}, - {file = "cryptography-47.0.0-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9af828c0d5a65c70ec729cd7495a4bf1a67ecb66417b8f02ff125ab8a6326a74"}, - {file = "cryptography-47.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:256d07c78a04d6b276f5df935a9923275f53bd1522f214447fdf365494e2d515"}, - {file = "cryptography-47.0.0-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:5d0e362ff51041b0c0d219cc7d6924d7b8996f57ce5712bdcef71eb3c65a59cc"}, - {file = "cryptography-47.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:1581aef4219f7ca2849d0250edaa3866212fb74bf5667284f46aa92f9e65c1ca"}, - {file = "cryptography-47.0.0-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a49a3eb5341b9503fa3000a9a0db033161db90d47285291f53c2a9d2cd1b7f76"}, - {file = "cryptography-47.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2207a498b03275d0051589e326b79d4cf59985c99031b05bb292ac52631c37fe"}, - {file = "cryptography-47.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7a02675e2fabd0c0fc04c868b8781863cbf1967691543c22f5470500ff840b31"}, - {file = "cryptography-47.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80887c5cbd1774683cb126f0ab4184567f080071d5acf62205acb354b4b753b7"}, - {file = "cryptography-47.0.0-cp38-abi3-win32.whl", hash = "sha256:ed67ea4e0cfb5faa5bc7ecb6e2b8838f3807a03758eec239d6c21c8769355310"}, - {file = "cryptography-47.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:835d2d7f47cdc53b3224e90810fb1d36ca94ea29cc1801fb4c1bc43876735769"}, - {file = "cryptography-47.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f1207974a904e005f762869996cf620e9bf79ecb4622f148550bb48e0eb35a7"}, - {file = "cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:1a405c08857258c11016777e11c02bacbe7ef596faf259305d282272a3a05cbe"}, - {file = "cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:20fdbe3e38fb67c385d233c89371fa27f9909f6ebca1cecc20c13518dae65475"}, - {file = "cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:f7db373287273d8af1414cf95dc4118b13ffdc62be521997b0f2b270771fef50"}, - {file = "cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:9fe6b7c64926c765f9dff301f9c1b867febcda5768868ca084e18589113732ab"}, - {file = "cryptography-47.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cffbba3392df0fa8629bb7f43454ee2925059ee158e23c54620b9063912b86c8"}, - {file = "cryptography-47.0.0.tar.gz", hash = "sha256:9f8e55fe4e63613a5e1cc5819030f27b97742d720203a087802ce4ce9ceb52bb"}, + {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"}, ] [package.dependencies] -cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""} +cffi = {version = ">=2.0.0", markers = "platform_python_implementation != \"PyPy\""} typing-extensions = {version = ">=4.13.2", markers = "python_full_version < \"3.11.0\""} [package.extras] @@ -719,14 +719,14 @@ files = [ [[package]] name = "hypothesis" -version = "6.152.4" +version = "6.152.8" description = "The property-based testing library for Python" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "hypothesis-6.152.4-py3-none-any.whl", hash = "sha256:e730fd93c7578182efadc7f90b3c5437ee4d55edf738930eb5043c81ac1d97e8"}, - {file = "hypothesis-6.152.4.tar.gz", hash = "sha256:31c8f9ce619716f543e2710b489b1633c833586641d9e6c94cee03f109a5afc4"}, + {file = "hypothesis-6.152.8-py3-none-any.whl", hash = "sha256:61b1e6e14f0623e8afe27a4a1b1fce5a4611beefef015b987a5c7d0359babbda"}, + {file = "hypothesis-6.152.8.tar.gz", hash = "sha256:9c0dd56c6ce5649ef3289555ae9fec40663401cf7134a99f926acf1b91fb6d9f"}, ] [package.dependencies] @@ -734,12 +734,12 @@ 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.102)", "django (>=4.2)", "dpcontracts (>=0.4)", "hypothesis-crosshair (>=0.0.27)", "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.1) ; sys_platform == \"win32\" or sys_platform == \"emscripten\"", "watchdog (>=4.0.0)"] +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)"] cli = ["black (>=20.8b0)", "click (>=7.0)", "rich (>=9.0.0)"] codemods = ["libcst (>=0.3.16)"] -crosshair = ["crosshair-tool (>=0.0.102)", "hypothesis-crosshair (>=0.0.27)"] +crosshair = ["crosshair-tool (>=0.0.104)", "hypothesis-crosshair (>=0.0.28)"] dateutil = ["python-dateutil (>=1.4)"] -django = ["django (>=4.2)"] +django = ["django (>=5.2)"] dpcontracts = ["dpcontracts (>=0.4)"] ghostwriter = ["black (>=20.8b0)"] lark = ["lark (>=0.10.1)"] @@ -749,18 +749,18 @@ pytest = ["pytest (>=4.6)"] pytz = ["pytz (>=2014.1)"] redis = ["redis (>=3.0.0)"] watchdog = ["watchdog (>=4.0.0)"] -zoneinfo = ["tzdata (>=2026.1) ; sys_platform == \"win32\" or sys_platform == \"emscripten\""] +zoneinfo = ["tzdata (>=2026.2) ; sys_platform == \"win32\" or sys_platform == \"emscripten\""] [[package]] name = "idna" -version = "3.13" +version = "3.15" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.8" groups = ["main", "dev", "docs"] files = [ - {file = "idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3"}, - {file = "idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242"}, + {file = "idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8"}, + {file = "idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc"}, ] [package.extras] @@ -851,146 +851,139 @@ yamlloader = "*" [[package]] name = "lxml" -version = "6.1.0" +version = "6.1.1" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "lxml-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:41dcc4c7b10484257cbd6c37b83ddb26df2b0e5aff5ac00d095689015af868ec"}, - {file = "lxml-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a31286dbb5e74c8e9a5344465b77ab4c5bd511a253b355b5ca2fae7e579fafec"}, - {file = "lxml-6.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1bc4cc83fb7f66ffb16f74d6dd0162e144333fc36ebcce32246f80c8735b2551"}, - {file = "lxml-6.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:20cf4d0651987c906a2f5cba4e3a8d6ba4bfdf973cfe2a96c0d6053888ea2ecd"}, - {file = "lxml-6.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffb34ea45a82dd637c2c97ae1bbb920850c1e59bcae79ce1c15af531d83e7215"}, - {file = "lxml-6.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a1d9b99e5b2597e4f5aed2484fef835256fa1b68a19e4265c97628ef4bf8bcf4"}, - {file = "lxml-6.1.0-cp310-cp310-manylinux_2_28_i686.whl", hash = "sha256:d43aa26dcda363f21e79afa0668f5029ed7394b3bb8c92a6927a3d34e8b610ea"}, - {file = "lxml-6.1.0-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:6262b87f9e5c1e5fe501d6c153247289af42eb44ad7660b9b3de17baaf92d6f6"}, - {file = "lxml-6.1.0-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d1392c569c032f78a11a25d1de1c43fff13294c793b39e19d84fade3045cbbc3"}, - {file = "lxml-6.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:045e387d1f4f42a418380930fa3f45c73c9b392faf67e495e58902e68e8f44a7"}, - {file = "lxml-6.1.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:9f93d5b8b07f73e8c77e3c6556a3db269918390c804b5e5fcdd4858232cc8f16"}, - {file = "lxml-6.1.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:de550d129f18d8ab819651ffe4f38b1b713c7e116707de3c0c6400d0ef34fbc1"}, - {file = "lxml-6.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c08da09dc003c9e8c70e06b53a11db6fb3b250c21c4236b03c7d7b443c318e7a"}, - {file = "lxml-6.1.0-cp310-cp310-win32.whl", hash = "sha256:37448bf9c7d7adfc5254763901e2bbd6bb876228dfc1fc7f66e58c06368a7544"}, - {file = "lxml-6.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:2593a0a6621545b9095b71ad74ed4226eba438a7d9fc3712a99bdb15508cf93a"}, - {file = "lxml-6.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:e80807d72f96b96ad5588cb85c75616e4f2795a7737d4630784c51497beb7776"}, - {file = "lxml-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cec05be8c876f92a5aa07b01d60bbb4d11cfbdd654cad0561c0d7b5c043a61b9"}, - {file = "lxml-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9c03e048b6ce8e77b09c734e931584894ecd58d08296804ca2d0b184c933ce50"}, - {file = "lxml-6.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:942454ff253da14218f972b23dc72fa4edf6c943f37edd19cd697618b626fac5"}, - {file = "lxml-6.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d036ee7b99d5148072ac7c9b847193decdfeac633db350363f7bce4fff108f0e"}, - {file = "lxml-6.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ae5d8d5427f3cc317e7950f2da7ad276df0cfa37b8de2f5658959e618ea8512"}, - {file = "lxml-6.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:363e47283bde87051b821826e71dde47f107e08614e1aa312ba0c5711e77738c"}, - {file = "lxml-6.1.0-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:f504d861d9f2a8f94020130adac88d66de93841707a23a86244263d1e54682f5"}, - {file = "lxml-6.1.0-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:23a5dc68e08ed13331d61815c08f260f46b4a60fdd1640bbeb82cf89a9d90289"}, - {file = "lxml-6.1.0-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f15401d8d3dbf239e23c818afc10c7207f7b95f9a307e092122b6f86dd43209a"}, - {file = "lxml-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fcf3da95e93349e0647d48d4b36a12783105bcc74cb0c416952f9988410846a3"}, - {file = "lxml-6.1.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:0d082495c5fcf426e425a6e28daaba1fcb6d8f854a4ff01effb1f1f381203eb9"}, - {file = "lxml-6.1.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:e3c4f84b24a1fcba435157d111c4b755099c6ff00a3daee1ad281817de75ed11"}, - {file = "lxml-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:976a6b39b1b13e8c354ad8d3f261f3a4ac6609518af91bdb5094760a08f132c4"}, - {file = "lxml-6.1.0-cp311-cp311-win32.whl", hash = "sha256:857efde87d365706590847b916baff69c0bc9252dc5af030e378c9800c0b10e3"}, - {file = "lxml-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:183bfb45a493081943be7ea2b5adfc2b611e1cf377cefa8b8a8be404f45ef9a7"}, - {file = "lxml-6.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:19f4164243fc206d12ed3d866e80e74f5bc3627966520da1a5f97e42c32a3f39"}, - {file = "lxml-6.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d2f17a16cd8751e8eb233a7e41aecdf8e511712e00088bf9be455f604cd0d28d"}, - {file = "lxml-6.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f0cea5b1d3e6e77d71bd2b9972eb2446221a69dc52bb0b9c3c6f6e5700592d93"}, - {file = "lxml-6.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fc46da94826188ed45cb53bd8e3fc076ae22675aea2087843d4735627f867c6d"}, - {file = "lxml-6.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9147d8e386ec3b82c3b15d88927f734f565b0aaadef7def562b853adca45784a"}, - {file = "lxml-6.1.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5715e0e28736a070f3f34a7ccc09e2fdcba0e3060abbcf61a1a5718ff6d6b105"}, - {file = "lxml-6.1.0-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4937460dc5df0cdd2f06a86c285c28afda06aefa3af949f9477d3e8df430c485"}, - {file = "lxml-6.1.0-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bc783ee3147e60a25aa0445ea82b3e8aabb83b240f2b95d32cb75587ff781814"}, - {file = "lxml-6.1.0-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:40d9189f80075f2e1f88db21ef815a2b17b28adf8e50aaf5c789bfe737027f32"}, - {file = "lxml-6.1.0-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:05b9b8787e35bec69e68daf4952b2e6dfcfb0db7ecf1a06f8cdfbbac4eb71aad"}, - {file = "lxml-6.1.0-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0f08beb0182e3e9a86fae124b3c47a7b41b7b69b225e1377db983802404e54"}, - {file = "lxml-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73becf6d8c81d4c76b1014dbd3584cb26d904492dcf73ca85dc8bff08dcd6d2d"}, - {file = "lxml-6.1.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1ae225f66e5938f4fa29d37e009a3bb3b13032ac57eb4eb42afa44f6e4054e69"}, - {file = "lxml-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:690022c7fae793b0489aa68a658822cea83e0d5933781811cabbf5ea3bcfe73d"}, - {file = "lxml-6.1.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:63aeafc26aac0be8aff14af7871249e87ea1319be92090bfd632ec68e03b16a5"}, - {file = "lxml-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:264c605ab9c0e4aa1a679636f4582c4d3313700009fac3ec9c3412ed0d8f3e1d"}, - {file = "lxml-6.1.0-cp312-cp312-win32.whl", hash = "sha256:56971379bc5ee8037c5a0f09fa88f66cdb7d37c3e38af3e45cf539f41131ac1f"}, - {file = "lxml-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:bba078de0031c219e5dd06cf3e6bf8fb8e6e64a77819b358f53bb132e3e03366"}, - {file = "lxml-6.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:c3592631e652afa34999a088f98ba7dfc7d6aff0d535c410bea77a71743f3819"}, - {file = "lxml-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a0092f2b107b69601adf562a57c956fbb596e05e3e6651cabd3054113b007e45"}, - {file = "lxml-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fc7140d7a7386e6b545d41b7358f4d02b656d4053f5fa6859f92f4b9c2572c4d"}, - {file = "lxml-6.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:419c58fc92cc3a2c3fa5f78c63dbf5da70c1fa9c1b25f25727ecee89a96c7de2"}, - {file = "lxml-6.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:37fabd1452852636cf38ecdcc9dd5ca4bba7a35d6c53fa09725deeb894a87491"}, - {file = "lxml-6.1.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2853c8b2170cc6cd54a6b4d50d2c1a8a7aeca201f23804b4898525c7a152cfc"}, - {file = "lxml-6.1.0-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8e369cbd690e788c8d15e56222d91a09c6a417f49cbc543040cba0fe2e25a79e"}, - {file = "lxml-6.1.0-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e69aa6805905807186eb00e66c6d97a935c928275182eb02ee40ba00da9623b2"}, - {file = "lxml-6.1.0-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:4bd1bdb8a9e0e2dd229de19b5f8aebac80e916921b4b2c6ef8a52bc131d0c1f9"}, - {file = "lxml-6.1.0-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:cbd7b79cdcb4986ad78a2662625882747f09db5e4cd7b2ae178a88c9c51b3dfe"}, - {file = "lxml-6.1.0-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:43e4d297f11080ec9d64a4b1ad7ac02b4484c9f0e2179d9c4ef78e886e747b88"}, - {file = "lxml-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cc16682cc987a3da00aa56a3aa3075b08edb10d9b1e476938cfdbee8f3b67181"}, - {file = "lxml-6.1.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:d6d8efe71429635f0559579092bb5e60560d7b9115ee38c4adbea35632e7fa24"}, - {file = "lxml-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e39ab3a28af7784e206d8606ec0e4bcad0190f63a492bca95e94e5a4aef7f6e"}, - {file = "lxml-6.1.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9eb667bf50856c4a58145f8ca2d5e5be160191e79eb9e30855a476191b3c3495"}, - {file = "lxml-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7f4a77d6f7edf9230cee3e1f7f6764722a41604ee5681844f18db9a81ea0ec33"}, - {file = "lxml-6.1.0-cp313-cp313-win32.whl", hash = "sha256:28902146ffbe5222df411c5d19e5352490122e14447e98cd118907ee3fd6ee62"}, - {file = "lxml-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:4a1503c56e4e2b38dc76f2f2da7bae69670c0f1933e27cfa34b2fa5876410b16"}, - {file = "lxml-6.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:e0af85773850417d994d019741239b901b22c6680206f46a34766926e466141d"}, - {file = "lxml-6.1.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:ab863fd37458fed6456525f297d21239d987800c46e67da5ef04fc6b3dd93ac8"}, - {file = "lxml-6.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6fd8b1df8254ff4fd93fd31da1fc15770bde23ac045be9bb1f87425702f61cc9"}, - {file = "lxml-6.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:47024feaae386a92a146af0d2aeed65229bf6fff738e6a11dda6b0015fb8fd03"}, - {file = "lxml-6.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3f00972f84450204cd5d93a5395965e348956aaceaadec693a22ec743f8ae3eb"}, - {file = "lxml-6.1.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97faa0860e13b05b15a51fb4986421ef7a30f0b3334061c416e0981e9450ca4c"}, - {file = "lxml-6.1.0-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:972a6451204798675407beaad97b868d0c733d9a74dafefc63120b81b8c2de28"}, - {file = "lxml-6.1.0-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fe022f20bc4569ec66b63b3fb275a3d628d9d32da6326b2982584104db6d3086"}, - {file = "lxml-6.1.0-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:75c4c7c619a744f972f4451bf5adf6d0fb00992a1ffc9fd78e13b0bc817cc99f"}, - {file = "lxml-6.1.0-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:3648f20d25102a22b6061c688beb3a805099ea4beb0a01ce62975d926944d292"}, - {file = "lxml-6.1.0-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:77b9f99b17cbf14026d1e618035077060fc7195dd940d025149f3e2e830fbfcb"}, - {file = "lxml-6.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32662519149fd7a9db354175aa5e417d83485a8039b8aaa62f873ceee7ea4cad"}, - {file = "lxml-6.1.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:73d658216fc173cf2c939e90e07b941c5e12736b0bf6a99e7af95459cfe8eabb"}, - {file = "lxml-6.1.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ac4db068889f8772a4a698c5980ec302771bb545e10c4b095d4c8be26749616f"}, - {file = "lxml-6.1.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:45e9dfbd1b661eb64ba0d4dbe762bd210c42d86dd1e5bd2bdf89d634231beb43"}, - {file = "lxml-6.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:89e8d73d09ac696a5ba42ec69787913d53284f12092f651506779314f10ba585"}, - {file = "lxml-6.1.0-cp314-cp314-win32.whl", hash = "sha256:ebe33f4ec1b2de38ceb225a1749a2965855bffeef435ba93cd2d5d540783bf2f"}, - {file = "lxml-6.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:398443df51c538bd578529aa7e5f7afc6c292644174b47961f3bf87fe5741120"}, - {file = "lxml-6.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:8c8984e1d8c4b3949e419158fda14d921ff703a9ed8a47236c6eb7a2b6cb4946"}, - {file = "lxml-6.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1081dd10bc6fa437db2500e13993abf7cc30716d0a2f40e65abb935f02ec559c"}, - {file = "lxml-6.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:dabecc48db5f42ba348d1f5d5afdc54c6c4cc758e676926c7cd327045749517d"}, - {file = "lxml-6.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e3dd5fe19c9e0ac818a9c7f132a5e43c1339ec1cbbfecb1a938bd3a47875b7c9"}, - {file = "lxml-6.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9e7b0a4ca6dcc007a4cef00a761bba2dea959de4bd2df98f926b33c92ca5dfb9"}, - {file = "lxml-6.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d27bbe326c6b539c64b42638b18bc6003a8d88f76213a97ac9ed4f885efeab7"}, - {file = "lxml-6.1.0-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4e425db0c5445ef0ad56b0eec54f89b88b2d884656e536a90b2f52aecb4ca86"}, - {file = "lxml-6.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b89b098105b8599dc57adac95d1813409ac476d3c948a498775d3d0c6124bfb"}, - {file = "lxml-6.1.0-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:c4a699432846df86cc3de502ee85f445ebad748a1c6021d445f3e514d2cd4b1c"}, - {file = "lxml-6.1.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:30e7b2ed63b6c8e97cca8af048589a788ab5c9c905f36d9cf1c2bb549f450d2f"}, - {file = "lxml-6.1.0-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:022981127642fe19866d2907d76241bb07ed21749601f727d5d5dd1ce5d1b773"}, - {file = "lxml-6.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:23cad0cc86046d4222f7f418910e46b89971c5a45d3c8abfad0f64b7b05e4a9b"}, - {file = "lxml-6.1.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:21c3302068f50d1e8728c67c87ba92aa87043abee517aa2576cca1855326b405"}, - {file = "lxml-6.1.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:be10838781cb3be19251e276910cd508fe127e27c3242e50521521a0f3781690"}, - {file = "lxml-6.1.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2173a7bffe97667bbf0767f8a99e587740a8c56fdf3befac4b09cb29a80276fd"}, - {file = "lxml-6.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c6854e9cf99c84beb004eecd7d3a3868ef1109bf2b1df92d7bc11e96a36c2180"}, - {file = "lxml-6.1.0-cp314-cp314t-win32.whl", hash = "sha256:00750d63ef0031a05331b9223463b1c7c02b9004cef2346a5b2877f0f9494dd2"}, - {file = "lxml-6.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:80410c3a7e3c617af04de17caa9f9f20adaa817093293d69eae7d7d0522836f5"}, - {file = "lxml-6.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:26dd9f57ee3bd41e7d35b4c98a2ffd89ed11591649f421f0ec19f67d50ec67ac"}, - {file = "lxml-6.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b6c2f225662bc5ad416bdd06f72ca301b31b39ce4261f0e0097017fc2891b940"}, - {file = "lxml-6.1.0-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a86f06f059e22a0d574990ee2df24ede03f7f3c68c1336293eee9536c4c776cd"}, - {file = "lxml-6.1.0-cp38-cp38-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:468479e52ecf3ec23799c863336d02c05fc2f7ffd1a1424eeeb9a28d4eb69d13"}, - {file = "lxml-6.1.0-cp38-cp38-manylinux_2_28_i686.whl", hash = "sha256:a02ca8fe48815bddcfca3248efe54451abb9dbf2f7d1c5744c8aa4142d476919"}, - {file = "lxml-6.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:bb40648d96157f9081886defe13eac99253e663be969ff938a9289eff6e47b72"}, - {file = "lxml-6.1.0-cp38-cp38-win32.whl", hash = "sha256:1dd6a1c3ad4cb674f44525d9957f3e9c209bb6dd9213245195167a281fcc2bdc"}, - {file = "lxml-6.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:4e2c54d6b47361d0f1d3bc8d4e082ad87201e56ccdcca4d3b9ee3644ff595ec8"}, - {file = "lxml-6.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:920354904d1cb86577d4b3cfe2830c2dbe81d6f4449e57ada428f1609b5985f7"}, - {file = "lxml-6.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c871299c595ee004d186f61840f0bfc4941aa3f17c8ba4a565ead7e4f4f820ee"}, - {file = "lxml-6.1.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d0d799ff958655781296ec870d5e2448e75150da2b3d07f13ff5b0c2c35beefd"}, - {file = "lxml-6.1.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ba11752e346bd804ea312ec2eea2532dfa8b8d3261d81a32ef9e6ab16256280"}, - {file = "lxml-6.1.0-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26c5272c6a4bf4cf32d3f5a7890c942b0e04438691157d341616d02cca74d4bd"}, - {file = "lxml-6.1.0-cp39-cp39-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c53fa3a5a52122d590e847a57ccf955557b9634a7f99ff5a35131321b0a85317"}, - {file = "lxml-6.1.0-cp39-cp39-manylinux_2_28_i686.whl", hash = "sha256:76b958b4ea3104483c20f74866d55aa056546e15ebe83dd7aecd63698f43b755"}, - {file = "lxml-6.1.0-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:8c11b984b5ce6add4dccc7144c7be5d364d298f15b0c6a57da1991baedc750ce"}, - {file = "lxml-6.1.0-cp39-cp39-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d3829a6e6fd550a219564912d4002c537f65da4c6ae4e093cc34462f4fa027ad"}, - {file = "lxml-6.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:52b0ac6903cf74ebf997eb8c682d2fbac7d1ab7e4c552413eec55868a9b73f39"}, - {file = "lxml-6.1.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:29f5c00cb7d752bce2c70ebd2d31b0a42f9499ffdd3ecb2f31a5b73ee43031ad"}, - {file = "lxml-6.1.0-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:c748ebcb6877de89f48ab90ca96642ac458fff5dec291a2b9337cd4d0934e383"}, - {file = "lxml-6.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:08950a23f296b3f83521577274e3d3b0f3d739bf2e68d01a752e4288bc50d286"}, - {file = "lxml-6.1.0-cp39-cp39-win32.whl", hash = "sha256:11a873c77a181b4fef9c2e357d08ed399542c2af1390101da66720a19c7c9618"}, - {file = "lxml-6.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:81ff55c70b67d19d52b6fd118a114c0a4c97d799cd3089ff9bd9e2ff4b414ee2"}, - {file = "lxml-6.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:481d6e2104285d9add34f41b42b247b76b61c5b5c26c303c2e9707bbf8bd9a64"}, - {file = "lxml-6.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:546b66c0dd1bb8d9fa89d7123e5fa19a8aff3a1f2141eb22df96112afb17b842"}, - {file = "lxml-6.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5cfa1a34df366d9dc0d5eaf420f4cf2bb1e1bebe1066d1c2fc28c179f8a4004c"}, - {file = "lxml-6.1.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db88156fcf544cdbf0d95588051515cfdfd4c876fc66444eb98bceb5d6db76de"}, - {file = "lxml-6.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:07f98f5496f96bf724b1e3c933c107f0cbf2745db18c03d2e13a291c3afd2635"}, - {file = "lxml-6.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4642e04449a1e164b5ff71ffd901ddb772dfabf5c9adf1b7be5dffe1212bc037"}, - {file = "lxml-6.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:7da13bb6fbadfafb474e0226a30570a3445cfd47c86296f2446dafbd77079ace"}, - {file = "lxml-6.1.0.tar.gz", hash = "sha256:bfd57d8008c4965709a919c3e9a98f76c2c7cb319086b3d26858250620023b13"}, + {file = "lxml-6.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:09dd5b7075dc2f7709654a46543ba1ea3c2e217b2ed8fbd413a8a945a0f40f60"}, + {file = "lxml-6.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f6ac4ef4d82dff54670227a69c67782ae0b811b5cf6b17954f1e8f7502fc0d1d"}, + {file = "lxml-6.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:556e94a63c9b04716f8e4de2abb65775061f846e89331b6c5be79183a24f98ea"}, + {file = "lxml-6.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6bf403fbb3b3e348a561a5f4f0b9961835657981c802a1df03653eef8a9074"}, + {file = "lxml-6.1.1-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1dde6131244bba38a17c745836ba190bc753fd73c9291666287fd0a3fa3dcf30"}, + {file = "lxml-6.1.1-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98fc784c2c1440667aeedf8465bdfe10208acf0ead656a2c68627299f546b315"}, + {file = "lxml-6.1.1-cp310-cp310-manylinux_2_28_i686.whl", hash = "sha256:add8cf6ddf9a65116119a28ece0f7886e30af27ba724a7594305f1d1b58a92a1"}, + {file = "lxml-6.1.1-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:cf9d57306d848218f3601fee7601fab1a327c942d56e2e97610583cb4dd74206"}, + {file = "lxml-6.1.1-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88136950da4d13c318bde414ce10219931937851327f44328f2df4d2c4614067"}, + {file = "lxml-6.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cecdd5dfdc87b1fd87dbf81d4b037a544f47f4c744200a67013771682d67686a"}, + {file = "lxml-6.1.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cd312b9692e831d2ffcad61eab31d91d4b4655a962e61de8fb410472cbcd37aa"}, + {file = "lxml-6.1.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:5b7328b46d49fc9477d91ae8f6d55340347d827b7734ba3ea33faae0efef1383"}, + {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-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"}, + {file = "lxml-6.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db1d75f6617a49c1c01bc7023713e0ff59ab32c9579ae62a7674c0e34f3b0b0a"}, + {file = "lxml-6.1.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a12689be69a28ddaa0ab99a5a1137da2afd5f8f16df7b5680b66f616d3eda1d"}, + {file = "lxml-6.1.1-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b73c339ae29b90fd2d06e58ebd555a751bde9cd6bbd36cc0281b9a2c94e9d8"}, + {file = "lxml-6.1.1-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:752d3bbfe874715ccd0aec7f88d7fc623c0f1fd7aa7b3238a084e017bad2a009"}, + {file = "lxml-6.1.1-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:6b1761fbf9ec984e2e9d9c589ef5f5fd684b7c19f92aadd567a26c5224958db6"}, + {file = "lxml-6.1.1-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d680fbcb768404c601ecb43519ecd8461f6954cb11c06a78962f666832ccfca8"}, + {file = "lxml-6.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:162af1091cd785f2f27e62d3547ae9bc58ec5c86dd314d67021fd02463708d83"}, + {file = "lxml-6.1.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e9308ff8241c532df3f3e570f9a5aeed6c853f888512ba4b75638d7c11c95ef6"}, + {file = "lxml-6.1.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5f6994074ebae6ffb04447268e37dc16edc304f9859cf91acb86e0af6c1b395c"}, + {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-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"}, + {file = "lxml-6.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:54a7f95e4de5fb94e2f9f4b9055c6ba33bf3d628fd77a1d647c5923caa2cdcdc"}, + {file = "lxml-6.1.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f2ec43df44b1f76249ee0a615334f9b5b060e1c8bd90e706dad2d14d02f383"}, + {file = "lxml-6.1.1-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:70ef8a7e102a1508f8121aae5b0867abd663f72c14f0a9c937e6554cb4587b7b"}, + {file = "lxml-6.1.1-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ebe6af670449830d6d9b752c256a983291c766a1365ba5d5460048f9e33a7818"}, + {file = "lxml-6.1.1-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:27acc820660aaffa4f7c087f29120e12980f7779d56d8492d263170111284740"}, + {file = "lxml-6.1.1-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:1db753c9115ec7100d073b744d17e25e88a8f90f5c39b2f5dd878149af59671f"}, + {file = "lxml-6.1.1-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4f469aebd783bb741c2ecb2a681008fd26bfe5c16a9a72ed5467f834e810df2"}, + {file = "lxml-6.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:766b010012d59470072c1816b5b6c69f1d243e5db36ea5968e94accf430a4635"}, + {file = "lxml-6.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b8d812c6011c08b8111a15e54dd990b8923692d80adf35488bee34026c35accf"}, + {file = "lxml-6.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:fe0306bd29505a9177aac19f1877174b0e7422c222a59f70b2cd41633448c3dc"}, + {file = "lxml-6.1.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5ba186ad207446c65d3bb3d3e0412b032b1d9f595e59861e2354798c5703d955"}, + {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-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"}, + {file = "lxml-6.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:762ff394d5bd56da0cf034a23dcce4e13923f15321a2adfa2ac00201dc6d3fca"}, + {file = "lxml-6.1.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a088f287f7d8275a33c07f2cac6c50b9319309a0200a39e7e75d80c707723099"}, + {file = "lxml-6.1.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e902da4b04e6b52e5893900d4b8ab46068f75f3561f01bf1080957f9fd932ed6"}, + {file = "lxml-6.1.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d4962d4c66bf830a7e59ed6cfc17d148149898a3aefa8ec6e59763e6e3ed085"}, + {file = "lxml-6.1.1-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:581d4c8ae690a6609e64862dd6b7c2489635c2d13907fc2b20f2bc200ff1d21e"}, + {file = "lxml-6.1.1-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:876e1ff5930ed8bf295ec5ef9a8155e9b6b1876bbf1deed8b3a8069311875a8f"}, + {file = "lxml-6.1.1-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9eb9b5a968f6e0f6d640092a567e14529ff8cea2e29d00da6f78a79fa49f013c"}, + {file = "lxml-6.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:aa49e06d94aba782c6a02eecb7e507969e7e7a41b267f1b359bb35585f295d5b"}, + {file = "lxml-6.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:70cdfd80589d59e43e18005dd7244e8895e93db8ab6a620b7e23df5445a4e3d2"}, + {file = "lxml-6.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:aad9aa39483ed8ec44d6d2e59e5b98a0d80676ef0d92f44bfc374836111f62f5"}, + {file = "lxml-6.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d49514be2f28d895c38cf9d2b72d7b9a07d00314519f456c0b50b53cfcf4c785"}, + {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-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"}, + {file = "lxml-6.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efe0374196335f93b53269acd811b944f2e6bdc88e8894f214bd636455484909"}, + {file = "lxml-6.1.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac931cdc9442c1763b8a8f6cd62c0c938737eafc5be75eff88df55fc73bc0d00"}, + {file = "lxml-6.1.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:aee395f5d0927f947758b4ec119fd5fc8ec71f07a1c5c52077b30b04c0fa6955"}, + {file = "lxml-6.1.1-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9395002973c827b3ed67db77e6ec09f092919a587022174554096a269378fb13"}, + {file = "lxml-6.1.1-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:73bc2086f141224ebddb7fc5c6a36ca58b31b94b561e1dfe8e073e3270fad1e7"}, + {file = "lxml-6.1.1-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:3779def59032b81e44a5f70096ef6bf2082f8d901937dca354474ba09782e245"}, + {file = "lxml-6.1.1-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:86c89b9d55ebf820ad7c90bc533410f0d098054f293351f10603c0c46ff598f5"}, + {file = "lxml-6.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19607c6bbff2a44cf3fe8250abccd20942d3462473e0a721d01d379ed017e462"}, + {file = "lxml-6.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c6ed5141a5c7507cf3ee76bd363b0d6f801e3321adc35b5d825a23115faa5465"}, + {file = "lxml-6.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:62aeb7e85b5d60320b9d77eef2e773994e2c0ce10121b277e0a19804e1654a5a"}, + {file = "lxml-6.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b1b963fd8f5caa68e99dfae060d54de1fe9cba899b8718b44a00cdca53c3e590"}, + {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-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"}, + {file = "lxml-6.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4f0dd2f01f9f8a89f565d000e03abcf0a13d692a346c8d22f628d49af098777a"}, + {file = "lxml-6.1.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b7e8a14c8634bf6f7a568634cb395305a6d964aeb5b7ee32248094bed3a7e2c"}, + {file = "lxml-6.1.1-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:86281fbdd6a8162756f8d603f37e3435bfa38043adb79c6dc6a2dfee065e7525"}, + {file = "lxml-6.1.1-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5d7152ec39ca7c402d8fb9bad86140a15b9503bd0c54484e3f1bbe3dd37ceca"}, + {file = "lxml-6.1.1-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:88d8cb75b9d82858497a5393e3c63cfbf03035225e4b35a49ed7ccb151e4dc0e"}, + {file = "lxml-6.1.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f64ec5397ea6a41fc1b4af0380d79b44a755b5531dcaccd9940fb260dca93038"}, + {file = "lxml-6.1.1-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d34bbf07dbc7ca5970671b1512e928991fb5e9d95365636c9b2d8b4f53af405e"}, + {file = "lxml-6.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:17e0e18d4ad8adbd0399291bc44845b69d9dd68439a3cdebdf35ff902ec05072"}, + {file = "lxml-6.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:3ab541146f1f6968c462d6c2ac495148e8cdba2f8347700b2141b6ec5a75bf52"}, + {file = "lxml-6.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2a0217714657e023ef4293500f65aa20fce6164c8fd6b08fa5bd4a859fb14b9b"}, + {file = "lxml-6.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:05a82eb6e1530a64f26225b55cbd178113bd0b5af1c2b625f25e5296742c26d2"}, + {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-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"}, + {file = "lxml-6.1.1-cp38-cp38-manylinux_2_28_i686.whl", hash = "sha256:76447f65250ed2501ead1a1552f5ce8edff159a86f308348e6a9c4acb5e1f1b4"}, + {file = "lxml-6.1.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ffecec8eb889b58ba9be5b95fb1cc78e22ea8eedea38e8736a1568fe1979250e"}, + {file = "lxml-6.1.1-cp38-cp38-win32.whl", hash = "sha256:c674693f055fa2495de12292cb45e9944199d8eaef5a2dec45175c7c61cb73e3"}, + {file = "lxml-6.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:55b03549819867ea141c0202242c4816c82e52ec36e7e648db9d8da5a3dc3ed6"}, + {file = "lxml-6.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c9f79d5325907f13e1be0b3e4dacc1049d1dffc4aeee3c995284bea5fe0fab7d"}, + {file = "lxml-6.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:83b6b30eb131da7a75b601f28c5d6971e6ed3e887919bf6b6a1ad3c2df289080"}, + {file = "lxml-6.1.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:441dd227fa0690eb9fc81edabc63cdcefc212bba99b906dcf6e32cc1a9d3e533"}, + {file = "lxml-6.1.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e07c65f443c887bbcf31cc1771d932ecc192a5273943589b3c7572b749f1ffb2"}, + {file = "lxml-6.1.1-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5bec7d03d78d853597d6107854c2310ce3f761fd218fe9fe91d5101fcf6c2efe"}, + {file = "lxml-6.1.1-cp39-cp39-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f76acfb5f68ba982635a53fd985a8044be98a35b43232c2a1ee235ffab3e1dd"}, + {file = "lxml-6.1.1-cp39-cp39-manylinux_2_28_i686.whl", hash = "sha256:8d43ca737b20e106e4aebc42b2f3ae19f00ba63d7eb731698ee083d72d15646f"}, + {file = "lxml-6.1.1-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:32ab449a5486f6c758e849bb86710d0e45edc24a04e250c01555f8f5653958f8"}, + {file = "lxml-6.1.1-cp39-cp39-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53c909b62a0532183542fed00c5a7218258c56292d409bc789886fe1cb04c438"}, + {file = "lxml-6.1.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:640f97d43d867bcb9c75b3af013b64850756b746cb6bce8ace83b70da3abba9d"}, + {file = "lxml-6.1.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:469e3618338bd7ab5beb412d2439825479fcf0dab99e394ca563dbc4eaf6c834"}, + {file = "lxml-6.1.1-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:aae97dfdb60715c164419ac2532a76d013c3918a665eb6cb7288098b5f349aaf"}, + {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-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"}, + {file = "lxml-6.1.1-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6f0ce10945fab9c4c06ce14e22af9059d1a87493a9af4501a5b0b9187e21cf2"}, + {file = "lxml-6.1.1-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f8844cd288697c6425c9beba919302241e3278871dc6519515e72b04e987abcf"}, + {file = "lxml-6.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:ed21202aec73cda4d55d1ce57b389aadb90ffb044e6cd1080b8347efe1b1ec84"}, + {file = "lxml-6.1.1.tar.gz", hash = "sha256:ba96ae44888e0185281e937633a743ea90d5a196c6000f82565ebb0580012d40"}, ] [package.extras] @@ -1034,14 +1027,14 @@ tabulate = ">=0.9.0,<0.10.0" [[package]] name = "markdown-it-py" -version = "4.0.0" +version = "4.2.0" description = "Python port of markdown-it. Markdown parsing, done right!" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"}, - {file = "markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"}, + {file = "markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a"}, + {file = "markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49"}, ] [package.dependencies] @@ -1054,7 +1047,7 @@ linkify = ["linkify-it-py (>=1,<3)"] plugins = ["mdit-py-plugins (>=0.5.0)"] profiling = ["gprof2dot"] rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "requests"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "pytest-timeout", "requests"] [[package]] name = "markdown-version-annotations" @@ -1492,19 +1485,19 @@ nicer-shell = ["ipython"] [[package]] name = "netmiko" -version = "4.6.0" +version = "4.7.0" description = "Multi-vendor library to simplify legacy CLI connections to network devices" optional = false -python-versions = "<4.0,>=3.9" +python-versions = "<4.0,>=3.10" groups = ["main"] files = [ - {file = "netmiko-4.6.0-py3-none-any.whl", hash = "sha256:0c9b7309005d2c8a010b275f3494628cadb1658a8841632131c848074b7cdadb"}, - {file = "netmiko-4.6.0.tar.gz", hash = "sha256:9701bb2c1a15eb2e8074cb2e28ca007c69b9fa52961b83b98c757ead6b80deef"}, + {file = "netmiko-4.7.0-py3-none-any.whl", hash = "sha256:406684e45b3822e17efa0f8e376644995693ff40a750c297ccce0c58e873f29d"}, + {file = "netmiko-4.7.0.tar.gz", hash = "sha256:94cf7bfe5daed1d058444ce1637e10177df22f903683a53d1fbee47553488c65"}, ] [package.dependencies] ntc-templates = ">=3.1.0" -paramiko = ">=2.9.5" +paramiko = ">=3.5.0,<5.0" pyserial = ">=3.3" pyyaml = ">=6.0.2" rich = ">=13.8" @@ -1699,14 +1692,14 @@ testutils = ["gitpython (>3)"] [[package]] name = "pymdown-extensions" -version = "10.21.2" +version = "10.21.3" description = "Extension pack for Python Markdown." optional = false python-versions = ">=3.9" groups = ["docs"] files = [ - {file = "pymdown_extensions-10.21.2-py3-none-any.whl", hash = "sha256:5c0fd2a2bea14eb39af8ff284f1066d898ab2187d81b889b75d46d4348c01638"}, - {file = "pymdown_extensions-10.21.2.tar.gz", hash = "sha256:c3f55a5b8a1d0edf6699e35dcbea71d978d34ff3fa79f3d807b8a5b3fa90fbdc"}, + {file = "pymdown_extensions-10.21.3-py3-none-any.whl", hash = "sha256:d7a5d08014fc571e80ca21dd6f854e31f94c489800350564d55d15b3c41e76b6"}, + {file = "pymdown_extensions-10.21.3.tar.gz", hash = "sha256:72cfcf55f07aea0d4af2c4f11dd4e52466ddfb1bb819673146398e0bd3a77354"}, ] [package.dependencies] @@ -1927,14 +1920,14 @@ pyyaml = "*" [[package]] name = "requests" -version = "2.33.1" +version = "2.34.2" description = "Python HTTP for Humans." optional = false python-versions = ">=3.10" groups = ["main", "dev", "docs"] files = [ - {file = "requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a"}, - {file = "requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517"}, + {file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"}, + {file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"}, ] [package.dependencies] @@ -2004,30 +1997,30 @@ oldlibyaml = ["ruamel.yaml.clib ; platform_python_implementation == \"CPython\"" [[package]] name = "ruff" -version = "0.15.12" +version = "0.15.13" 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.12-py3-none-linux_armv6l.whl", hash = "sha256:f86f176e188e94d6bdbc09f09bfd9dc729059ad93d0e7390b5a73efe19f8861c"}, - {file = "ruff-0.15.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e3bcd123364c3770b8e1b7baaf343cc99a35f197c5c6e8af79015c666c423a6c"}, - {file = "ruff-0.15.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fe87510d000220aa1ed530d4448a7c696a0cae1213e5ec30e5874287b66557b5"}, - {file = "ruff-0.15.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84a1630093121375a3e2a95b4a6dc7b59e2b4ee76216e32d81aae550a832d002"}, - {file = "ruff-0.15.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fb129f40f114f089ebe0ca56c0d251cf2061b17651d464bb6478dc01e69f11f5"}, - {file = "ruff-0.15.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0c862b172d695db7598426b8af465e7e9ac00a3ea2a3630ee67eb82e366aaa6"}, - {file = "ruff-0.15.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2849ea9f3484c3aca43a82f484210370319e7170df4dfe4843395ddf6c57bc33"}, - {file = "ruff-0.15.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e77c7e51c07fe396826d5969a5b846d9cd4c402535835fb6e21ce8b28fef847"}, - {file = "ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b2f4f2f3b1026b5fb449b467d9264bf22067b600f7b6f41fc5958909f449d0"}, - {file = "ruff-0.15.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9ba3b8f1afd7e2e43d8943e55f249e13f9682fde09711644a6e7290eb4f3e339"}, - {file = "ruff-0.15.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e852ba9fdc890655e1d78f2df1499efbe0e54126bd405362154a75e2bde159c5"}, - {file = "ruff-0.15.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dd8aed930da53780d22fc70bdf84452c843cf64f8cb4eb38984319c24c5cd5fd"}, - {file = "ruff-0.15.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:01da3988d225628b709493d7dc67c3b9b12c0210016b08690ef9bd27970b262b"}, - {file = "ruff-0.15.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9cae0f92bd5700d1213188b31cd3bdd2b315361296d10b96b8e2337d3d11f53e"}, - {file = "ruff-0.15.12-py3-none-win32.whl", hash = "sha256:d0185894e038d7043ba8fd6aee7499ece6462dc0ea9f1e260c7451807c714c20"}, - {file = "ruff-0.15.12-py3-none-win_amd64.whl", hash = "sha256:c87a162d61ab3adca47c03f7f717c68672edec7d1b5499e652331780fe74950d"}, - {file = "ruff-0.15.12-py3-none-win_arm64.whl", hash = "sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f"}, - {file = "ruff-0.15.12.tar.gz", hash = "sha256:ecea26adb26b4232c0c2ca19ccbc0083a68344180bba2a600605538ce51a40a6"}, + {file = "ruff-0.15.13-py3-none-linux_armv6l.whl", hash = "sha256:444b580fc72fd6887e650acd3e575e18cdc79dbcf42fb4030b491057921f61f8"}, + {file = "ruff-0.15.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6590d009e7cb7ebf36f83dbdd44a3fa48a0994ff6f1cdc1b08006abe58f98dc7"}, + {file = "ruff-0.15.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1c26d2f66163deeb6e08d8b39fbbe983ce3c71cea06a6d7591cfd1421793c629"}, + {file = "ruff-0.15.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbd6f94b434f896308e4d57fb7bfde0d02b99f7a64b3bdab0fdfa6a864203a5"}, + {file = "ruff-0.15.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3259f3be4d181bda591da5db2571aed6853c6a048157756448020bc6c5cd22"}, + {file = "ruff-0.15.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae9c17e5eb4430c154e76abc25d79a318190f5a997f38fb6b114416c5319ffc9"}, + {file = "ruff-0.15.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e2e39bff6c341f4b577a21b801326fab0b11847f48fcaa83f00a113c9b3cb55"}, + {file = "ruff-0.15.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e8d9a8e08013542e94d3220bc5b62cc3e5ef87c5f74bff367d3fac14fab013e6"}, + {file = "ruff-0.15.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc411dfebe5eebe55ce041c6ae080eb7668955e866daa2fbb16692a784f1c4ca"}, + {file = "ruff-0.15.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:768494eb08b9cee54e2fd27969966f74db5a57f6eaa7a90fcb3306af34dfc4bd"}, + {file = "ruff-0.15.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fb75f9a3a7e42ffe117d734494e6c5e5cb3565d66e12612cb63d0e572a41a5b6"}, + {file = "ruff-0.15.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8cb74dd33bb2f6613faf7fc03b660053b5ac4f80e706d5788c6335e2a8048d51"}, + {file = "ruff-0.15.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7ef823f817fcd191dc934e984be9cf4094f808effa16f2542ad8e821ba02bbf2"}, + {file = "ruff-0.15.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f345a13937bd7f09f6f5d19fa0721b0c103e00e7f62bc67089a8e5e037719e0b"}, + {file = "ruff-0.15.13-py3-none-win32.whl", hash = "sha256:4044f94208b3b05ba0fc4a4abd0558cf4d6459bd18325eead7fd8cc66f909b41"}, + {file = "ruff-0.15.13-py3-none-win_amd64.whl", hash = "sha256:7064884d442b7d477b4e7473d12da7f08851d2b1982763c5d3f388a19468a1a4"}, + {file = "ruff-0.15.13-py3-none-win_arm64.whl", hash = "sha256:2471da9bd1068c8c064b5fd9c0c4b6dddffd6369cb1cd68b29993b1709ff1b21"}, + {file = "ruff-0.15.13.tar.gz", hash = "sha256:f9d89f17f7ba7fb2ed42921f0df75da797a9a5d71bc39049e2c687cf2baf44b7"}, ] [[package]] @@ -2201,14 +2194,14 @@ files = [ [[package]] name = "tomlkit" -version = "0.14.0" +version = "0.15.0" description = "Style preserving TOML library" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680"}, - {file = "tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064"}, + {file = "tomlkit-0.15.0-py3-none-any.whl", hash = "sha256:4dbc8f0fc024412b57ced8757ac7461305126a648ff8c2c807fcb8e133a78738"}, + {file = "tomlkit-0.15.0.tar.gz", hash = "sha256:7d1a9ecba3086638211b13814ea79c90dd54dd11993564376f3aa92271f5c7a3"}, ] [[package]] @@ -2265,14 +2258,14 @@ files = [ [[package]] name = "urllib3" -version = "2.6.3" +version = "2.7.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main", "dev", "docs"] files = [ - {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, - {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, + {file = "urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"}, + {file = "urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"}, ] [package.extras] diff --git a/pyntc/devices/f5_device.py b/pyntc/devices/f5_device.py index 758c3246..12bfa7f7 100644 --- a/pyntc/devices/f5_device.py +++ b/pyntc/devices/f5_device.py @@ -7,7 +7,13 @@ import warnings import requests -from f5.bigip import ManagementRoot + +try: + from f5.bigip import ManagementRoot + + HAS_F5_BIGIP = True +except ModuleNotFoundError: + HAS_F5_BIGIP = False from pyntc import log from pyntc.devices.base_device import BaseDevice @@ -30,6 +36,10 @@ def __init__(self, host, username, password, **kwargs): # noqa: D403 password (str): The password to authenticate with the device. kwargs (dict): Additional keyword arguments. """ + # Re-import f5.bigip so that the error raises if running Python >3.11 + if not HAS_F5_BIGIP: + from f5.bigip import ManagementRoot as _ManagementRoot # pylint: disable=import-outside-toplevel # noqa: F401, I001 + super().__init__(host, username, password, device_type="f5_tmos_icontrol") self.api_handler = ManagementRoot(self.host, self.username, self.password) diff --git a/pyntc/devices/nxos_device.py b/pyntc/devices/nxos_device.py index 8a453beb..ac949aa6 100644 --- a/pyntc/devices/nxos_device.py +++ b/pyntc/devices/nxos_device.py @@ -5,6 +5,7 @@ import time from netmiko import ConnectHandler +from netmiko.exceptions import NetmikoBaseException, NetmikoTimeoutException from requests.exceptions import ConnectTimeout, ReadTimeout from pyntc import log @@ -66,23 +67,60 @@ def __init__(self, host, username, password, transport="http", timeout=30, port= log.init(host=host) def _image_booted(self, image_name, **vendor_specifics): - version_data = self.show("show version", raw_text=True) + version_data = self.show_netmiko("show version", raw_text=True) return bool(re.search(image_name, version_data)) def _wait_for_device_reboot(self, timeout=3600): + """Block until the device reboots and accepts a fresh SSH session. + + Records the pre-reboot uptime, drops the existing SSH session, and polls + for the device to come back. The reboot is considered complete when a new + SSH connection succeeds and reports an uptime lower than the original. + + The pre-reboot SSH session must be discarded — once the device restarts the + socket is dead but reads against it will hang or return stale buffered + bytes, so each iteration opens a brand-new connection. + """ + self._uptime = None + original_uptime = self.uptime start = time.time() + + # Drop the pre-reboot SSH session so subsequent probes can't read from + # a half-closed socket. + try: + self.close() + except Exception as close_exc: # pylint: disable=broad-except + log.debug("Host %s: Pre-reboot disconnect raised %s (ignored).", self.host, close_exc) + self.native_ssh = None + self._connected = False + while time.time() - start < timeout: - try: # NXOS stays online, when it installs OS - self.refresh() - if self.uptime < 180: - log.info("Host %s: Device rebooted.", self.host) + try: + self.open() + self._uptime = None + current_uptime = self.uptime + if current_uptime < original_uptime: + log.info( + "Host %s: Device rebooted (uptime %ss < pre-reboot %ss).", + self.host, + current_uptime, + original_uptime, + ) return - except: # noqa E722 # nosec # pylint: disable=bare-except - log.debug("Host %s: Pausing for 10 sec before retrying.", self.host) - time.sleep(10) + log.debug( + "Host %s: SSH reachable but uptime %ss >= pre-reboot %ss; still waiting.", + self.host, + current_uptime, + original_uptime, + ) + except Exception as exc: # pylint: disable=broad-except + log.debug("Host %s: Reboot probe failed (%s); will retry.", self.host, exc) + self.native_ssh = None + self._connected = False + time.sleep(10) log.error("Host %s: Device timed out while rebooting.", self.host) - raise RebootTimeoutError(hostname=self.hostname, wait_time=timeout) + raise RebootTimeoutError(hostname=self.host, wait_time=timeout) def refresh(self): """Refresh caches on device instance.""" @@ -158,7 +196,23 @@ def uptime(self): (int): Uptime of the device in seconds. """ if self._uptime is None: - self._uptime = self.native.facts.get("uptime") + self._uptime = 0 + try: + parsed_uptime = self.show_netmiko("show version")[0]["uptime"] + for interval in parsed_uptime.split(","): + duration, unit = interval.strip().split(" ") + if "day" in unit.lower(): + self._uptime += int(duration) * 24 * 60 * 60 + elif "hour" in unit.lower(): + self._uptime += int(duration) * 60 * 60 + elif "minute" in unit.lower(): + self._uptime += int(duration) * 60 + elif "second" in unit.lower(): + self._uptime += int(duration) + else: + raise CommandError(command="show version", message=f"Unknown time unit in uptime: {unit}") + except (IndexError, KeyError, ValueError) as e: + raise CommandError(command="show version", message="Failed to parse 'show version' command.") from e log.debug("Host %s: Uptime %s", self.host, self._uptime) return self._uptime @@ -183,7 +237,7 @@ def hostname(self): (str): Hostname of the device. """ if self._hostname is None: - self._hostname = self.native.facts.get("hostname") + self._hostname = self.show_netmiko("show hostname")[0]["hostname"] log.debug("Host %s: Hostname %s", self.host, self._hostname) return self._hostname @@ -248,7 +302,7 @@ def os_version(self): (str): Device version. """ if self._os_version is None: - self._os_version = self.native.facts.get("os_version") + self._os_version = self.show_netmiko("show version")[0]["os"] log.debug("Host %s: OS version %s", self.host, self._os_version) return self._os_version @@ -328,7 +382,8 @@ def _get_file_system(self): Raises: FileSystemNotFoundError: When the module is unable to determine the default file system. """ - raw_data = self.show("dir", raw_text=True) + raw_data = self.native_ssh.send_command("dir", read_timeout=30) + try: file_system = re.search(r"bootflash:", raw_data).group(0) except AttributeError: @@ -343,7 +398,7 @@ def _get_free_space(self, file_system=None): if file_system is None: file_system = self._get_file_system() - raw_data = self.show(f"dir {file_system}", raw_text=True) + raw_data = self.native_ssh.send_command(f"dir {file_system}", read_timeout=30) # Example NXOS dir output: 47171194880 bytes free match = re.search(r"(\d+)\s+bytes\s+free", raw_data) if match is None: @@ -368,7 +423,7 @@ def _build_url_copy_command_simple(self, src, file_system, dest): """Build copy command for simple URL-based transfers (TFTP, HTTP, HTTPS without credentials).""" netloc = self._netloc(src) path = self._source_path(src, dest) - return f"copy {src.scheme}://{netloc}{path} {file_system}", False + return f"copy {src.scheme}://{netloc}{path} {file_system}" def _build_url_copy_command_with_creds(self, src, file_system, dest): """Build copy command for URL-based transfers with credentials (HTTP/HTTPS/SCP/FTP/SFTP).""" @@ -400,7 +455,6 @@ def check_file_exists(self, filename, file_system=None): """ exists = False - self.open() file_system = file_system or self._get_file_system() command = f"dir {file_system}/{filename}" result = self.native_ssh.send_command(command, read_timeout=30) @@ -448,7 +502,6 @@ def get_remote_checksum(self, filename, hashing_algorithm="md5", **kwargs): f"Supported algorithms: {sorted(NXOS_SUPPORTED_HASHING_ALGORITHMS)}" ) - self.open() file_system = kwargs.get("file_system") if file_system is None: file_system = self._get_file_system() @@ -457,9 +510,11 @@ def get_remote_checksum(self, filename, hashing_algorithm="md5", **kwargs): if not file_system.startswith("/") and not file_system.endswith(":"): file_system = f"{file_system}:" - # Use NXOS verify command to get the checksum - # Example: show file bootflash:nautobot.png sha512sum - command = f"show file {file_system}/{filename} {hashing_algorithm}sum" + # Use NXOS verify command to get the checksum. The file_system already + # ends with ":" (e.g. "bootflash:"), so concatenate directly — NXOS rejects + # "bootflash:/name" as a syntax error. + # Example: show file bootflash:nautobot.png md5sum + command = f"show file {file_system}{filename} {hashing_algorithm}sum" try: result = self.native_ssh.send_command(command, read_timeout=30) @@ -470,14 +525,19 @@ def get_remote_checksum(self, filename, hashing_algorithm="md5", **kwargs): command, result, ) - print(f"result: {result}") - remote_checksum = result - return remote_checksum - except Exception as e: log.error("Host %s: Error getting remote checksum: %s", self.host, str(e)) raise CommandError(command, f"Error getting remote checksum: {str(e)}") + # NXOS sometimes returns just the digest, sometimes prefixes/suffixes it + # with the filename or other context. Extract the first hex run long + # enough to be a real digest (md5=32, sha256=64, sha512=128). + match = re.search(r"\b([a-fA-F0-9]{32,128})\b", result) + if not match: + log.error("Host %s: Could not parse checksum from '%s': %s", self.host, command, result) + raise CommandError(command, f"Could not parse checksum from device output: {result}") + return match.group(1) + def remote_file_copy(self, src: FileCopyModel, dest=None, file_system=None, **kwargs): # noqa: R0912 pylint: disable=too-many-branches """Copy a file from remote source to device. Skips if file already exists and is verified on remote device. @@ -537,6 +597,7 @@ def remote_file_copy(self, src: FileCopyModel, dest=None, file_system=None, **kw r"Source username": src.username or "", r"yes/no|Are you sure you want to continue connecting": "yes", r"(confirm|Address or name of remote host|Source filename|Destination filename)": "", + r"Enter vrf.*:": src.vrf or "", } keys = list(prompt_answers.keys()) + [current_prompt] expect_regex = f"({'|'.join(keys)})" @@ -632,7 +693,7 @@ def install_os(self, image_name, reboot=True, **vendor_specifics): Returns: (bool): True if new image is boot option on device. Otherwise, false. """ - self.native.show("terminal dont-ask") + self.show_netmiko("terminal dont-ask", raw_text=True) timeout = vendor_specifics.get("timeout", 3600) if not self._image_booted(image_name): log.info("Host %s: Setting Image %s in boot options.", self.host, image_name) @@ -674,7 +735,7 @@ def redundancy_state(self): """ if self._redundancy_state is None: try: - output = self.native.show("show redundancy state", raw_text=True) + output = self.show_netmiko("show redundancy state", raw_text=True) # Parse the redundancy state from output # Example output: "Redundancy state = active" match = re.search(r"Redundancy\s+state\s*=\s*(\w+)", output, re.IGNORECASE) @@ -749,7 +810,7 @@ def reboot(self, wait_for_reload=False, **kwargs): log.warning("Passing 'confirm' to reboot method is deprecated.") raise DeprecationWarning("Passing 'confirm' to reboot method is deprecated.") try: - self.native.show_list(["terminal dont-ask", "reload"]) + self.show_netmiko(["terminal dont-ask", "reload"], raw_text=True) # The native reboot is not always properly disabling confirmation. Above is more consistent. # self.native.reboot(confirm=True) except ReadTimeout as expected_exception: @@ -796,7 +857,8 @@ def save(self, filename="startup-config"): (bool): True if configuration is saved. """ log.debug("Host %s: Copy running config with name %s.", self.host, filename) - return self.native.save(filename=filename) + self.show_netmiko(f"copy running-config {filename}", raw_text=True) + return True def set_boot_options(self, image_name, kickstart=None, reboot=True, **vendor_specifics): """Set boot variables. @@ -812,15 +874,14 @@ def set_boot_options(self, image_name, kickstart=None, reboot=True, **vendor_spe """ file_system = vendor_specifics.get("file_system") if file_system is None: - file_system = "bootflash:" + file_system = self._get_file_system() - file_system_files = self.show(f"dir {file_system}", raw_text=True) - if re.search(image_name, file_system_files) is None: + if not self.check_file_exists(image_name, file_system=file_system): log.error("Host %s: File not found error for image %s.", self.host, image_name) raise NTCFileNotFoundError(hostname=self.hostname, file=image_name, directory=file_system) if kickstart is not None: - if re.search(kickstart, file_system_files) is None: + if not self.check_file_exists(kickstart, file_system=file_system): log.error("Host %s: File not found error for image %s.", self.host, image_name) raise NTCFileNotFoundError(hostname=self.hostname, file=kickstart, directory=file_system) @@ -828,7 +889,20 @@ def set_boot_options(self, image_name, kickstart=None, reboot=True, **vendor_spe image_name = file_system + image_name try: - self.native.set_boot_options(image_name, kickstart=kickstart, reboot=reboot) + self.show_netmiko("terminal dont-ask", raw_text=True) + if reboot: + reboot_arg = "" + else: + reboot_arg = " no-reload" + try: + if kickstart is None: + self.show_netmiko(f"install all nxos {image_name}{reboot_arg}", raw_text=True) + else: + self.show_netmiko( + f"install all system {image_name} kickstart {kickstart}{reboot_arg}", raw_text=True + ) + except (NetmikoBaseException, NetmikoTimeoutException): + pass except (ReadTimeout, ConnectTimeout): pass log.info("Host %s: boot options have been set to %s", self.host, image_name) @@ -843,32 +917,60 @@ def set_timeout(self, timeout): self.native.timeout = timeout def show(self, command, raw_text=False): - """Send a non-configuration command. + """Send a non-configuration command using netmiko. + + Args: + command (str, list): The command (or list of commands) to send to the device. + raw_text (bool, optional): When True return raw text; when False parse with TextFSM + into a list of dicts. Defaults to False. + + Raises: + CommandError: A single command failed on the device. + CommandListError: A command within a list failed on the device. + + Returns: + (str | list): Raw text or TextFSM-parsed result; a list when ``command`` is a list, else a string. + """ + try: + return self.show_netmiko(command, raw_text=raw_text) + except CLIError as e: + if isinstance(command, list): + log.error("Host %s: Command error for command %s with message %s.", self.host, e.command, str(e)) + raise CommandListError(command, e.command, str(e)) + log.error("Host %s: Command error %s.", self.host, str(e)) + raise CommandError(command, str(e)) + + def show_netmiko(self, command, raw_text=False, read_timeout=None): + """Send a non-configuration command using netmiko. Args: command (str): The command to send to the device. raw_text (bool, optional): Whether to return raw text or structured data. Defaults to False. + read_timeout (int, optional): Timeout to pass to Netmiko read_timeout. Defaults to the Netmiko timeout if not set. Raises: CommandError: Error message stating which command failed. Returns: - (str): Results of the command ran. + (str | list): Raw text or TextFSM-parsed result; a list when ``command`` is a list, else a string. """ - log.debug("Host %s: Successfully executed command 'show' with responses.", self.host) + if read_timeout is None: + read_timeout = self.native_ssh.timeout if isinstance(command, list): - try: - log.debug("Host %s: Successfully executed command 'show' with commands %s.", self.host, command) - return self.native.show_list(command, raw_text=raw_text) - except CLIError as e: - log.error("Host %s: Command error for command %s with message %s.", self.host, e.command, str(e)) - raise CommandListError(command, e.command, str(e)) + results = [] + for inner in command: + results.append(self.show_netmiko(inner, raw_text=raw_text, read_timeout=read_timeout)) + return results try: - log.debug("Host %s: Successfully executed command 'show'.", self.host) - return self.native.show(command, raw_text=raw_text) - except CLIError as e: - log.error("Host %s: Command error %s.", self.host, str(e)) - raise CommandError(command, str(e)) + result = self.native_ssh.send_command(command, use_textfsm=not raw_text, read_timeout=read_timeout) + log.debug("Host %s: Successfully executed command '%s'.", self.host, command) + return result + except NetmikoTimeoutException as e: + log.error("Host %s: Command timed out %s.", self.host, str(e)) + raise CommandError(command=command, message="Command timed out") from e + except NetmikoBaseException as e: + log.error("Host %s: Command failed %s.", self.host, str(e)) + raise CommandError(command=command, message="Error retrieving command output") from e @property def startup_config(self): @@ -877,4 +979,4 @@ def startup_config(self): Returns: (str): Startup configuration. """ - return self.show("show startup-config", raw_text=True) + return self.show_netmiko("show startup-config", raw_text=True) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 1a17977e..6e4fb233 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -22,7 +22,7 @@ "test_asa_device": "sha512", "test_jnpr_device": "sha256", "test_ios_device": "md5", - "test_nxos_device": "sha256", + "test_nxos_device": "md5", } # Maps each hashing algorithm to the suffix convention used on the diff --git a/tests/integration/test_nxos_device.py b/tests/integration/test_nxos_device.py new file mode 100644 index 00000000..f4ed7868 --- /dev/null +++ b/tests/integration/test_nxos_device.py @@ -0,0 +1,290 @@ +"""Integration tests for NXOSDevice.remote_file_copy. + +These tests connect to an actual Cisco NXOS device in the lab and are run manually. +They are NOT part of the CI unit test suite. + +Usage (from project root): + export NXOS_HOST= + export NXOS_USER= + export NXOS_PASS= + export FTP_URL=ftp://:@/ + export TFTP_URL=tftp:/// + export SCP_URL=scp://:@/ + export HTTP_URL=http://:@:8081/ + export HTTPS_URL=https://:@:8443/ + export SFTP_URL=sftp://:@/ + export FILE_CHECKSUM_MD5= + export FILE_SIZE= + export FILE_SIZE_UNIT=megabytes # optional; defaults to "bytes" + # export NXOS_VRF=management # optional; applied to every copy test + poetry run pytest tests/integration/test_nxos_device.py -v + +Set only the protocol URL vars for the servers you have available; each +protocol test will skip automatically if its URL is not set. ``conftest.py`` +maps this module to md5 (older NXOS releases only support md5/cksum) and +copies ``FILE_CHECKSUM_MD5`` into ``FILE_CHECKSUM`` automatically. + +Environment variables: + NXOS_HOST - IP address or hostname of the lab NXOS device + NXOS_USER - SSH username + NXOS_PASS - SSH password + NXOS_VRF - Optional VRF name; when set, every copy test routes through this VRF + (needed when the file servers are only reachable via the management VRF) + FTP_URL - FTP URL of the file to transfer + TFTP_URL - TFTP URL of the file to transfer + SCP_URL - SCP URL of the file to transfer + HTTP_URL - HTTP URL of the file to transfer + HTTPS_URL - HTTPS URL of the file to transfer + SFTP_URL - SFTP URL of the file to transfer + FILE_NAME - Destination filename on the device (default: basename of URL path) + FILE_CHECKSUM_MD5 - Expected md5 checksum of the file (shared across all protocols) + FILE_SIZE - Expected size of the file expressed in FILE_SIZE_UNIT units; used for + the pre-transfer free-space check + FILE_SIZE_UNIT - One of "bytes", "megabytes", or "gigabytes" (default: "bytes") +""" + +import os +from unittest import mock + +import pytest + +from pyntc.devices import NXOSDevice +from pyntc.errors import NotEnoughFreeSpaceError +from pyntc.utils.models import FILE_SIZE_UNITS, FileCopyModel + +from ._helpers import PROTOCOL_URL_VARS, build_file_copy_model, first_available_url + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture(scope="module") +def device(): + """Connect to the lab NXOS device. Skips all tests if credentials are not set.""" + host = os.environ.get("NXOS_HOST") + user = os.environ.get("NXOS_USER") + password = os.environ.get("NXOS_PASS") + + if not all([host, user, password]): + pytest.skip("NXOS_HOST / NXOS_USER / NXOS_PASS environment variables not set") + + dev = NXOSDevice(host, user, password) + yield dev + dev.close() + + +def _build_nxos_file_copy_model(env_var): + """Wrap ``build_file_copy_model`` to stamp ``NXOS_VRF`` onto the model. + + NXOS file servers are typically only reachable via the management VRF, so + the device's ``copy`` command needs ``vrf `` appended. The shared + helper has no concept of VRF; this driver-local wrapper bridges that gap + without leaking NXOS specifics into the shared helper. + """ + model = build_file_copy_model(env_var) + vrf = os.environ.get("NXOS_VRF") + if vrf: + model.vrf = vrf + return model + + +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- + + +def test_device_connects(device): + """Verify the device is reachable and responds to show commands.""" + assert device.hostname + assert device.os_version + + +def test_check_file_exists_false(device, any_file_copy_model): + """Before the copy, the file should not exist (or this test is a no-op if it does).""" + result = device.check_file_exists(any_file_copy_model.file_name) + assert isinstance(result, bool) + + +def test_get_remote_checksum_after_exists(device, any_file_copy_model): + """If the file already exists, verify get_remote_checksum returns a non-empty string.""" + if not device.check_file_exists(any_file_copy_model.file_name): + pytest.skip("File does not exist on device; run test_remote_file_copy_* first") + checksum = device.get_remote_checksum( + any_file_copy_model.file_name, hashing_algorithm=any_file_copy_model.hashing_algorithm + ) + assert checksum and len(checksum) > 0 + + +def test_remote_file_copy_ftp(device): + """Transfer the file using FTP and verify it exists on the device.""" + model = _build_nxos_file_copy_model("FTP_URL") + device.remote_file_copy(model) + assert device.check_file_exists(model.file_name) + + +def test_remote_file_copy_tftp(device): + """Transfer the file using TFTP and verify it exists on the device.""" + model = _build_nxos_file_copy_model("TFTP_URL") + device.remote_file_copy(model) + assert device.check_file_exists(model.file_name) + + +def test_remote_file_copy_scp(device): + """Transfer the file using SCP and verify it exists on the device.""" + model = _build_nxos_file_copy_model("SCP_URL") + device.remote_file_copy(model) + assert device.check_file_exists(model.file_name) + + +def test_remote_file_copy_http(device): + """Transfer the file using HTTP and verify it exists on the device.""" + model = _build_nxos_file_copy_model("HTTP_URL") + device.remote_file_copy(model) + assert device.check_file_exists(model.file_name) + + +def test_remote_file_copy_https(device): + """Transfer the file using HTTPS and verify it exists on the device.""" + model = _build_nxos_file_copy_model("HTTPS_URL") + device.remote_file_copy(model) + assert device.check_file_exists(model.file_name) + + +def test_remote_file_copy_sftp(device): + """Transfer the file using SFTP and verify it exists on the device.""" + model = _build_nxos_file_copy_model("SFTP_URL") + device.remote_file_copy(model) + assert device.check_file_exists(model.file_name) + + +def test_verify_file_after_copy(device, any_file_copy_model): + """After a successful copy the file should verify cleanly.""" + if not device.check_file_exists(any_file_copy_model.file_name): + pytest.skip("File does not exist on device; run a copy test first") + assert device.verify_file( + any_file_copy_model.checksum, + any_file_copy_model.file_name, + hashing_algorithm=any_file_copy_model.hashing_algorithm, + ) + + +# --------------------------------------------------------------------------- +# Free-space / pre-transfer tests +# --------------------------------------------------------------------------- + + +def test_get_free_space_returns_positive_int(device): + """``_get_free_space`` parses the ``dir`` trailer into a positive int.""" + free = device._get_free_space() # pylint: disable=protected-access + assert isinstance(free, int) + assert free > 0 + + +def test_check_free_space_succeeds_for_small_request(device): + """A 1-byte request must always fit; ``_check_free_space`` returns ``None``.""" + # pylint: disable=protected-access + assert device._check_free_space(required_bytes=1) is None + + +def test_check_free_space_raises_when_required_exceeds_free(device): + """When required bytes exceed what the device reports, raise NotEnoughFreeSpaceError.""" + # pylint: disable=protected-access + free = device._get_free_space() + with pytest.raises(NotEnoughFreeSpaceError): + device._check_free_space(required_bytes=free + 1) + + +def test_file_size_unit_conversion_matches_device_free_space(device): + """A megabyte-denominated request converts through ``FILE_SIZE_UNITS`` correctly.""" + # pylint: disable=protected-access + free_bytes = device._get_free_space() + one_mb = FILE_SIZE_UNITS["megabytes"] + if free_bytes < one_mb: + pytest.skip("Device has less than 1 MB free; conversion sanity test not meaningful") + # 1 MB should always fit when free space is at least that large. + assert device._check_free_space(required_bytes=one_mb) is None + + +def test_remote_file_copy_rejects_oversized_transfer(device): + """remote_file_copy raises NotEnoughFreeSpaceError and never copies the file.""" + checksum = os.environ.get("FILE_CHECKSUM") + scheme, url = first_available_url() + if not (url and checksum): + pytest.skip("No protocol URL / FILE_CHECKSUM environment variables not set") + + # pylint: disable=protected-access + free_bytes = device._get_free_space() + free_gb = free_bytes // FILE_SIZE_UNITS["gigabytes"] + # Ask for ten times the currently-free capacity (minimum 10 GB), expressed in + # gigabytes so this also exercises the unit conversion end-to-end. + oversized_gb = max(free_gb * 10, 10) + + unique_name = f"pyntc_integration_space_check_{os.getpid()}_{scheme}.bin" + model = FileCopyModel( + download_url=url, + checksum=checksum, + file_name=unique_name, + file_size=oversized_gb, + file_size_unit="gigabytes", + hashing_algorithm="md5", + vrf=os.environ.get("NXOS_VRF"), + timeout=60, + ) + + assert not device.check_file_exists(unique_name), "Unique filename unexpectedly exists before test" + + with pytest.raises(NotEnoughFreeSpaceError): + device.remote_file_copy(model) + + # The transfer must never have started — file should still be absent. + assert not device.check_file_exists(unique_name) + + +def test_remote_file_copy_accepts_declared_size_within_free_space(device): + """A correctly-sized FileCopyModel copies without the space check interfering.""" + scheme, _url = first_available_url() + if scheme is None: + pytest.skip("No protocol URL environment variables set") + model = _build_nxos_file_copy_model(PROTOCOL_URL_VARS[scheme]) + # pylint: disable=protected-access + free_bytes = device._get_free_space() + assert model.file_size_bytes <= free_bytes, ( + "Configured FILE_SIZE/FILE_SIZE_UNIT exceeds device free space; update env vars" + ) + device.remote_file_copy(model) + assert device.check_file_exists(model.file_name) + + +def test_remote_file_copy_skips_space_check_when_file_size_omitted(device): + """When FileCopyModel has no file_size, _check_free_space is never called. + + Spies on ``NXOSDevice._check_free_space`` for the duration of the + transfer and asserts it was not invoked. The transfer itself uses the + same canonical ``FILE_NAME`` that the other copy tests use. The file + already existing from a prior test run is fine — the assertion that + matters is ``spy.assert_not_called()`` combined with the transfer + completing without raising ``FileTransferError``. + """ + checksum = os.environ.get("FILE_CHECKSUM") + file_name = os.environ.get("FILE_NAME") + _, url = first_available_url() + if not (url and checksum and file_name): + pytest.skip("URL / FILE_CHECKSUM / FILE_NAME environment variables not set") + + model = FileCopyModel( + download_url=url, + checksum=checksum, + file_name=file_name, + hashing_algorithm="md5", + vrf=os.environ.get("NXOS_VRF"), + timeout=60, + ) # file_size intentionally omitted + assert model.file_size is None + assert model.file_size_bytes is None + + with mock.patch.object(NXOSDevice, "_check_free_space") as spy: + device.remote_file_copy(model) + + spy.assert_not_called() + assert device.check_file_exists(model.file_name) diff --git a/tests/unit/test_devices/device_mocks/nxos/__init__.py b/tests/unit/test_devices/device_mocks/nxos/__init__.py index 85e01e88..26dbd8b0 100644 --- a/tests/unit/test_devices/device_mocks/nxos/__init__.py +++ b/tests/unit/test_devices/device_mocks/nxos/__init__.py @@ -3,7 +3,7 @@ from pyntc.devices.pynxos.errors import CLIError -CURRNENT_DIR = os.path.dirname(os.path.realpath(__file__)) +CURRENT_DIR = os.path.dirname(os.path.realpath(__file__)) def show(command, raw_text=False): @@ -11,9 +11,9 @@ def show(command, raw_text=False): command = command.replace("/", "_") if raw_text: - path = os.path.join(CURRNENT_DIR, "show_raw", command) + path = os.path.join(CURRENT_DIR, "show_raw", command) else: - path = os.path.join(CURRNENT_DIR, "show", command) + path = os.path.join(CURRENT_DIR, "show", command) if not os.path.isfile(path): raise CLIError(command, "Invalid command.") @@ -27,6 +27,30 @@ def show(command, raw_text=False): return json.loads(response) +def netmiko_send_command(command, use_textfsm=False, read_timeout=1): + if isinstance(command, list): + return [netmiko_send_command(c, use_textfsm=use_textfsm, read_timeout=read_timeout) for c in command] + + command = command.replace(" ", "_") + command = command.replace("/", "_") + + if use_textfsm: + path = os.path.join(CURRENT_DIR, "show_netmiko", command) + else: + path = os.path.join(CURRENT_DIR, "show_raw", command) + + if not os.path.isfile(path): + raise CLIError(command, "Invalid command.") + + with open(path, "r") as f: + response = f.read() + + if not use_textfsm: + return response + else: + return json.loads(response) + + def show_list(commands, raw_text=False): responses = [] for command in commands: diff --git a/tests/unit/test_devices/device_mocks/nxos/show_netmiko/dir b/tests/unit/test_devices/device_mocks/nxos/show_netmiko/dir new file mode 100644 index 00000000..2309f35f --- /dev/null +++ b/tests/unit/test_devices/device_mocks/nxos/show_netmiko/dir @@ -0,0 +1,9 @@ +[ + {"size": "4096", "date_time": "May 01 18:24:24 2026", "item_name": ".rpmstore/", "total_size": "51904524288", "total_free": "48916582400", "total_used": "2987941888", "file_system": "bootflash"}, + {"size": "4096", "date_time": "Feb 10 09:06:54 2017", "item_name": ".snapshots/", "total_size": "51904524288", "total_free": "48916582400", "total_used": "2987941888", "file_system": "bootflash"}, + {"size": "4096", "date_time": "Jan 19 02:13:25 2017", "item_name": ".swtam/", "total_size": "51904524288", "total_free": "48916582400", "total_used": "2987941888", "file_system": "bootflash"}, + {"size": "757450240", "date_time": "Aug 11 13:04:13 2025", "item_name": "flash:", "total_size": "51904524288", "total_free": "48916582400", "total_used": "2987941888", "file_system": "bootflash"}, + {"size": "536306688", "date_time": "Nov 04 13:43:29 2016", "item_name": "nxos.7.0.3.I2.2d.bin", "total_size": "51904524288", "total_free": "48916582400", "total_used": "2987941888", "file_system": "bootflash"}, + {"size": "757307904", "date_time": "Mar 10 13:57:55 2025", "item_name": "nxos.7.0.3.I5.2.bin", "total_size": "51904524288", "total_free": "48916582400", "total_used": "2987941888", "file_system": "bootflash"}, + {"size": "568", "date_time": "Mar 20 10:04:03 2017", "item_name": "vlan.dat", "total_size": "51904524288", "total_free": "48916582400", "total_used": "2987941888", "file_system": "bootflash"} +] diff --git a/tests/unit/test_devices/device_mocks/nxos/show_netmiko/reload b/tests/unit/test_devices/device_mocks/nxos/show_netmiko/reload new file mode 100644 index 00000000..93d51406 --- /dev/null +++ b/tests/unit/test_devices/device_mocks/nxos/show_netmiko/reload @@ -0,0 +1 @@ +[{}] diff --git a/tests/unit/test_devices/device_mocks/nxos/show_netmiko/show_cdp_neighbors b/tests/unit/test_devices/device_mocks/nxos/show_netmiko/show_cdp_neighbors new file mode 100644 index 00000000..edb87b6d --- /dev/null +++ b/tests/unit/test_devices/device_mocks/nxos/show_netmiko/show_cdp_neighbors @@ -0,0 +1,18 @@ +[ + { + "neighbor_name": "PERIMETER", + "local_interface": "mgmt0", + "holdtime": "129", + "capabilities": "R S I", + "platform": "WS-C3750-48TS", + "neighbor_interface": "Fas1/0/32" + }, + { + "neighbor_name": "nyc-leaf-01.networktocode.com", + "local_interface": "Eth1/1", + "holdtime": "179", + "capabilities": "R S I s", + "platform": "N9K-C9372TX", + "neighbor_interface": "Eth1/1" + } +] diff --git a/tests/unit/test_devices/device_mocks/nxos/show_netmiko/show_clock b/tests/unit/test_devices/device_mocks/nxos/show_netmiko/show_clock new file mode 100644 index 00000000..e6b4deea --- /dev/null +++ b/tests/unit/test_devices/device_mocks/nxos/show_netmiko/show_clock @@ -0,0 +1,10 @@ +[ + { + "time": "23:25:04.677", + "timezone": "UTC", + "dayweek": "Wed", + "month": "May", + "day": "20", + "year": "2026" + } +] diff --git a/tests/unit/test_devices/device_mocks/nxos/show_netmiko/show_hostname b/tests/unit/test_devices/device_mocks/nxos/show_netmiko/show_hostname new file mode 100644 index 00000000..86a21a1d --- /dev/null +++ b/tests/unit/test_devices/device_mocks/nxos/show_netmiko/show_hostname @@ -0,0 +1,5 @@ +[ + { + "hostname": "n9k1.cisconxapi.com" + } +] diff --git a/tests/unit/test_devices/device_mocks/nxos/show_netmiko/terminal_dont-ask b/tests/unit/test_devices/device_mocks/nxos/show_netmiko/terminal_dont-ask new file mode 100644 index 00000000..93d51406 --- /dev/null +++ b/tests/unit/test_devices/device_mocks/nxos/show_netmiko/terminal_dont-ask @@ -0,0 +1 @@ +[{}] diff --git a/tests/unit/test_devices/device_mocks/nxos/show_raw/dir b/tests/unit/test_devices/device_mocks/nxos/show_raw/dir new file mode 100644 index 00000000..c2dcd5cd --- /dev/null +++ b/tests/unit/test_devices/device_mocks/nxos/show_raw/dir @@ -0,0 +1,11 @@ + 4096 May 01 18:24:24 2026 .rpmstore/ + 4096 Feb 10 09:06:54 2017 .snapshots/ + 4096 Jan 19 02:13:25 2017 .swtam/ + 757450240 Aug 11 13:04:13 2025 flash: + 536306688 Nov 04 13:43:29 2016 nxos.7.0.3.I2.2d.bin + 757307904 Mar 10 13:57:55 2025 nxos.7.0.3.I5.2.bin + +Usage for bootflash:// + 3250384896 bytes used +48654139392 bytes free +51904524288 bytes total diff --git a/tests/unit/test_devices/device_mocks/nxos/show_raw/terminal_dont-ask b/tests/unit/test_devices/device_mocks/nxos/show_raw/terminal_dont-ask new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/test_devices/test_nxos_device.py b/tests/unit/test_devices/test_nxos_device.py index 0fb30aa1..a8e9e8b4 100644 --- a/tests/unit/test_devices/test_nxos_device.py +++ b/tests/unit/test_devices/test_nxos_device.py @@ -1,3 +1,4 @@ +import itertools import unittest import mock @@ -12,10 +13,11 @@ FileTransferError, NotEnoughFreeSpaceError, NTCFileNotFoundError, + RebootTimeoutError, ) from pyntc.utils.models import FileCopyModel -from .device_mocks.nxos import show, show_list +from .device_mocks.nxos import netmiko_send_command, show, show_list BOOT_IMAGE = "n9000-dk9.9.2.1.bin" KICKSTART_IMAGE = "n9000-kickstart.9.2.1.bin" @@ -31,6 +33,19 @@ "interfaces": ["mgmt0", "Ethernet1/1", "Ethernet1/2", "Ethernet1/3"], "fqdn": "N/A", } +NXOS_DIR_CMD = """ + 4096 May 01 18:24:24 2026 .rpmstore/ + 4096 Feb 10 09:06:54 2017 .snapshots/ + 4096 Jan 19 02:13:25 2017 .swtam/ + 757450240 Aug 11 13:04:13 2025 flash: + 536306688 Nov 04 13:43:29 2016 nxos.7.0.3.I2.2d.bin + 757307904 Mar 10 13:57:55 2025 nxos.7.0.3.I5.2.bin + +Usage for bootflash:// + 3250384896 bytes used +48654139392 bytes free +51904524288 bytes total +""" class TestNXOSDevice(unittest.TestCase): @@ -39,6 +54,7 @@ class TestNXOSDevice(unittest.TestCase): @mock.patch("pyntc.devices.pynxos.device.Device.facts", new_callable=mock.PropertyMock) def setUp(self, mock_facts, mock_device, mock_connect_handler): self.mock_native_ssh = mock_connect_handler.return_value + self.mock_native_ssh.send_command.side_effect = netmiko_send_command self.device = NXOSDevice("host", "user", "pass") mock_device.show.side_effect = show mock_device.show_list.side_effect = show_list @@ -81,10 +97,9 @@ def test_show(self): command = "show cdp neighbors" result = self.device.show(command) - self.assertIsInstance(result, dict) - self.assertIsInstance(result.get("neigh_count"), int) - - self.device.native.show.assert_called_with(command, raw_text=False) + # TextFSM-parsed output is a list of per-neighbor dicts. + self.assertIsInstance(result, list) + self.assertIn("neighbor_name", result[0]) def test_bad_show(self): command = "show microsoft" @@ -97,30 +112,58 @@ def test_show_raw_text(self): self.assertIsInstance(result, str) self.assertEqual(result, "n9k1.cisconxapi.com") - self.device.native.show.assert_called_with(command, raw_text=True) def test_show_list(self): commands = ["show hostname", "show clock"] result = self.device.show(commands) self.assertIsInstance(result, list) - - self.assertIn("hostname", result[0]) - self.assertIn("simple_time", result[1]) - - self.device.native.show_list.assert_called_with(commands, raw_text=False) + # Each element is itself a TextFSM-parsed list of dicts. + self.assertIn("hostname", result[0][0]) + self.assertIn("time", result[1][0]) def test_bad_show_list(self): commands = ["show badcommand", "show clock"] with self.assertRaisesRegex(CommandListError, "show badcommand"): self.device.show(commands) + @mock.patch("pyntc.devices.nxos_device.time.sleep", return_value=None) + @mock.patch.object(NXOSDevice, "open") + @mock.patch.object(NXOSDevice, "close") + def test_wait_for_device_reboot_returns_when_uptime_drops(self, mock_close, mock_open, mock_sleep): + # First read establishes the pre-reboot uptime; second read (after open()) returns + # a lower value, signalling the reboot completed. + with mock.patch.object(NXOSDevice, "uptime", new_callable=mock.PropertyMock, side_effect=[100, 5]): + self.device._wait_for_device_reboot(timeout=60) + + # Pre-reboot session dropped, then reopened before re-reading uptime. + mock_close.assert_called_once() + mock_open.assert_called() + self.assertIsNone(self.device.native_ssh) + + @mock.patch( + "pyntc.devices.nxos_device.time.time", + side_effect=itertools.chain([0], itertools.repeat(999)), + ) + @mock.patch("pyntc.devices.nxos_device.time.sleep", return_value=None) + @mock.patch.object(NXOSDevice, "open", side_effect=Exception("connection refused")) + @mock.patch.object(NXOSDevice, "close") + def test_wait_for_device_reboot_raises_on_timeout(self, mock_close, mock_open, mock_sleep, mock_time): + with mock.patch.object(NXOSDevice, "uptime", new_callable=mock.PropertyMock, return_value=100): + with self.assertRaises(RebootTimeoutError): + self.device._wait_for_device_reboot(timeout=1) + def test_save(self): + self.device.native_ssh.send_command.side_effect = None + self.device.native_ssh.send_command.return_value = ( + "[########################################] 100%\nCopy complete." + ) result = self.device.save() - self.device.native.save.return_value = True self.assertTrue(result) - self.device.native.save.assert_called_with(filename="startup-config") + self.device.native_ssh.send_command.assert_called_with( + "copy running-config startup-config", use_textfsm=False, read_timeout=self.device.native_ssh.timeout + ) def test_file_copy_remote_exists(self): self.device.native.file_copy_remote_exists.return_value = True @@ -178,9 +221,13 @@ def test_file_copy_raises_not_enough_free_space(self, mock_fcre, mock_getsize, m self.device.file_copy("source_file") def test_reboot(self): + self.device.native_ssh.send_command.side_effect = None self.device.reboot() - self.device.native.show_list.assert_called_with(["terminal dont-ask", "reload"]) - # self.device.native.reboot.assert_called_with(confirm=True) + calls = [ + mock.call("terminal dont-ask", use_textfsm=False, read_timeout=mock.ANY), + mock.call("reload", use_textfsm=False, read_timeout=mock.ANY), + ] + self.device.native_ssh.send_command.assert_has_calls(calls) def test_boot_options(self): expected = {"sys": "my_sys", "boot": "my_boot"} @@ -189,31 +236,72 @@ def test_boot_options(self): self.assertEqual(boot_options, expected) def test_set_boot_options(self): + self.device.native_ssh.send_command.side_effect = [ + NXOS_DIR_CMD, # _get_file_system + f"12345 bootflash:/{BOOT_IMAGE}", # check_file_exists for image + "", # terminal dont-ask + "", # install all nxos + ] self.device.set_boot_options(BOOT_IMAGE) - self.device.native.set_boot_options.assert_called_with( - f"{FILE_SYSTEM}{BOOT_IMAGE}", kickstart=None, reboot=True + self.device.native_ssh.send_command.assert_called_with( + f"install all nxos {FILE_SYSTEM}{BOOT_IMAGE}", use_textfsm=False, read_timeout=mock.ANY + ) + + def test_set_boot_options_no_reboot(self): + self.device.native_ssh.send_command.side_effect = [ + NXOS_DIR_CMD, # _get_file_system + f"12345 bootflash:/{BOOT_IMAGE}", # check_file_exists for image + "", # terminal dont-ask + "", # install all nxos + ] + self.device.set_boot_options(BOOT_IMAGE, reboot=False) + self.device.native_ssh.send_command.assert_called_with( + f"install all nxos {FILE_SYSTEM}{BOOT_IMAGE} no-reload", use_textfsm=False, read_timeout=mock.ANY ) def test_set_boot_options_dir(self): + self.device.native_ssh.send_command.side_effect = [ + f"12345 bootflash:/{BOOT_IMAGE}", # check_file_exists for image + "", # terminal dont-ask + "", # install all nxos + ] self.device.set_boot_options(BOOT_IMAGE, file_system=FILE_SYSTEM) - self.device.native.set_boot_options.assert_called_with( - f"{FILE_SYSTEM}{BOOT_IMAGE}", kickstart=None, reboot=True + self.device.native_ssh.send_command.assert_called_with( + f"install all nxos {FILE_SYSTEM}{BOOT_IMAGE}", use_textfsm=False, read_timeout=mock.ANY ) def test_set_boot_options_kickstart(self): + self.device.native_ssh.send_command.side_effect = [ + NXOS_DIR_CMD, # _get_file_system + f"12345 bootflash:/{BOOT_IMAGE}", # check_file_exists for image + f"12345 bootflash:/{KICKSTART_IMAGE}", # check_file_exists for kickstart + "", # terminal dont-ask + "", # install all system + ] self.device.set_boot_options(BOOT_IMAGE, kickstart=KICKSTART_IMAGE) - self.device.native.set_boot_options.assert_called_with( - f"{FILE_SYSTEM}{BOOT_IMAGE}", kickstart=f"{FILE_SYSTEM}{KICKSTART_IMAGE}", reboot=True + self.device.native_ssh.send_command.assert_called_with( + f"install all system {FILE_SYSTEM}{BOOT_IMAGE} kickstart {FILE_SYSTEM}{KICKSTART_IMAGE}", + use_textfsm=False, + read_timeout=mock.ANY, ) - @mock.patch.object(NXOSDevice, "show", return_value=FILE_SYSTEM) - def test_set_boot_options_no_file(self, mock_show): + def test_set_boot_options_no_file(self): + self.device._hostname = "n9k1" + self.device.native_ssh.send_command.side_effect = [ + NXOS_DIR_CMD, # _get_file_system + "No such file or directory", # check_file_exists - file not found + ] with self.assertRaises(NTCFileNotFoundError) as no_file: self.device.set_boot_options(BOOT_IMAGE) self.assertIn(f"{BOOT_IMAGE} was not found in {FILE_SYSTEM}", no_file.exception.message) - @mock.patch.object(NXOSDevice, "show", return_value=f"{FILE_SYSTEM}\n{BOOT_IMAGE}") - def test_set_boot_options_no_kickstart(self, mock_show): + def test_set_boot_options_no_kickstart(self): + self.device._hostname = "n9k1" + self.device.native_ssh.send_command.side_effect = [ + NXOS_DIR_CMD, # _get_file_system + f"12345 bootflash:/{BOOT_IMAGE}", # check_file_exists for image + "No such file or directory", # check_file_exists - kickstart not found + ] with self.assertRaises(NTCFileNotFoundError) as no_file: self.device.set_boot_options(BOOT_IMAGE, kickstart=KICKSTART_IMAGE) self.assertIn(f"{KICKSTART_IMAGE} was not found in {FILE_SYSTEM}", no_file.exception.message) @@ -239,14 +327,20 @@ def test_checkpiont(self): self.device.native.checkpoint.assert_called_with("good_checkpoint") def test_uptime(self): + self.device.native_ssh.send_command.side_effect = None + self.device.native_ssh.send_command.return_value = [ + {"uptime": "13 day(s), 1 hour(s), 8 minute(s), 6 second(s)"} + ] uptime = self.device.uptime - assert uptime == 1127286 + self.assertEqual(uptime, (13 * 24 * 60 * 60) + (1 * 60 * 60) + (8 * 60) + 6) def test_vendor(self): vendor = self.device.vendor assert vendor == "cisco" def test_os_version(self): + self.device.native_ssh.send_command.side_effect = None + self.device.native_ssh.send_command.return_value = [{"os": "7.0(3)I2(1)"}] os_version = self.device.os_version assert os_version == "7.0(3)I2(1)" @@ -256,7 +350,7 @@ def test_interfaces(self): def test_hostname(self): hostname = self.device.hostname - assert hostname == "n9k1" + assert hostname == "n9k1.cisconxapi.com" def test_fqdn(self): fqdn = self.device.fqdn @@ -281,79 +375,92 @@ def test_starting_config(self): self.assertEqual(self.device.startup_config, expected) def test_refresh(self): + self.device.native_ssh.send_command.side_effect = None self.assertTrue(hasattr(self.device.native, "_facts")) self.device.refresh() - self.assertIsNone(self.device._uptime) + self.assertIsNone(self.device._interfaces) self.assertFalse(hasattr(self.device.native, "_facts")) - @mock.patch.object(NXOSDevice, "show", return_value="bootflash:") - def test_get_file_system(self, mock_show): + def test_get_file_system(self): + self.device.native_ssh.send_command.side_effect = None + self.device.native_ssh.send_command.return_value = NXOS_DIR_CMD self.assertEqual(self.device._get_file_system(), "bootflash:") - mock_show.assert_called_with("dir", raw_text=True) + self.device.native_ssh.send_command.assert_called_with("dir", read_timeout=30) - @mock.patch.object(NXOSDevice, "show", return_value="no filesystems here") - def test_get_file_system_not_found(self, mock_show): + def test_get_file_system_not_found(self): + self.device.native_ssh.send_command.side_effect = None + self.device.native_ssh.send_command.return_value = "no filesystems here" with self.assertRaises(FileSystemNotFoundError): self.device._get_file_system() - mock_show.assert_called_with("dir", raw_text=True) + self.device.native_ssh.send_command.assert_called_with("dir", read_timeout=30) - @mock.patch.object(NXOSDevice, "show") - def test_get_free_space(self, mock_show): + def test_get_free_space(self): """Test _get_free_space parses NXOS dir output correctly.""" # NXOS dir output format with free space at the end - mock_show.return_value = """Directory of bootflash:/ -4096 Mar 03 22:47:15 2026 .rpmstore/ -4733329408 bytes used -47171194880 bytes free -51904524288 bytes total - -""" + self.device.native_ssh.send_command.side_effect = None + self.device.native_ssh.send_command.return_value = NXOS_DIR_CMD result = self.device._get_free_space() - self.assertEqual(result, 47171194880) - mock_show.assert_called_with("dir bootflash:", raw_text=True) + self.assertEqual(result, 48654139392) + # Should call _get_file_system (which uses SSH) and then dir command via SSH + ssh_calls = self.device.native_ssh.send_command.call_args_list + self.assertTrue(any("dir" in str(call) for call in ssh_calls)) - @mock.patch.object(NXOSDevice, "show") - def test_get_free_space_with_custom_filesystem(self, mock_show): + def test_get_free_space_with_custom_filesystem(self): """Test _get_free_space uses custom file system when provided.""" - mock_show.return_value = """Directory of disk0:/ -1000000 bytes used -2000000 bytes free -3000000 bytes total - + self.device.native_ssh.send_command.side_effect = None + self.device.native_ssh.send_command.return_value = """ + 31 May 18 22:15:23 2026 dmesg + 0 May 18 22:15:24 2026 libfipf.5934 + 0 May 18 22:15:25 2026 libfipf.5961 + 0 May 18 22:15:35 2026 libfipf.6282 + 9343 May 18 22:20:52 2026 messages + 186 May 18 22:17:27 2026 mtm_lib.log + 169 May 18 22:15:25 2026 startupdebug + 663 May 18 22:15:26 2026 syslogd_ha_debug + +Usage for log://sup-local + 53248 bytes used + 52375552 bytes free + 52428800 bytes total """ - result = self.device._get_free_space("disk0:") - self.assertEqual(result, 2000000) - mock_show.assert_called_with("dir disk0:", raw_text=True) + result = self.device._get_free_space("log:") + self.assertEqual(result, 52375552) + self.device.native_ssh.send_command.assert_called_with("dir log:", read_timeout=30) - @mock.patch.object(NXOSDevice, "show") - def test_get_free_space_raises_on_parse_error(self, mock_show): + def test_get_free_space_raises_on_parse_error(self): """Test _get_free_space raises CommandError when output can't be parsed.""" - mock_show.return_value = "Directory of bootflash:/\nNo free space info here\n" + self.device.native_ssh.send_command.side_effect = None + self.device.native_ssh.send_command.return_value = "Directory of bootflash:/\nNo free space info here\n" with self.assertRaises(CommandError): self.device._get_free_space() def test_check_file_exists_true(self): + self.device.native_ssh.send_command.side_effect = None self.device.native_ssh.send_command.return_value = "12345 bootflash:/nxos.bin" result = self.device.check_file_exists("nxos.bin", file_system="bootflash:") self.assertTrue(result) self.device.native_ssh.send_command.assert_called_with("dir bootflash:/nxos.bin", read_timeout=30) def test_check_file_exists_false(self): + self.device.native_ssh.send_command.side_effect = None self.device.native_ssh.send_command.return_value = "No such file or directory" result = self.device.check_file_exists("nxos.bin", file_system="bootflash:") self.assertFalse(result) self.device.native_ssh.send_command.assert_called_with("dir bootflash:/nxos.bin", read_timeout=30) def test_check_file_exists_command_error(self): + self.device.native_ssh.send_command.side_effect = None self.device.native_ssh.send_command.return_value = "some ambiguous output" with self.assertRaises(CommandError): self.device.check_file_exists("nxos.bin", file_system="bootflash:") def test_get_remote_checksum(self): - self.device.native_ssh.send_command.return_value = "abc123" + self.device.native_ssh.send_command.side_effect = None + # NXOS returns just the hex digest on its own line for ``show file md5sum``. + self.device.native_ssh.send_command.return_value = "4357603f1a9ed6ae27906b96d4daac49" result = self.device.get_remote_checksum("nxos.bin", hashing_algorithm="md5", file_system="bootflash:") - self.assertEqual(result, "abc123") - self.device.native_ssh.send_command.assert_called_with("show file bootflash:/nxos.bin md5sum", read_timeout=30) + self.assertEqual(result, "4357603f1a9ed6ae27906b96d4daac49") + self.device.native_ssh.send_command.assert_called_with("show file bootflash:nxos.bin md5sum", read_timeout=30) def test_get_remote_checksum_invalid_algorithm(self): with self.assertRaises(ValueError): @@ -397,10 +504,15 @@ def test_remote_file_copy_transfer_success(self): timeout=30, ) self.device.native_ssh.find_prompt.return_value = "host#" - self.device.native_ssh.send_command.return_value = "Copy complete" + # Mock send_command to return success message that includes the prompt + self.device.native_ssh.send_command.side_effect = None + self.device.native_ssh.send_command.return_value = "Copy complete, now saving to disk (please wait)...\nhost#" with mock.patch.object(NXOSDevice, "verify_file", side_effect=[False, True]): self.device.remote_file_copy(src, file_system="bootflash:") + # Verify send_command was called with expect_string parameter self.device.native_ssh.send_command.assert_called_once() + call_args = self.device.native_ssh.send_command.call_args + self.assertIn("expect_string", call_args.kwargs) def test_remote_file_copy_transfer_fails_verification(self): src = FileCopyModel( @@ -411,7 +523,9 @@ def test_remote_file_copy_transfer_fails_verification(self): timeout=30, ) self.device.native_ssh.find_prompt.return_value = "host#" - self.device.native_ssh.send_command.return_value = "Copy complete" + # Mock send_command to return success message that includes the prompt + self.device.native_ssh.send_command.side_effect = None + self.device.native_ssh.send_command.return_value = "Copy complete, now saving to disk (please wait)...\nhost#" with mock.patch.object(NXOSDevice, "verify_file", side_effect=[False, False]): with self.assertRaises(FileTransferError): self.device.remote_file_copy(src, file_system="bootflash:") @@ -433,6 +547,60 @@ def test_remote_file_copy_raises_not_enough_free_space(self, mock_get_free_space self.device.remote_file_copy(src, file_system="bootflash:") self.device.native_ssh.send_command.assert_not_called() + def test_remote_file_copy_with_vrf_prompt_handling(self): + """Test remote_file_copy handles VRF prompts correctly.""" + src = FileCopyModel( + download_url="ftp://example.com/nxos.bin", + checksum="abc123", + file_name="nxos.bin", + hashing_algorithm="md5", + timeout=30, + username="testuser", + token="testpass", + vrf="management", # VRF specified for prompt response + ) + self.device.native_ssh.find_prompt.return_value = "host#" + # Mock send_command to return success message that includes the prompt + self.device.native_ssh.send_command.side_effect = None + self.device.native_ssh.send_command.return_value = "Copy complete, now saving to disk (please wait)...\nhost#" + with mock.patch.object(NXOSDevice, "verify_file", side_effect=[False, True]): + self.device.remote_file_copy(src, file_system="bootflash:") + + # Verify send_command was called with VRF prompt handling + self.device.native_ssh.send_command.assert_called_once() + call_args = self.device.native_ssh.send_command.call_args + self.assertIn("expect_string", call_args.kwargs) + # Verify the expect_string contains VRF prompt pattern + expect_string = call_args.kwargs["expect_string"] + self.assertIn("Enter vrf", expect_string) + + def test_remote_file_copy_with_no_vrf_specified(self): + """Test remote_file_copy handles VRF prompts when no VRF is specified.""" + src = FileCopyModel( + download_url="ftp://example.com/nxos.bin", + checksum="abc123", + file_name="nxos.bin", + hashing_algorithm="md5", + timeout=30, + username="testuser", + token="testpass", + # No VRF specified - should respond with empty string to VRF prompt + ) + self.device.native_ssh.find_prompt.return_value = "host#" + # Mock send_command to return success message that includes the prompt + self.device.native_ssh.send_command.side_effect = None + self.device.native_ssh.send_command.return_value = "Copy complete, now saving to disk (please wait)...\nhost#" + with mock.patch.object(NXOSDevice, "verify_file", side_effect=[False, True]): + self.device.remote_file_copy(src, file_system="bootflash:") + + # Verify send_command was called with VRF prompt handling + self.device.native_ssh.send_command.assert_called_once() + call_args = self.device.native_ssh.send_command.call_args + self.assertIn("expect_string", call_args.kwargs) + # Verify the expect_string contains VRF prompt pattern + expect_string = call_args.kwargs["expect_string"] + self.assertIn("Enter vrf", expect_string) + def test_remote_file_copy_invalid_scheme(self): src = FileCopyModel( download_url="smtp://example.com/nxos.bin", @@ -455,6 +623,125 @@ def test_remote_file_copy_query_string_not_supported(self): with self.assertRaises(ValueError): self.device.remote_file_copy(src, file_system="bootflash:") + def test_remote_file_copy_uses_ssh_for_filesystem_detection(self): + """remote_file_copy should use SSH for _get_file_system calls.""" + filename = "nxos.bin" + checksum = "a" * 32 + self.device.native_ssh.reset_mock() + src = FileCopyModel( + download_url=f"https://example.com/{filename}", + checksum=checksum, + file_name=filename, + hashing_algorithm="md5", + timeout=30, + ) + + self.device.native_ssh.send_command.side_effect = None + self.device.native_ssh.send_command.return_value = NXOS_DIR_CMD + self.device.native_ssh.find_prompt.return_value = "host#" + + with ( + mock.patch.object(self.device, "verify_file", return_value=True), + mock.patch.object(self.device, "show") as mock_show, + ): + self.device.remote_file_copy(src) + mock_show.assert_not_called + + ssh_calls = self.device.native_ssh.send_command.call_args_list + self.assertTrue( + any("dir" in str(call) for call in ssh_calls), + "Expected SSH 'dir' command for filesystem detection", + ) + + @mock.patch("pyntc.devices.nxos_device.ConnectHandler", create=True) + @mock.patch("pyntc.devices.nxos_device.NXOSNative", autospec=True) + def test_port_default(self, mock_device, mock_connect_handler): + """Test that port defaults to None when not specified.""" + _ = NXOSDevice("host", "user", "pass") + + # Verify NXOSNative was called with default port (None) + mock_device.assert_called_with( + "host", + "user", + "pass", + transport="http", + timeout=30, + port=None, # Default port + verify=True, + ) + + @mock.patch("pyntc.devices.nxos_device.ConnectHandler", create=True) + @mock.patch("pyntc.devices.nxos_device.NXOSNative", autospec=True) + def test_port_custom(self, mock_device, mock_connect_handler): + """Test that custom port is passed to NXOSNative.""" + _ = NXOSDevice("host", "user", "pass", port=8080) + + # Verify NXOSNative was called with custom port + mock_device.assert_called_with( + "host", + "user", + "pass", + transport="http", + timeout=30, + port=8080, # Custom port + verify=True, + ) + + @mock.patch("pyntc.devices.nxos_device.ConnectHandler", create=True) + @mock.patch("pyntc.devices.nxos_device.NXOSNative", autospec=True) + def test_port_with_https(self, mock_device, mock_connect_handler): + """Test that port works with HTTPS transport.""" + _ = NXOSDevice("host", "user", "pass", transport="https", port=8443) + + # Verify NXOSNative was called with HTTPS and custom port + mock_device.assert_called_with( + "host", + "user", + "pass", + transport="https", + timeout=30, + port=8443, # Custom HTTPS port + verify=True, + ) + + @mock.patch("pyntc.devices.nxos_device.ConnectHandler", create=True) + @mock.patch("pyntc.devices.nxos_device.NXOSNative", autospec=True) + def test_port_parameter_stored(self, mock_device, mock_connect_handler): + """Test that the port parameter is stored and used for NX-API connection.""" + device = NXOSDevice("host", "user", "pass", port=8080) + + # Verify port is used for NXOSNative (NX-API) + mock_device.assert_called_with( + "host", + "user", + "pass", + transport="http", + timeout=30, + port=8080, # port for NX-API + verify=True, + ) + + # Verify port parameter is stored + self.assertEqual(device.port, 8080) + + @mock.patch("pyntc.devices.nxos_device.ConnectHandler", create=True) + @mock.patch("pyntc.devices.nxos_device.NXOSNative", autospec=True) + def test_backward_compatibility_no_port(self, mock_device, mock_connect_handler): + """Test backward compatibility when port is not specified.""" + # Create device without specifying port + _ = NXOSDevice("host", "user", "pass", transport="http") + + # Should default to port None + mock_device.assert_called_with( + "host", + "user", + "pass", + transport="http", + timeout=30, + port=None, # Default port + verify=True, + ) + if __name__ == "__main__": unittest.main() From a82f509d61a5e628edbf3f670e13609164eb4f76 Mon Sep 17 00:00:00 2001 From: Gary Snider <75227981+gsnider2195@users.noreply.github.com> Date: Tue, 26 May 2026 07:07:49 -0700 Subject: [PATCH 2/6] v3.0.0 post sync to develop (#385) * Release 3.0.0 * bump version --------- Co-authored-by: Jeff Kala <48843785+jeffkala@users.noreply.github.com> Co-authored-by: James Williams --- changes/383.changed | 1 - docs/admin/release_notes/version_3.0.md | 14 +++++ mkdocs.yml | 1 + poetry.lock | 79 ++++++++++++++----------- pyproject.toml | 4 +- towncrier_template.j2 | 3 +- 6 files changed, 61 insertions(+), 41 deletions(-) delete mode 100644 changes/383.changed create mode 100644 docs/admin/release_notes/version_3.0.md diff --git a/changes/383.changed b/changes/383.changed deleted file mode 100644 index a5816568..00000000 --- a/changes/383.changed +++ /dev/null @@ -1 +0,0 @@ -The pyntc rotating file handler is now opt-in via the `PYNTC_LOG_FILE` environment variable. When unset, no log file is created. When set, its value is used as the log file path, and the handler is registered only once per logger to avoid duplicate entries on repeated `get_log` calls. diff --git a/docs/admin/release_notes/version_3.0.md b/docs/admin/release_notes/version_3.0.md new file mode 100644 index 00000000..3d73011c --- /dev/null +++ b/docs/admin/release_notes/version_3.0.md @@ -0,0 +1,14 @@ +# v3.0 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 + +- Pyntc now requires the `PYNTC_LOG_FILE` environment variable to output logging to a file. The new default behavior is to only log to stderr. + + +## [v3.0.0 (2026-05-06)](https://github.com/networktocode/pyntc/releases/tag/v3.0.0) + +### Breaking Changes + +- [#383](https://github.com/networktocode/pyntc/issues/383) - The pyntc rotating file handler is now opt-in via the `PYNTC_LOG_FILE` environment variable. When unset, no log file is created. When set, its value is used as the log file path, and the handler is registered only once per logger to avoid duplicate entries on repeated `get_log` calls. diff --git a/mkdocs.yml b/mkdocs.yml index 2e34d2ad..de0ef850 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -132,6 +132,7 @@ nav: - Uninstall: "admin/uninstall.md" - Release Notes: - "admin/release_notes/index.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" - v2.2: "admin/release_notes/version_2.2.md" diff --git a/poetry.lock b/poetry.lock index 3e9e9c6b..fbcf7862 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.3.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. [[package]] name = "astroid" @@ -149,14 +149,14 @@ typecheck = ["mypy"] [[package]] name = "certifi" -version = "2026.4.22" +version = "2026.5.20" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" groups = ["main", "dev", "docs"] files = [ - {file = "certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a"}, - {file = "certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580"}, + {file = "certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897"}, + {file = "certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d"}, ] [[package]] @@ -398,14 +398,14 @@ files = [ [[package]] name = "click" -version = "8.4.0" +version = "8.4.1" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.10" groups = ["dev", "docs"] files = [ - {file = "click-8.4.0-py3-none-any.whl", hash = "sha256:40c50b7c6c6adac2823d411041ec84f3f103f1b280d5e9ce0d7f998995832f81"}, - {file = "click-8.4.0.tar.gz", hash = "sha256:638f1338fe1235c8f4e008e4a8a254fb5c5fbdcbb40ece3c9142ebb78e792973"}, + {file = "click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2"}, + {file = "click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96"}, ] [package.dependencies] @@ -719,14 +719,14 @@ files = [ [[package]] name = "hypothesis" -version = "6.152.8" +version = "6.153.0" description = "The property-based testing library for Python" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "hypothesis-6.152.8-py3-none-any.whl", hash = "sha256:61b1e6e14f0623e8afe27a4a1b1fce5a4611beefef015b987a5c7d0359babbda"}, - {file = "hypothesis-6.152.8.tar.gz", hash = "sha256:9c0dd56c6ce5649ef3289555ae9fec40663401cf7134a99f926acf1b91fb6d9f"}, + {file = "hypothesis-6.153.0-py3-none-any.whl", hash = "sha256:2aeda9bbb44ae0ee0bfa67ef744a25be05c1f804dca4eb6479c63518dc9f2900"}, + {file = "hypothesis-6.153.0.tar.gz", hash = "sha256:11616e5158fc485d62bae19d9cc69333237faa8050ad44a45218254a1ef272bb"}, ] [package.dependencies] @@ -753,14 +753,14 @@ zoneinfo = ["tzdata (>=2026.2) ; sys_platform == \"win32\" or sys_platform == \" [[package]] name = "idna" -version = "3.15" +version = "3.16" description = "Internationalized Domain Names in Applications (IDNA)" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main", "dev", "docs"] files = [ - {file = "idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8"}, - {file = "idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc"}, + {file = "idna-3.16-py3-none-any.whl", hash = "sha256:cc246e3a3f89580c3a951b5ad298ca4638078b2cdd4f115654332b5c26daded5"}, + {file = "idna-3.16.tar.gz", hash = "sha256:d7a6da03db833450fca25d2358ac9ff06cd624577a4aea3a596d5c0f77b8e03d"}, ] [package.extras] @@ -826,14 +826,14 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "junos-eznc" -version = "2.7.6" +version = "2.8.0" description = "Junos 'EZ' automation for non-programmers" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "junos_eznc-2.7.6-py2.py3-none-any.whl", hash = "sha256:a4f9cc290fde95b881b7e83ecd5bc315ec32051679894d811075d6758cc1fb20"}, - {file = "junos_eznc-2.7.6.tar.gz", hash = "sha256:c4187fc2879c92939102799d7231c33fd49dfa1c5bc5357683bab7a0bd891194"}, + {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"}, ] [package.dependencies] @@ -872,6 +872,7 @@ 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"}, @@ -887,6 +888,7 @@ 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"}, @@ -904,6 +906,7 @@ 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"}, @@ -921,6 +924,7 @@ 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"}, @@ -938,6 +942,7 @@ 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"}, @@ -955,6 +960,7 @@ 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"}, @@ -977,6 +983,7 @@ 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"}, @@ -1997,30 +2004,30 @@ oldlibyaml = ["ruamel.yaml.clib ; platform_python_implementation == \"CPython\"" [[package]] name = "ruff" -version = "0.15.13" +version = "0.15.14" 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.13-py3-none-linux_armv6l.whl", hash = "sha256:444b580fc72fd6887e650acd3e575e18cdc79dbcf42fb4030b491057921f61f8"}, - {file = "ruff-0.15.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6590d009e7cb7ebf36f83dbdd44a3fa48a0994ff6f1cdc1b08006abe58f98dc7"}, - {file = "ruff-0.15.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1c26d2f66163deeb6e08d8b39fbbe983ce3c71cea06a6d7591cfd1421793c629"}, - {file = "ruff-0.15.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbd6f94b434f896308e4d57fb7bfde0d02b99f7a64b3bdab0fdfa6a864203a5"}, - {file = "ruff-0.15.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3259f3be4d181bda591da5db2571aed6853c6a048157756448020bc6c5cd22"}, - {file = "ruff-0.15.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae9c17e5eb4430c154e76abc25d79a318190f5a997f38fb6b114416c5319ffc9"}, - {file = "ruff-0.15.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e2e39bff6c341f4b577a21b801326fab0b11847f48fcaa83f00a113c9b3cb55"}, - {file = "ruff-0.15.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e8d9a8e08013542e94d3220bc5b62cc3e5ef87c5f74bff367d3fac14fab013e6"}, - {file = "ruff-0.15.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc411dfebe5eebe55ce041c6ae080eb7668955e866daa2fbb16692a784f1c4ca"}, - {file = "ruff-0.15.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:768494eb08b9cee54e2fd27969966f74db5a57f6eaa7a90fcb3306af34dfc4bd"}, - {file = "ruff-0.15.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fb75f9a3a7e42ffe117d734494e6c5e5cb3565d66e12612cb63d0e572a41a5b6"}, - {file = "ruff-0.15.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8cb74dd33bb2f6613faf7fc03b660053b5ac4f80e706d5788c6335e2a8048d51"}, - {file = "ruff-0.15.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7ef823f817fcd191dc934e984be9cf4094f808effa16f2542ad8e821ba02bbf2"}, - {file = "ruff-0.15.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f345a13937bd7f09f6f5d19fa0721b0c103e00e7f62bc67089a8e5e037719e0b"}, - {file = "ruff-0.15.13-py3-none-win32.whl", hash = "sha256:4044f94208b3b05ba0fc4a4abd0558cf4d6459bd18325eead7fd8cc66f909b41"}, - {file = "ruff-0.15.13-py3-none-win_amd64.whl", hash = "sha256:7064884d442b7d477b4e7473d12da7f08851d2b1982763c5d3f388a19468a1a4"}, - {file = "ruff-0.15.13-py3-none-win_arm64.whl", hash = "sha256:2471da9bd1068c8c064b5fd9c0c4b6dddffd6369cb1cd68b29993b1709ff1b21"}, - {file = "ruff-0.15.13.tar.gz", hash = "sha256:f9d89f17f7ba7fb2ed42921f0df75da797a9a5d71bc39049e2c687cf2baf44b7"}, + {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"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index f1da5ebb..76c7f229 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pyntc" -version = "2.4.2a0" +version = "3.0.1a0" 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 --ignore-glob='*mock*'" [tool.towncrier] package = "pyntc" directory = "changes" -filename = "docs/admin/release_notes/version_2.4.md" +filename = "docs/admin/release_notes/version_3.0.md" template = "towncrier_template.j2" start_string = "" issue_format = "[#{issue}](https://github.com/networktocode/pyntc/issues/{issue})" diff --git a/towncrier_template.j2 b/towncrier_template.j2 index f69a5668..243e94a0 100644 --- a/towncrier_template.j2 +++ b/towncrier_template.j2 @@ -1,4 +1,3 @@ - # v{{ versiondata.version.split(".")[:2] | join(".") }} 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). @@ -8,6 +7,7 @@ This document describes all new features and changes in the release. The format - Major features or milestones - Changes to compatibility with Nautobot and/or other apps, libraries etc. + {% if render_title %} ## [v{{ versiondata.version }} ({{ versiondata.date }})](https://github.com/networktocode/pyntc/releases/tag/v{{ versiondata.version}}) @@ -40,4 +40,3 @@ No significant changes. {% endif %} {% endfor %} - From 8c636a222384b2dfebddaaaafd934a701f396c32 Mon Sep 17 00:00:00 2001 From: James Williams Date: Tue, 26 May 2026 15:14:32 -0500 Subject: [PATCH 3/6] Release/3.0.1 post sync to develop (#393) * Release 3.0.0 * Release v3.0.1 (#391) * Bug Fix NXOS (#387) * Fix operations to use native_ssh instead of nx-api * Update NXOS so that both nx-api and ssh can co-exist * Updates to remove api_port * remove redundant line, update tests * changelog * minor tweak to f5 workaround * Updated the following NXOSDevice methods to use netmiko: - _image_booted - _wait_for_device_reboot - uptime - hostname - redundancy_state - reboot - set_boot_options Removed caching for the uptime and uptime_string properties Added an NXOSDevice.show_netmiko method and added a deprecation warning to the existing NXOSDevice.show method * added test mocks for most of the netmiko commands and updated tests * revert breaking property changes and fix pylint * revert breaking changes * Updated the NXOSDevice driver to use netmiko for the os_version * updated the nxos file transfer methods to resolve file verification * ruff ruff * add changelog fragments * update the NXOSDevice.save to use netmiko instead of NXAPI * Migrate NXOSDevice.show to netmiko from NXAPI * update _wait_for_device_reboot to reconnect via ssh * updates per CI failures * updates per CI failures --------- Co-authored-by: Gary Snider <75227981+gsnider2195@users.noreply.github.com> Co-authored-by: James Williams * v3.0.0 post sync to develop (#385) * Release 3.0.0 * bump version --------- Co-authored-by: Jeff Kala <48843785+jeffkala@users.noreply.github.com> Co-authored-by: James Williams * bump version * create release notes * fix v3 release notes * fix v3 release notes --------- Co-authored-by: Matt Miller Co-authored-by: Gary Snider <75227981+gsnider2195@users.noreply.github.com> Co-authored-by: Jeff Kala <48843785+jeffkala@users.noreply.github.com> * bump version --------- Co-authored-by: Gary Snider <75227981+gsnider2195@users.noreply.github.com> Co-authored-by: Jeff Kala <48843785+jeffkala@users.noreply.github.com> Co-authored-by: Matt Miller --- changes/387.added | 1 - changes/387.changed | 1 - changes/387.fixed | 8 -------- docs/admin/release_notes/version_3.0.md | 22 ++++++++++++++++++++++ pyproject.toml | 2 +- 5 files changed, 23 insertions(+), 11 deletions(-) delete mode 100644 changes/387.added delete mode 100644 changes/387.changed delete mode 100644 changes/387.fixed diff --git a/changes/387.added b/changes/387.added deleted file mode 100644 index 24934f42..00000000 --- a/changes/387.added +++ /dev/null @@ -1 +0,0 @@ -Added an `NXOSDevice.show_netmiko` method and deprecated the existing `NXOSDevice.show` method that uses pynxos. Developers should transition to the `show_netmiko` method to prepare for the eventual removal of pynxos. diff --git a/changes/387.changed b/changes/387.changed deleted file mode 100644 index 40851e39..00000000 --- a/changes/387.changed +++ /dev/null @@ -1 +0,0 @@ -Changed the following NXOSDevice methods/properties to use Netmiko instead of pynxos: `_image_booted`, `_wait_for_device_reboot`, `uptime`, `hostname`, `os_version`, `_get_file_system`, `_get_free_space`, `remote_file_copy`, `redundancy_state`, `reboot`, `set_boot_options`, and `startup_config`. diff --git a/changes/387.fixed b/changes/387.fixed deleted file mode 100644 index 5f1a8b4e..00000000 --- a/changes/387.fixed +++ /dev/null @@ -1,8 +0,0 @@ -Fixed a bug in nxos where nx-api commands were mixed with ssh commands. -Fixed a bug in nxos `_build_url_copy_command_simple` returning the wrong type of data. -Fixed a bug in nxos failing to answer a prompt when using remote_file_copy. -Fixed NXOSDevice.os_version to use netmiko SSH instead of NX-API. -Fixed NXOSDevice.get_remote_checksum to use the correct `show file` command form and parse the digest out of the device output. -Fixed NXOSDevice.save to use netmiko SSH instead of NX-API. -Fixed NXOSDevice.show to use netmiko SSH instead of NX-API. Structured (non-`raw_text`) results are now TextFSM-parsed lists of dicts. -Fixed NXOSDevice._wait_for_device_reboot to drop the pre-reboot SSH session and reconnect each poll so it reliably detects when the device comes back from a reload. diff --git a/docs/admin/release_notes/version_3.0.md b/docs/admin/release_notes/version_3.0.md index 3d73011c..336193ee 100644 --- a/docs/admin/release_notes/version_3.0.md +++ b/docs/admin/release_notes/version_3.0.md @@ -7,6 +7,28 @@ This document describes all new features and changes in the release. The format - Pyntc now requires the `PYNTC_LOG_FILE` environment variable to output logging to a file. The new default behavior is to only log to stderr. + +## [v3.0.1 (2026-05-26)](https://github.com/networktocode/pyntc/releases/tag/v3.0.1) + +### Added + +- [#387](https://github.com/networktocode/pyntc/issues/387) - Added an `NXOSDevice.show_netmiko` method and deprecated the existing `NXOSDevice.show` method that uses pynxos. Developers should transition to the `show_netmiko` method to prepare for the eventual removal of pynxos. + +### Changed + +- [#387](https://github.com/networktocode/pyntc/issues/387) - Changed the following NXOSDevice methods/properties to use Netmiko instead of pynxos: `_image_booted`, `_wait_for_device_reboot`, `uptime`, `hostname`, `os_version`, `_get_file_system`, `_get_free_space`, `remote_file_copy`, `redundancy_state`, `reboot`, `set_boot_options`, and `startup_config`. + +### Fixed + +- [#387](https://github.com/networktocode/pyntc/issues/387) - Fixed a bug in nxos where nx-api commands were mixed with ssh commands. +- [#387](https://github.com/networktocode/pyntc/issues/387) - Fixed a bug in nxos `_build_url_copy_command_simple` returning the wrong type of data. +- [#387](https://github.com/networktocode/pyntc/issues/387) - Fixed a bug in nxos failing to answer a prompt when using remote_file_copy. +- [#387](https://github.com/networktocode/pyntc/issues/387) - Fixed NXOSDevice.os_version to use netmiko SSH instead of NX-API. +- [#387](https://github.com/networktocode/pyntc/issues/387) - Fixed NXOSDevice.get_remote_checksum to use the correct `show file` command form and parse the digest out of the device output. +- [#387](https://github.com/networktocode/pyntc/issues/387) - Fixed NXOSDevice.save to use netmiko SSH instead of NX-API. +- [#387](https://github.com/networktocode/pyntc/issues/387) - Fixed NXOSDevice.show to use netmiko SSH instead of NX-API. Structured (non-`raw_text`) results are now TextFSM-parsed lists of dicts. +- [#387](https://github.com/networktocode/pyntc/issues/387) - Fixed NXOSDevice._wait_for_device_reboot to drop the pre-reboot SSH session and reconnect each poll so it reliably detects when the device comes back from a reload. + ## [v3.0.0 (2026-05-06)](https://github.com/networktocode/pyntc/releases/tag/v3.0.0) ### Breaking Changes diff --git a/pyproject.toml b/pyproject.toml index 76c7f229..d1b608e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pyntc" -version = "3.0.1a0" +version = "3.0.2a0" description = "Python library focused on tasks related to device level and OS management." authors = ["Network to Code, LLC "] readme = "README.md" From 508cf9b67966f9b466e7d80301a04dddaffd7a0c Mon Sep 17 00:00:00 2001 From: James Williams Date: Thu, 28 May 2026 11:25:54 -0500 Subject: [PATCH 4/6] NAPPS-968 | Adds device.install_mode to base driver and ios driver (#395) * adds device.install_mode to base driver and ios driver * pylint * Updates per review --- changes/395.added | 1 + changes/395.deprecated | 1 + pyntc/devices/base_device.py | 14 ++ pyntc/devices/ios_device.py | 38 +++- tests/unit/test_devices/test_base_device.py | 15 ++ tests/unit/test_devices/test_ios_device.py | 182 ++++++++++++++++++-- 6 files changed, 235 insertions(+), 16 deletions(-) create mode 100644 changes/395.added create mode 100644 changes/395.deprecated create mode 100644 tests/unit/test_devices/test_base_device.py diff --git a/changes/395.added b/changes/395.added new file mode 100644 index 00000000..91173338 --- /dev/null +++ b/changes/395.added @@ -0,0 +1 @@ +Added an ``install_mode`` property to ``BaseDevice`` (abstract) and ``IOSDevice``; the IOS implementation returns ``True`` when the device boots from ``packages.conf``. diff --git a/changes/395.deprecated b/changes/395.deprecated new file mode 100644 index 00000000..4358f3af --- /dev/null +++ b/changes/395.deprecated @@ -0,0 +1 @@ +Deprecated the ``install_mode`` argument to ``IOSDevice.install_os``; install mode is now derived from the device's ``boot_options`` via the new ``install_mode`` property and will be removed in a future release. diff --git a/pyntc/devices/base_device.py b/pyntc/devices/base_device.py index b24a7496..b01c8ff6 100644 --- a/pyntc/devices/base_device.py +++ b/pyntc/devices/base_device.py @@ -450,6 +450,20 @@ def verify_file(self, checksum, filename, hashing_algorithm="md5", **kwargs): """ raise NotImplementedError + @property + def install_mode(self): + """Indicate whether the device is operating in install mode. + + Drivers override this to derive the value from the device's current boot + configuration. Used by ``install_os`` to choose between install-mode and + legacy upgrade procedures. + + Returns: + (bool): True when the device boots from an install-mode bundle + (e.g., ``packages.conf`` on IOS-XE), False otherwise. + """ + raise NotImplementedError + def install_os(self, image_name, reboot=True, **vendor_specifics): """Install the OS from specified image_name. diff --git a/pyntc/devices/ios_device.py b/pyntc/devices/ios_device.py index 471c53c5..5baeb4e3 100644 --- a/pyntc/devices/ios_device.py +++ b/pyntc/devices/ios_device.py @@ -3,6 +3,7 @@ import os import re import time +import warnings from netmiko import ConnectHandler, FileTransfer from netmiko.exceptions import ReadTimeout @@ -319,6 +320,17 @@ def boot_options(self): log.debug("Host %s: the boot options are {dict(sys=boot_image)}", self.host) return {"sys": boot_image} + @property + def install_mode(self): + """Return whether the device is currently booted in install mode. + + Returns: + (bool): True when the current boot image equals + :data:`INSTALL_MODE_FILE_NAME` (i.e., ``packages.conf``), + False otherwise. + """ + return self.boot_options.get("sys") == INSTALL_MODE_FILE_NAME + def checkpoint(self, checkpoint_file): """Create checkpoint file. @@ -877,13 +889,28 @@ def file_copy_remote_exists(self, src, dest=None, file_system=None): log.debug("Host %s: File %s does not already exist on remote.", self.host, src) return False - def install_os(self, image_name, reboot=True, install_mode=False, read_timeout=2000, **vendor_specifics): + def _resolve_install_mode(self, install_mode): + """Return the effective install_mode flag, warning if the caller passed it explicitly.""" + if install_mode is None: + return self.install_mode + warnings.warn( + "The install_mode argument to install_os is deprecated; install mode is now " + "derived from the device's boot_options via the install_mode property.", + DeprecationWarning, + ) + return install_mode + + def install_os(self, image_name, reboot=True, install_mode=None, read_timeout=2000, **vendor_specifics): """Installs the prescribed Network OS, which must be present before issuing this command. Args: image_name (str): Name of the IOS image to boot into reboot (bool): Whether to reboot the device after setting the boot options. Defaults to true. - install_mode (bool, optional): Uses newer install method on devices. Defaults to False. + install_mode (bool, optional): **Deprecated.** Whether to use the newer install-mode + upgrade procedure. When omitted (the default), the value is derived from + :attr:`install_mode`, which reads the device's current boot configuration. + Passing the argument explicitly still works but emits a ``DeprecationWarning`` + and will be removed in a future release. read_timeout (int, optional): Netmiko timeout when waiting for device prompt. Default 2000. vendor_specifics (dict, optional): Vendor specific arguments to pass to the install command. @@ -893,14 +920,15 @@ def install_os(self, image_name, reboot=True, install_mode=False, read_timeout=2 Returns: (bool): False if no install is needed, true if the install completes successfully """ + use_install_mode = self._resolve_install_mode(install_mode) timeout = vendor_specifics.get("timeout", 3600) if not self._image_booted(image_name): - if install_mode and not reboot: + if use_install_mode and not reboot: raise ValueError( "IOS devices automatically reboot after installation when using install mode but the reboot argument was set to false." ) - if install_mode: + if use_install_mode: # Change boot statement to be boot system :packages.conf self.set_boot_options(INSTALL_MODE_FILE_NAME, **vendor_specifics) @@ -942,7 +970,7 @@ def install_os(self, image_name, reboot=True, install_mode=False, read_timeout=2 self._wait_for_device_reboot(timeout=timeout) # Set FastCLI back to originally set when using install mode - if install_mode: + if use_install_mode: image_name = INSTALL_MODE_FILE_NAME # Verify the OS level if not self._image_booted(image_name): diff --git a/tests/unit/test_devices/test_base_device.py b/tests/unit/test_devices/test_base_device.py new file mode 100644 index 00000000..4e3c6ecb --- /dev/null +++ b/tests/unit/test_devices/test_base_device.py @@ -0,0 +1,15 @@ +"""Tests for the abstract :class:`pyntc.devices.base_device.BaseDevice` contract.""" + +import pytest + +from pyntc.devices.base_device import BaseDevice + + +@pytest.fixture +def base_device(): + return BaseDevice(host="host", username="user", password="pass") + + +def test_install_mode_raises_not_implemented(base_device): + with pytest.raises(NotImplementedError): + _ = base_device.install_mode diff --git a/tests/unit/test_devices/test_ios_device.py b/tests/unit/test_devices/test_ios_device.py index ccc141e0..6e7b9b47 100644 --- a/tests/unit/test_devices/test_ios_device.py +++ b/tests/unit/test_devices/test_ios_device.py @@ -392,22 +392,26 @@ def test_enable_from_config(self): self.device.native.check_config_mode.assert_called() self.device.native.exit_config_mode.assert_called() + @mock.patch.object(IOSDevice, "install_mode", new_callable=mock.PropertyMock, return_value=False) @mock.patch.object(IOSDevice, "_image_booted", side_effect=[False, True]) @mock.patch.object(IOSDevice, "set_boot_options") @mock.patch.object(IOSDevice, "reboot") @mock.patch.object(IOSDevice, "_wait_for_device_reboot") - def test_install_os(self, mock_wait, mock_reboot, mock_set_boot, mock_image_booted): + def test_install_os(self, mock_wait, mock_reboot, mock_set_boot, mock_image_booted, mock_install_mode): state = self.device.install_os(BOOT_IMAGE) mock_set_boot.assert_called() mock_reboot.assert_called() mock_wait.assert_called() self.assertEqual(state, True) + @mock.patch.object(IOSDevice, "install_mode", new_callable=mock.PropertyMock, return_value=False) @mock.patch.object(IOSDevice, "_image_booted", side_effect=[True]) @mock.patch.object(IOSDevice, "set_boot_options") @mock.patch.object(IOSDevice, "reboot") @mock.patch.object(IOSDevice, "_wait_for_device_reboot") - def test_install_os_already_installed(self, mock_wait, mock_reboot, mock_set_boot, mock_image_booted): + def test_install_os_already_installed( + self, mock_wait, mock_reboot, mock_set_boot, mock_image_booted, mock_install_mode + ): state = self.device.install_os(BOOT_IMAGE) mock_image_booted.assert_called_once() mock_set_boot.assert_not_called() @@ -415,15 +419,19 @@ def test_install_os_already_installed(self, mock_wait, mock_reboot, mock_set_boo mock_wait.assert_not_called() self.assertEqual(state, False) + @mock.patch.object(IOSDevice, "install_mode", new_callable=mock.PropertyMock, return_value=False) @mock.patch.object(IOSDevice, "_image_booted", side_effect=[False, False]) @mock.patch.object(IOSDevice, "set_boot_options") @mock.patch.object(IOSDevice, "reboot") @mock.patch.object(IOSDevice, "_wait_for_device_reboot") @mock.patch.object(IOSDevice, "_raw_version_data") - def test_install_os_error(self, mock_wait, mock_reboot, mock_set_boot, mock_image_booted, mock_raw_version_data): + def test_install_os_error( + self, mock_wait, mock_reboot, mock_set_boot, mock_image_booted, mock_raw_version_data, mock_install_mode + ): mock_raw_version_data.return_value = DEVICE_FACTS self.assertRaises(ios_module.OSInstallError, self.device.install_os, BOOT_IMAGE) + @mock.patch.object(IOSDevice, "install_mode", new_callable=mock.PropertyMock, return_value=True) @mock.patch.object(IOSDevice, "os_version", new_callable=mock.PropertyMock) @mock.patch.object(IOSDevice, "_image_booted", side_effect=[False, True]) @mock.patch.object(IOSDevice, "set_boot_options") @@ -440,11 +448,12 @@ def test_install_os_not_enough_space( mock_set_boot, mock_image_booted, mock_os_version, + mock_install_mode, ): mock_raw_version_data.return_value = DEVICE_FACTS mock_os_version.return_value = "17.4.3" mock_show.return_value = "FAILED: There is not enough free disk available to perform this operation on switch 1. At least 1276287 KB of free disk is required" - self.assertRaises(ios_module.OSInstallError, self.device.install_os, image_name=BOOT_IMAGE, install_mode=True) + self.assertRaises(ios_module.OSInstallError, self.device.install_os, image_name=BOOT_IMAGE) mock_wait.assert_not_called() mock_reboot.assert_not_called() @@ -1260,12 +1269,36 @@ def test_set_boot_options_image_packages_conf_file( mock_boot_options.assert_called_once() +# +# TESTS FOR IOS INSTALL MODE PROPERTY +# + + +@mock.patch.object(IOSDevice, "boot_options", new_callable=mock.PropertyMock) +def test_install_mode_true_when_boot_image_is_packages_conf(mock_boot_options, ios_device): + mock_boot_options.return_value = {"sys": ios_module.INSTALL_MODE_FILE_NAME} + assert ios_device.install_mode is True + + +@mock.patch.object(IOSDevice, "boot_options", new_callable=mock.PropertyMock) +def test_install_mode_false_for_image_bin(mock_boot_options, ios_device): + mock_boot_options.return_value = {"sys": "cat9k_iosxe.16.12.04.SPA.bin"} + assert ios_device.install_mode is False + + +@mock.patch.object(IOSDevice, "boot_options", new_callable=mock.PropertyMock) +def test_install_mode_false_for_missing_sys_key(mock_boot_options, ios_device): + mock_boot_options.return_value = {} + assert ios_device.install_mode is False + + # # TESTS FOR IOS INSTALL MODE METHOD # # Test install mode upgrade for install mode with latest method +@mock.patch.object(IOSDevice, "install_mode", new_callable=mock.PropertyMock) @mock.patch.object(IOSDevice, "os_version", new_callable=mock.PropertyMock) @mock.patch.object(IOSDevice, "_image_booted") @mock.patch.object(IOSDevice, "set_boot_options") @@ -1281,16 +1314,18 @@ def test_install_os_install_mode( mock_set_boot_options, mock_image_booted, mock_os_version, + mock_install_mode, ios_device, ): image_name = "cat9k_iosxe.16.12.04.SPA.bin" file_system = "flash:" + mock_install_mode.return_value = True mock_get_file_system.return_value = file_system mock_os_version.return_value = "16.12.03a" mock_image_booted.side_effect = [False, True] mock_show.side_effect = [IOError("Search pattern never detected in send_command")] # Call the install os function - actual = ios_device.install_os(image_name, install_mode=True) + actual = ios_device.install_os(image_name) # Check the results mock_set_boot_options.assert_called_with("packages.conf") @@ -1305,6 +1340,7 @@ def test_install_os_install_mode( # Test install mode upgrade fail +@mock.patch.object(IOSDevice, "install_mode", new_callable=mock.PropertyMock) @mock.patch.object(IOSDevice, "os_version", new_callable=mock.PropertyMock) @mock.patch.object(IOSDevice, "_image_booted") @mock.patch.object(IOSDevice, "set_boot_options") @@ -1322,8 +1358,10 @@ def test_install_os_install_mode_failed( mock_set_boot_options, mock_image_booted, mock_os_version, + mock_install_mode, ios_device, ): + mock_install_mode.return_value = True mock_hostname.return_value = "ntc-rtr01" image_name = "cat9k_iosxe.16.12.04.SPA.bin" file_system = "flash:" @@ -1333,7 +1371,7 @@ def test_install_os_install_mode_failed( mock_show.side_effect = [IOError("Search pattern never detected in send_command")] # Call the install os function with pytest.raises(ios_module.OSInstallError) as err: - ios_device.install_os(image_name, install_mode=True) + ios_device.install_os(image_name) assert err.value.message == "ntc-rtr01 was unable to boot into packages.conf" @@ -1349,6 +1387,7 @@ def test_install_os_install_mode_failed( # Test install mode upgrade for install mode with latest method +@mock.patch.object(IOSDevice, "install_mode", new_callable=mock.PropertyMock) @mock.patch.object(IOSDevice, "os_version", new_callable=mock.PropertyMock) @mock.patch.object(IOSDevice, "_image_booted") @mock.patch.object(IOSDevice, "set_boot_options") @@ -1364,16 +1403,18 @@ def test_install_os_install_mode_no_upgrade( mock_set_boot_options, mock_image_booted, mock_os_version, + mock_install_mode, ios_device, ): image_name = "cat9k_iosxe.16.12.04.SPA.bin" file_system = "flash:" + mock_install_mode.return_value = True mock_get_file_system.return_value = file_system mock_os_version.return_value = "16.12.03a" mock_image_booted.side_effect = [True, True] mock_show.side_effect = [IOError("Search pattern never detected in send_command")] # Call the install os function - actual = ios_device.install_os(image_name, install_mode=True) + actual = ios_device.install_os(image_name) # Check the results mock_set_boot_options.assert_not_called() @@ -1391,6 +1432,7 @@ def test_install_os_install_mode_no_upgrade( # Test install mode upgrade for install mode with interim method on OS Version +@mock.patch.object(IOSDevice, "install_mode", new_callable=mock.PropertyMock) @mock.patch.object(IOSDevice, "os_version", new_callable=mock.PropertyMock) @mock.patch.object(IOSDevice, "_image_booted") @mock.patch.object(IOSDevice, "set_boot_options") @@ -1406,15 +1448,17 @@ def test_install_os_install_mode_from_everest( mock_set_boot_options, mock_image_booted, mock_os_version, + mock_install_mode, ios_device, ): image_name = "cat9k_iosxe.16.12.04.SPA.bin" file_system = "flash:" + mock_install_mode.return_value = True mock_get_file_system.return_value = file_system mock_os_version.return_value = "16.6.1" mock_image_booted.side_effect = [False, True] # Call the install_os - actual = ios_device.install_os(image_name, install_mode=True) + actual = ios_device.install_os(image_name) # Test the results mock_set_boot_options.assert_called_with("packages.conf") @@ -1430,6 +1474,7 @@ def test_install_os_install_mode_from_everest( # Test install mode upgrade for install mode with interim method on OS Version with error unable to complete +@mock.patch.object(IOSDevice, "install_mode", new_callable=mock.PropertyMock) @mock.patch.object(IOSDevice, "os_version", new_callable=mock.PropertyMock) @mock.patch.object(IOSDevice, "_image_booted") @mock.patch.object(IOSDevice, "set_boot_options") @@ -1448,8 +1493,10 @@ def test_install_os_install_mode_from_everest_failed( mock_set_boot_options, mock_image_booted, mock_os_version, + mock_install_mode, ios_device, ): + mock_install_mode.return_value = True mock_hostname.return_value = "ntc-rtr01" image_name = "cat9k_iosxe.16.12.04.SPA.bin" file_system = "flash:" @@ -1458,7 +1505,7 @@ def test_install_os_install_mode_from_everest_failed( mock_image_booted.side_effect = [False, False] # Call the install_os with pytest.raises(ios_module.OSInstallError) as err: - ios_device.install_os(image_name, install_mode=True) + ios_device.install_os(image_name) assert err.value.message == "ntc-rtr01 was unable to boot into packages.conf" @@ -1475,6 +1522,7 @@ def test_install_os_install_mode_from_everest_failed( # Test install mode upgrade for install mode with interim method on OS Version with error unable to complete +@mock.patch.object(IOSDevice, "install_mode", new_callable=mock.PropertyMock) @mock.patch.object(IOSDevice, "os_version", new_callable=mock.PropertyMock) @mock.patch.object(IOSDevice, "_image_booted") @mock.patch.object(IOSDevice, "set_boot_options") @@ -1493,8 +1541,10 @@ def test_install_os_install_mode_from_everest_to_everest( mock_set_boot_options, mock_image_booted, mock_os_version, + mock_install_mode, ios_device, ): + mock_install_mode.return_value = True mock_hostname.return_value = "ntc-rtr01" image_name = "cat9k_iosxe.16.05.01a.SPA.bin" file_system = "flash:" @@ -1502,7 +1552,7 @@ def test_install_os_install_mode_from_everest_to_everest( mock_os_version.return_value = "16.5.1" mock_image_booted.side_effect = [True, True] # Call the install_os - actual = ios_device.install_os(image_name, install_mode=True) + actual = ios_device.install_os(image_name) # Test the results mock_set_boot_options.assert_not_called() @@ -1514,6 +1564,7 @@ def test_install_os_install_mode_from_everest_to_everest( assert actual is False +@mock.patch.object(IOSDevice, "install_mode", new_callable=mock.PropertyMock) @mock.patch.object(IOSDevice, "_has_reload_happened_recently") @mock.patch.object(IOSDevice, "os_version", new_callable=mock.PropertyMock) @mock.patch.object(IOSDevice, "_image_booted") @@ -1531,10 +1582,12 @@ def test_install_os_install_mode_with_retries( mock_image_booted, mock_os_version, mock_has_reload_happened_recently, + mock_install_mode, ios_device, ): image_name = "cat9k_iosxe.16.12.04.SPA.bin" file_system = "flash:" + mock_install_mode.return_value = True mock_get_file_system.return_value = file_system mock_os_version.return_value = "16.12.03a" mock_has_reload_happened_recently.side_effect = [False, False, True] @@ -1542,7 +1595,7 @@ def test_install_os_install_mode_with_retries( mock_sleep.return_value = None mock_show.return_value = "show must go on" # Call the install os function - actual = ios_device.install_os(image_name, install_mode=True) + actual = ios_device.install_os(image_name) # Check the results mock_set_boot_options.assert_called_with("packages.conf") @@ -1556,6 +1609,113 @@ def test_install_os_install_mode_with_retries( assert actual is True +# +# TESTS FOR install_os PROPERTY-DRIVEN BEHAVIOR & DEPRECATION +# + + +@mock.patch.object(IOSDevice, "install_mode", new_callable=mock.PropertyMock) +@mock.patch.object(IOSDevice, "_image_booted") +@mock.patch.object(IOSDevice, "set_boot_options") +@mock.patch.object(IOSDevice, "show") +@mock.patch.object(IOSDevice, "_wait_for_device_reboot") +@mock.patch.object(IOSDevice, "_get_file_system") +@mock.patch.object(IOSDevice, "reboot") +def test_install_os_uses_install_mode_property_true( + mock_reboot, + mock_get_file_system, + mock_wait_for_reboot, + mock_show, + mock_set_boot_options, + mock_image_booted, + mock_install_mode, + ios_device, +): + image_name = "cat9k_iosxe.16.12.04.SPA.bin" + file_system = "flash:" + mock_install_mode.return_value = True + mock_get_file_system.return_value = file_system + mock_image_booted.side_effect = [False, True] + mock_show.side_effect = [IOError("Search pattern never detected in send_command")] + with mock.patch.object(IOSDevice, "os_version", new_callable=mock.PropertyMock) as mock_os_version: + mock_os_version.return_value = "16.12.03a" + actual = ios_device.install_os(image_name) + + mock_set_boot_options.assert_called_with("packages.conf") + mock_show.assert_called_with( + f"install add file {file_system}{image_name} activate commit prompt-level none", read_timeout=2000 + ) + mock_reboot.assert_not_called() + mock_wait_for_reboot.assert_called() + assert actual is True + + +@mock.patch.object(IOSDevice, "install_mode", new_callable=mock.PropertyMock) +@mock.patch.object(IOSDevice, "_image_booted") +@mock.patch.object(IOSDevice, "set_boot_options") +@mock.patch.object(IOSDevice, "show") +@mock.patch.object(IOSDevice, "_wait_for_device_reboot") +@mock.patch.object(IOSDevice, "reboot") +def test_install_os_uses_install_mode_property_false( + mock_reboot, + mock_wait_for_reboot, + mock_show, + mock_set_boot_options, + mock_image_booted, + mock_install_mode, + ios_device, +): + image_name = "cat9k_iosxe.16.12.04.SPA.bin" + mock_install_mode.return_value = False + mock_image_booted.side_effect = [False, True] + actual = ios_device.install_os(image_name) + + mock_set_boot_options.assert_called_once_with(image_name) + # The install-mode "install add file" command must NOT be issued on the legacy path. + for call in mock_show.call_args_list: + args, _ = call + if args: + assert "install add file" not in args[0] + mock_reboot.assert_called_once() + mock_wait_for_reboot.assert_called() + assert actual is True + + +@mock.patch.object(IOSDevice, "install_mode", new_callable=mock.PropertyMock) +@mock.patch.object(IOSDevice, "os_version", new_callable=mock.PropertyMock) +@mock.patch.object(IOSDevice, "_image_booted") +@mock.patch.object(IOSDevice, "set_boot_options") +@mock.patch.object(IOSDevice, "show") +@mock.patch.object(IOSDevice, "_wait_for_device_reboot") +@mock.patch.object(IOSDevice, "_get_file_system") +@mock.patch.object(IOSDevice, "reboot") +def test_install_os_install_mode_kwarg_emits_deprecation_warning( + mock_reboot, + mock_get_file_system, + mock_wait_for_reboot, + mock_show, + mock_set_boot_options, + mock_image_booted, + mock_os_version, + mock_install_mode, + ios_device, +): + image_name = "cat9k_iosxe.16.12.04.SPA.bin" + file_system = "flash:" + # Property would say False, but the explicit kwarg must override. + mock_install_mode.return_value = False + mock_get_file_system.return_value = file_system + mock_os_version.return_value = "16.12.03a" + mock_image_booted.side_effect = [False, True] + mock_show.side_effect = [IOError("Search pattern never detected in send_command")] + + with pytest.warns(DeprecationWarning, match="install_mode argument"): + actual = ios_device.install_os(image_name, install_mode=True) + + mock_set_boot_options.assert_called_with("packages.conf") + assert actual is True + + def test_show(ios_send_command): command = "show_ip_arp" device = ios_send_command([f"{command}.txt"]) From cc54d9b4a19e716d0af9c5ea0bde09769ef99052 Mon Sep 17 00:00:00 2001 From: Gavin Acosta <155584291+Defiantearth@users.noreply.github.com> Date: Thu, 28 May 2026 15:54:07 -0500 Subject: [PATCH 5/6] Fixed Junos reboots not being detected when waiting for the device to reload (#394) --- changes/394.fixed | 2 + pyntc/devices/jnpr_device.py | 93 ++++++++++++++++----- tests/unit/test_devices/test_jnpr_device.py | 82 +++++++++++++++--- 3 files changed, 143 insertions(+), 34 deletions(-) create mode 100644 changes/394.fixed diff --git a/changes/394.fixed b/changes/394.fixed new file mode 100644 index 00000000..20126908 --- /dev/null +++ b/changes/394.fixed @@ -0,0 +1,2 @@ +Fixed Junos reboots not being detected when waiting for the device to reload. +Increased the default Junos reboot wait timeout from 1 hour to 2 hours. diff --git a/pyntc/devices/jnpr_device.py b/pyntc/devices/jnpr_device.py index 70c8fcad..b6431204 100644 --- a/pyntc/devices/jnpr_device.py +++ b/pyntc/devices/jnpr_device.py @@ -228,18 +228,53 @@ def _uptime_to_string(self, uptime_full_string): days, hours, minutes, seconds = self._uptime_components(uptime_full_string) return f"{days:02d}:{hours:02d}:{minutes:02d}:{seconds:02d}" - def _wait_for_device_reboot(self, timeout=3600): + def _wait_for_device_reboot(self, original_uptime, timeout=7200): + """Block until the device reboots and accepts a fresh connection. + + Drops the existing NETCONF session and polls for the device to come back. + The reboot is considered complete when a new connection succeeds and the + device reports an uptime lower than ``original_uptime`` (i.e., it has + booted since the reboot was issued). + + The pre-reboot session must be discarded first: once the device restarts, + PyEZ still reports it as connected even though the transport is dead, so it + is closed here to force each probe to establish a fresh connection. + + Args: + original_uptime (int): Device uptime in seconds captured before the reboot. + timeout (int, optional): Max seconds to wait for the device to return. Defaults to 2 hours. + """ start = time.time() - disconnected = False + + # Drop the pre-reboot NETCONF session so subsequent probes can't read from + # a stale connection PyEZ still reports as connected. + try: + self.close() + except Exception as close_exc: # pylint: disable=broad-exception-caught + log.debug("Host %s: Pre-reboot disconnect raised %s (ignored).", self.host, close_exc) + while time.time() - start < timeout: - if disconnected: - try: - self.open() + try: + self.open() + self._uptime = None + current_uptime = self.uptime + if current_uptime is not None and current_uptime < original_uptime: + log.info( + "Host %s: Device rebooted (uptime %ss < pre-reboot %ss).", + self.host, + current_uptime, + original_uptime, + ) return - except: # noqa E722 # nosec # pylint: disable=bare-except - pass - elif not self.connected: - disconnected = True + log.debug( + "Host %s: Reachable but uptime %ss >= pre-reboot %ss; still waiting.", + self.host, + current_uptime, + original_uptime, + ) + except Exception as exc: # pylint: disable=broad-exception-caught + log.debug("Host %s: Reboot probe failed (%s); will retry.", self.host, exc) + self.native.connected = False time.sleep(10) raise RebootTimeoutError(hostname=self.hostname, wait_time=timeout) @@ -318,12 +353,14 @@ def uptime(self): Returns: (int): Device uptime in seconds. """ - try: - native_uptime_string = self.native.facts["RE0"]["up_time"] - except (AttributeError, TypeError): - native_uptime_string = None - if self._uptime is None: + try: + # Bust PyEZ's cached facts so a cold cache always reflects the live device. + self.native.facts_refresh(keys="RE0") + native_uptime_string = self.native.facts["RE0"]["up_time"] + except (AttributeError, TypeError, KeyError): + native_uptime_string = None + if native_uptime_string is not None: self._uptime = self._uptime_to_seconds(native_uptime_string) @@ -337,13 +374,16 @@ def uptime_string(self): Returns: (str): Device uptime. """ - try: - native_uptime_string = self.native.facts["RE0"]["up_time"] - except (AttributeError, TypeError): - native_uptime_string = None - if self._uptime_string is None: - self._uptime_string = self._uptime_to_string(native_uptime_string) + try: + # Bust PyEZ's cached facts so a cold cache always reflects the live device. + self.native.facts_refresh(keys="RE0") + native_uptime_string = self.native.facts["RE0"]["up_time"] + except (AttributeError, TypeError, KeyError): + native_uptime_string = None + + if native_uptime_string is not None: + self._uptime_string = self._uptime_to_string(native_uptime_string) return self._uptime_string @@ -505,13 +545,13 @@ def open(self): if not self.connected: self.native.open() - def reboot(self, wait_for_reload=False, timeout=3600, confirm=None): + def reboot(self, wait_for_reload=False, timeout=7200, confirm=None): """ Reload the controller or controller pair. Args: wait_for_reload (bool): Whether the reboot method should wait for the device to come back up before returning. Defaults to False. - timeout (int, optional): Time in seconds to wait for the device to return after reboot. Defaults to 1 hour. + timeout (int, optional): Time in seconds to wait for the device to return after reboot. Defaults to 2 hours. confirm (None): Not used. Deprecated since v0.17.0. Example: @@ -522,9 +562,16 @@ def reboot(self, wait_for_reload=False, timeout=3600, confirm=None): if confirm is not None: warnings.warn("Passing 'confirm' to reboot method is deprecated.", DeprecationWarning) + self._uptime = None + original_uptime = self.uptime + if original_uptime is None: + raise CommandError( + command="reboot", + message="Could not determine pre-reboot uptime; refusing to wait for reload.", + ) self.sw.reboot(in_min=0) if wait_for_reload: - self._wait_for_device_reboot(timeout=timeout) + self._wait_for_device_reboot(original_uptime, timeout=timeout) def rollback(self, filename): """Rollback to a specific configuration file. diff --git a/tests/unit/test_devices/test_jnpr_device.py b/tests/unit/test_devices/test_jnpr_device.py index 5b9d325d..28271a64 100644 --- a/tests/unit/test_devices/test_jnpr_device.py +++ b/tests/unit/test_devices/test_jnpr_device.py @@ -244,18 +244,28 @@ def test_reboot(self): @mock.patch("pyntc.devices.jnpr_device.time.sleep") def test_wait_for_device_to_reboot(self, mock_sleep): - with mock.patch.object(self.device, "open") as mock_open: - # Emulate the device disconnected and reconnecting - type(self.device.native).connected = mock.PropertyMock(side_effect=[True, False, True]) - mock_open.side_effect = [Exception, Exception, True] - self.device.reboot(wait_for_reload=True, timeout=3) - mock_open.assert_has_calls([mock.call()] * 3) + """Reboot completes when the device returns with a lower uptime than the baseline.""" + with ( + mock.patch.object(self.device, "open") as mock_open, + mock.patch.object(self.device, "close"), + mock.patch.object(type(self.device), "uptime", new_callable=mock.PropertyMock) as mock_uptime, + ): + # First read is the pre-reboot baseline; second is the post-reboot uptime. + mock_uptime.side_effect = [455, 30] + self.device.reboot(wait_for_reload=True, timeout=30) + + self.device.sw.reboot.assert_called_with(in_min=0) + mock_open.assert_called() @mock.patch("pyntc.devices.jnpr_device.time.sleep") def test_wait_for_device_to_reboot_error(self, mock_sleep): - with mock.patch.object(self.device, "open") as mock_open: - type(self.device.native).connected = mock.PropertyMock(side_effect=[True, False]) - mock_open.side_effect = Exception + """Raise RebootTimeoutError when the device never reports a lower uptime within the timeout.""" + with ( + mock.patch.object(self.device, "open"), + mock.patch.object(self.device, "close"), + mock.patch.object(type(self.device), "uptime", new_callable=mock.PropertyMock) as mock_uptime, + ): + mock_uptime.return_value = 455 with pytest.raises(RebootTimeoutError): self.device.reboot(wait_for_reload=True, timeout=1) @@ -290,12 +300,62 @@ def test_checkpoint(self, mock_scp): self.device.show.assert_called_with("show config") def test_uptime(self): + """Cold cache (_uptime is None) refreshes facts and parses the uptime.""" + self.assertIsNone(self.device._uptime) + uptime = self.device.uptime + self.assertEqual(uptime, 455) + self.device.native.facts_refresh.assert_called_once_with(keys="RE0") + + def test_uptime_cached(self): + """A populated cache is returned as-is, with no device round-trip.""" + self.device._uptime = 1234 uptime = self.device.uptime - assert uptime == 455 + self.assertEqual(uptime, 1234) + self.device.native.facts_refresh.assert_not_called() + + def test_uptime_refreshes_after_cache_cleared(self): + """Clearing the cache forces a fresh read.""" + self.assertEqual(self.device.uptime, 455) + + self.device._uptime = None + self.device.native.facts = {"RE0": {"up_time": "30 seconds"}} + + self.assertEqual(self.device.uptime, 30) + + def test_uptime_none_when_facts_unavailable(self): + """Missing/unavailable facts return None gracefully instead of raising an Exception.""" + self.device._uptime = None + self.device.native.facts = {} + self.assertIsNone(self.device.uptime) def test_uptime_string(self): + """Cold cache (_uptime_string is None) refreshes facts and formats the uptime.""" + self.assertIsNone(self.device._uptime_string) + uptime_string = self.device.uptime_string + self.assertEqual(uptime_string, "00:00:07:35") + self.device.native.facts_refresh.assert_called_once_with(keys="RE0") + + def test_uptime_string_cached(self): + """A populated cache is returned as-is, with no device round-trip.""" + self.device._uptime_string = "01:02:03:04" uptime_string = self.device.uptime_string - assert uptime_string == "00:00:07:35" + self.assertEqual(uptime_string, "01:02:03:04") + self.device.native.facts_refresh.assert_not_called() + + def test_uptime_string_refreshes_after_cache_cleared(self): + """Clearing the cache forces a fresh read.""" + self.assertEqual(self.device.uptime_string, "00:00:07:35") + + self.device._uptime_string = None + self.device.native.facts = {"RE0": {"up_time": "30 seconds"}} + + self.assertEqual(self.device.uptime_string, "00:00:00:30") + + def test_uptime_string_none_when_facts_unavailable(self): + """Missing/unavailable facts return None gracefully instead of raising an Exception.""" + self.device._uptime_string = None + self.device.native.facts = {} + self.assertIsNone(self.device.uptime_string) def test_vendor(self): vendor = self.device.vendor From b85478410c537a59db40874ee39a3a8d28e29369 Mon Sep 17 00:00:00 2001 From: James Williams Date: Thu, 28 May 2026 15:59:49 -0500 Subject: [PATCH 6/6] Release v3.0.2 --- changes/394.fixed | 2 -- changes/395.added | 1 - changes/395.deprecated | 1 - docs/admin/release_notes/version_3.0.md | 14 ++++++++++++++ pyproject.toml | 2 +- 5 files changed, 15 insertions(+), 5 deletions(-) delete mode 100644 changes/394.fixed delete mode 100644 changes/395.added delete mode 100644 changes/395.deprecated diff --git a/changes/394.fixed b/changes/394.fixed deleted file mode 100644 index 20126908..00000000 --- a/changes/394.fixed +++ /dev/null @@ -1,2 +0,0 @@ -Fixed Junos reboots not being detected when waiting for the device to reload. -Increased the default Junos reboot wait timeout from 1 hour to 2 hours. diff --git a/changes/395.added b/changes/395.added deleted file mode 100644 index 91173338..00000000 --- a/changes/395.added +++ /dev/null @@ -1 +0,0 @@ -Added an ``install_mode`` property to ``BaseDevice`` (abstract) and ``IOSDevice``; the IOS implementation returns ``True`` when the device boots from ``packages.conf``. diff --git a/changes/395.deprecated b/changes/395.deprecated deleted file mode 100644 index 4358f3af..00000000 --- a/changes/395.deprecated +++ /dev/null @@ -1 +0,0 @@ -Deprecated the ``install_mode`` argument to ``IOSDevice.install_os``; install mode is now derived from the device's ``boot_options`` via the new ``install_mode`` property and will be removed in a future release. diff --git a/docs/admin/release_notes/version_3.0.md b/docs/admin/release_notes/version_3.0.md index 336193ee..dd258994 100644 --- a/docs/admin/release_notes/version_3.0.md +++ b/docs/admin/release_notes/version_3.0.md @@ -7,6 +7,20 @@ This document describes all new features and changes in the release. The format - Pyntc now requires the `PYNTC_LOG_FILE` environment variable to output logging to a file. The new default behavior is to only log to stderr. +## [v3.0.2 (2026-05-28)](https://github.com/networktocode/pyntc/releases/tag/v3.0.2) + +### Added + +- [#395](https://github.com/networktocode/pyntc/issues/395) - Added an ``install_mode`` property to ``BaseDevice`` (abstract) and ``IOSDevice``; the IOS implementation returns ``True`` when the device boots from ``packages.conf``. + +### Deprecated + +- [#395](https://github.com/networktocode/pyntc/issues/395) - Deprecated the ``install_mode`` argument to ``IOSDevice.install_os``; install mode is now derived from the device's ``boot_options`` via the new ``install_mode`` property and will be removed in a future release. + +### Fixed + +- [#394](https://github.com/networktocode/pyntc/issues/394) - Fixed Junos reboots not being detected when waiting for the device to reload. +- [#394](https://github.com/networktocode/pyntc/issues/394) - Increased the default Junos reboot wait timeout from 1 hour to 2 hours. ## [v3.0.1 (2026-05-26)](https://github.com/networktocode/pyntc/releases/tag/v3.0.1) diff --git a/pyproject.toml b/pyproject.toml index d1b608e5..978fdc61 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pyntc" -version = "3.0.2a0" +version = "3.0.2" description = "Python library focused on tasks related to device level and OS management." authors = ["Network to Code, LLC "] readme = "README.md"