From f85d969f0660435e40113a92a7d0b90d15bd9175 Mon Sep 17 00:00:00 2001 From: Doug Goldstein Date: Mon, 24 Aug 2026 16:11:45 -0500 Subject: [PATCH 01/21] test(neutron-understack): add in-process ML2 scenario test harness Reuse neutron's Ml2PluginV2TestCase to stand up a real Ml2Plugin on in-memory SQLite with our mechanism drivers loaded, so tests drive real create/update/delete through the ML2 manager and assert which calls reach our drivers and with what data. SCENARIOS.md is the test-plan catalog; each scenario test is tagged @pytest.mark.scenario(""). test_scenario_coverage.py enforces catalog<->marker equivalence and conftest.py fails collection if a scenario test lacks its marker. Pin neutron/neutron-lib to the understack/2026.1 fork (matching the container) via [tool.uv.sources], and add neutron's test-only deps. --- containers/neutron/Dockerfile | 5 + .../tests/scenarios/SCENARIOS.md | 25 + .../tests/scenarios/__init__.py | 0 .../tests/scenarios/base.py | 104 ++ .../tests/scenarios/conftest.py | 28 + .../tests/scenarios/test_scenario_coverage.py | 62 + python/neutron-understack/pyproject.toml | 23 +- python/neutron-understack/uv.lock | 1156 +++++++++-------- 8 files changed, 849 insertions(+), 554 deletions(-) create mode 100644 python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md create mode 100644 python/neutron-understack/neutron_understack/tests/scenarios/__init__.py create mode 100644 python/neutron-understack/neutron_understack/tests/scenarios/base.py create mode 100644 python/neutron-understack/neutron_understack/tests/scenarios/conftest.py create mode 100644 python/neutron-understack/neutron_understack/tests/scenarios/test_scenario_coverage.py diff --git a/containers/neutron/Dockerfile b/containers/neutron/Dockerfile index 9afa4a76e..0ef7b9c27 100644 --- a/containers/neutron/Dockerfile +++ b/containers/neutron/Dockerfile @@ -21,9 +21,14 @@ ARG OPENSTACK_VERSION="required_argument" ADD https://releases.openstack.org/constraints/upper/${OPENSTACK_VERSION} /upper-constraints.txt RUN sed -i '/^neutron-lib==.*/d' /upper-constraints.txt +# --no-sources ignores neutron-understack's [tool.uv.sources] (a dev/test-only +# pin of neutron/neutron-lib to the understack/2026.1 git forks). Here we install +# those forks explicitly from /src, so honoring the git sources would just create +# a conflicting-URL error for neutron-lib. RUN --mount=type=cache,target=/root/.cache/uv \ uv pip install \ --upgrade \ + --no-sources \ --constraint /upper-constraints.txt \ /src/neutron \ /src/neutron-lib \ diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md b/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md new file mode 100644 index 000000000..33b46903d --- /dev/null +++ b/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md @@ -0,0 +1,25 @@ +# ML2 mechanism scenario catalog + +This is the human-readable **test plan** for the `neutron-understack` ML2 mechanism +drivers. Each scenario is given a stable ID and describes the operation it models +and the **mechanism calls + data** we expect to observe. + +Every scenario here must be implemented by at least one test tagged +`@pytest.mark.scenario("")`, and every such marker must reference a scenario +that exists here. `test_scenario_coverage.py` enforces this equivalence in both +directions, and `conftest.py` fails the run if any scenario test is missing a +marker. So this file and the tests cannot silently drift apart. + +## Conventions + +- Scenario IDs are declared as `### ` headings, e.g. + `### BM-BIND-01 — baremetal vif-attach binds hierarchically`. IDs use the form + `<AREA>-<TOPIC>-<NN>` (uppercase, hyphenated), which the coverage check parses. +- Ideas not yet implemented go under a "Planned / future" section as plain bullets + (no `###` heading) so they are not treated as catalogued-but-untested. +- A scenario whose desired behavior is a known bug is still catalogued; its test + asserts the desired behavior and is marked `@pytest.mark.xfail(strict=True)` with + a reason. Record such bugs under a "Known bugs" section. + +Scenarios are added per driver/area (baremetal port binding, routers, trunks, ...) +as those test modules land. diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/__init__.py b/python/neutron-understack/neutron_understack/tests/scenarios/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/base.py b/python/neutron-understack/neutron_understack/tests/scenarios/base.py new file mode 100644 index 000000000..bcc739f95 --- /dev/null +++ b/python/neutron-understack/neutron_understack/tests/scenarios/base.py @@ -0,0 +1,104 @@ +"""In-process ML2 scenario-test harness for the understack mechanism drivers. + +These tests reuse neutron's own functional test base (``Ml2PluginV2TestCase``), +which stands up a real ``Ml2Plugin`` backed by in-memory SQLite. We load our two +mechanism drivers -- ``understack`` and ``undersync`` -- into the binding chain and +drive real ``create_network`` / ``create_port`` / bind operations through the ML2 +manager, so a scenario can assert *which* calls reach our drivers and *with what +data*. This is the same style upstream neutron uses to test its own ML2 drivers +(OVN, OVS). + +Only the genuine external edge on the binding path -- the Undersync HTTP client -- +is mocked. Router scenarios (a follow-up) additionally fake the OVN IDL that +``neutron_understack.routers`` reaches into. +""" + +from unittest import mock + +from neutron.conf.plugins.ml2.drivers import driver_type +from neutron.tests.unit.plugins.ml2.test_plugin import Ml2PluginV2TestCase +from neutron_lib.plugins import directory +from oslo_config import cfg + +from neutron_understack import config as understack_config +from neutron_understack.undersync import Undersync + +#: A physnet the type manager knows about (the parent setUp configures physnet1 +#: with VLAN range 1:100), so ``allocate_dynamic_segment`` succeeds for the VLAN +#: group named in a baremetal binding profile. +DEFAULT_PHYSNET = "physnet1" + +#: Stand-in switch identity for the binding profile's local_link_information. +DEFAULT_SWITCH_ID = "11:22:33:44:55:66" +DEFAULT_SWITCH_INFO = "a1-1-1.iad3.rackspace.net" + + +class UnderstackMl2ScenarioBase(Ml2PluginV2TestCase): + """Base class for understack ML2 scenario tests.""" + + # Order matters: understack performs hierarchical VXLAN->VLAN binding and + # undersync finalizes the VLAN segment. 'logger' mirrors the production list. + _mechanism_drivers = ["logger", "understack", "undersync"] + + def setUp(self): + # ml2 opts are registered at plugin import time, so these overrides are + # in place before the plugin loads inside setup_parent(). Tenant networks + # are VXLAN (matching production); dynamic VLAN segments come from the + # physnet ranges the parent setUp configures. + cfg.CONF.set_override("project_network_types", ["vxlan"], group="ml2") + # The ml2_type_vxlan group is only registered when the vxlan type driver + # loads, so register it here before overriding its (empty) vni_ranges. + driver_type.register_ml2_drivers_vxlan_opts(cfg.CONF) + cfg.CONF.set_override("vni_ranges", ["1000:2000"], group="ml2_type_vxlan") + + # UnderstackDriver.initialize() registers these itself, but we register + # up front so we can set values the driver reads while loading. + understack_config.register_ml2_understack_opts(cfg.CONF) + cfg.CONF.set_override( + "undersync_url", "http://undersync.test", group="ml2_understack" + ) + cfg.CONF.set_override("undersync_dry_run", False, group="ml2_understack") + cfg.CONF.set_override( + "provisioning_network", + "00000000-0000-0000-0000-000000000000", + group="ml2_understack", + ) + + super().setUp() + + # Replace the real HTTP Undersync client with a mock -- the single + # genuine external edge on the binding path. The trunk driver captured + # its own reference at construction (trunk.py), so patch that too. + self.understack_driver = self._mech_driver("understack") + self.undersync_mock = mock.MagicMock(spec_set=Undersync) + self.understack_driver.undersync = self.undersync_mock + self.understack_driver.trunk_driver.undersync = self.undersync_mock + + def _mech_driver(self, name): + """Return the loaded mechanism driver instance registered under ``name``.""" + manager = directory.get_plugin().mechanism_manager + for ext in manager.ordered_mech_drivers: + if ext.name == name: + return ext.obj + raise AssertionError(f"mechanism driver {name!r} not loaded") + + @staticmethod + def baremetal_binding_profile(physnet=DEFAULT_PHYSNET, port_id="Ethernet1/1"): + """A baremetal port binding profile as Ironic supplies on vif-attach. + + ``physical_network`` names the VLAN group; ``local_link_information`` + identifies the switch port. Pass ``physnet=None`` to omit the + ``physical_network`` key (the "missing physnet" edge case). + """ + profile: dict = { + "local_link_information": [ + { + "port_id": port_id, + "switch_id": DEFAULT_SWITCH_ID, + "switch_info": DEFAULT_SWITCH_INFO, + } + ], + } + if physnet is not None: + profile["physical_network"] = physnet + return profile diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/conftest.py b/python/neutron-understack/neutron_understack/tests/scenarios/conftest.py new file mode 100644 index 000000000..6632c1ee8 --- /dev/null +++ b/python/neutron-understack/neutron_understack/tests/scenarios/conftest.py @@ -0,0 +1,28 @@ +"""Scenario-test enforcement hooks. + +Fails the collection if any test under the scenarios package is missing a +``@pytest.mark.scenario("<ID>")`` marker, so a scenario test cannot silently +escape the catalog↔marker coverage check in test_scenario_coverage.py. +""" + +import pytest + +# The coverage meta-test verifies the catalog itself and is not a scenario. +_EXEMPT_FILES = {"test_scenario_coverage.py"} + + +def pytest_collection_modifyitems(items): + missing = [] + for item in items: + path = str(getattr(item, "fspath", "")).replace("\\", "/") + if "/tests/scenarios/" not in path: + continue + if path.rsplit("/", 1)[-1] in _EXEMPT_FILES: + continue + if item.get_closest_marker("scenario") is None: + missing.append(item.nodeid) + if missing: + raise pytest.UsageError( + "scenario tests missing an @pytest.mark.scenario('<ID>') marker:\n " + + "\n ".join(missing) + ) diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/test_scenario_coverage.py b/python/neutron-understack/neutron_understack/tests/scenarios/test_scenario_coverage.py new file mode 100644 index 000000000..237b50576 --- /dev/null +++ b/python/neutron-understack/neutron_understack/tests/scenarios/test_scenario_coverage.py @@ -0,0 +1,62 @@ +"""Enforce that the scenario catalog and the tests agree, in both directions. + +- Every ``### <ID> — ...`` entry in SCENARIOS.md must be implemented by at least + one test tagged ``@pytest.mark.scenario("<ID>")``. +- Every such marker must reference an ID that exists in SCENARIOS.md. + +This is a static cross-check (it scans the catalog and the test sources), so it +holds regardless of which subset of tests a given run collects. +""" + +import re +from pathlib import Path + +SCENARIOS_DIR = Path(__file__).parent +CATALOG = SCENARIOS_DIR / "SCENARIOS.md" + +# Catalog IDs are declared as headings: "### BM-BIND-01 — title". +_CATALOG_ID_RE = re.compile(r"^#{2,3}\s+([A-Z][A-Z0-9]*(?:-[A-Z0-9]+)+)\b") +# Markers in test sources: @pytest.mark.scenario("BM-BIND-01"). +_MARKER_ID_RE = re.compile(r"""\.scenario\(\s*["']([^"']+)["']""") + + +def _catalog_ids(): + ids = [] + for line in CATALOG.read_text().splitlines(): + match = _CATALOG_ID_RE.match(line) + if match: + ids.append(match.group(1)) + return ids + + +def _marker_ids(): + ids = set() + for path in SCENARIOS_DIR.glob("test_*.py"): + if path.name == Path(__file__).name: + continue + ids.update(_MARKER_ID_RE.findall(path.read_text())) + return ids + + +def test_catalog_has_no_duplicate_ids(): + ids = _catalog_ids() + duplicates = sorted({i for i in ids if ids.count(i) > 1}) + assert not duplicates, f"duplicate scenario IDs in SCENARIOS.md: {duplicates}" + + +def test_every_catalogued_scenario_has_a_test(): + catalog = set(_catalog_ids()) + markers = _marker_ids() + untested = sorted(catalog - markers) + assert ( + not untested + ), f"catalogued scenarios with no @pytest.mark.scenario test: {untested}" + + +def test_every_scenario_marker_is_catalogued(): + catalog = set(_catalog_ids()) + markers = _marker_ids() + uncataloged = sorted(markers - catalog) + assert ( + not uncataloged + ), f"@pytest.mark.scenario IDs missing from SCENARIOS.md: {uncataloged}" diff --git a/python/neutron-understack/pyproject.toml b/python/neutron-understack/pyproject.toml index 953bf2772..702dda070 100644 --- a/python/neutron-understack/pyproject.toml +++ b/python/neutron-understack/pyproject.toml @@ -65,15 +65,36 @@ test = [ "pytest-cov>=7,<8", "pytest-mock>=3.14.0,<4", "sqlalchemy", + # Base classes reused from neutron.tests (Ml2PluginV2TestCase et al.) sit on + # top of neutron's own test-only stack, which is not pulled in when neutron + # is installed as a runtime dependency. + "oslotest", + "testtools", + "fixtures", + "webtest", + "testresources", + "testscenarios", ] [tool.uv] default-groups = ["test"] +# Test against the same neutron/neutron-lib the container ships (the +# understack/2026.1 fork), not PyPI. Keep these revs in lockstep with +# containers/neutron/Dockerfile (NEUTRON_GIT_REF / NEUTRON_LIB_GIT_REF). +[tool.uv.sources] +# renovate: name=openstack/neutron repo=https://github.com/rackerlabs/neutron.git branch=understack/2026.1 +neutron = { git = "https://github.com/rackerlabs/neutron.git", rev = "b0678e56e9c5a7ec2f335c4271465fc1c3202e2a" } +# renovate: name=openstack/neutron-lib repo=https://github.com/rackerlabs/neutron-lib.git branch=understack/2026.1 +neutron-lib = { git = "https://github.com/rackerlabs/neutron-lib.git", rev = "6d0367183cc92656ba449acefd830cb4e11f2f5b" } + [tool.pytest.ini_options] minversion = "6.0" -addopts = "-ra" +addopts = "-ra --strict-markers" filterwarnings = "ignore::DeprecationWarning" +markers = [ + "scenario(id): links a test to a scenario ID in tests/scenarios/SCENARIOS.md", +] [tool.ruff] # use our default and override anything we need specifically diff --git a/python/neutron-understack/uv.lock b/python/neutron-understack/uv.lock index d59082165..c748242b4 100644 --- a/python/neutron-understack/uv.lock +++ b/python/neutron-understack/uv.lock @@ -4,16 +4,16 @@ requires-python = "==3.12.*" [[package]] name = "alembic" -version = "1.17.2" +version = "1.19.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mako" }, { name = "sqlalchemy" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/02/a6/74c8cadc2882977d80ad756a13857857dbcf9bd405bc80b662eb10651282/alembic-1.17.2.tar.gz", hash = "sha256:bbe9751705c5e0f14877f02d46c53d10885e377e3d90eda810a016f9baa19e8e", size = 1988064, upload-time = "2025-11-14T20:35:04.057Z" } +sdist = { url = "https://files.pythonhosted.org/packages/16/2b/e4153978368de59918115c9e01d3ebf58a558a7285efa7e960c383c4b59a/alembic-1.19.1.tar.gz", hash = "sha256:e0fca0518118c78acc493e31bcb5402f190057aaf6df8b5b95ce94c4789cf648", size = 2070816, upload-time = "2026-08-08T16:32:01.565Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/88/6237e97e3385b57b5f1528647addea5cc03d4d65d5979ab24327d41fb00d/alembic-1.17.2-py3-none-any.whl", hash = "sha256:f483dd1fe93f6c5d49217055e4d15b905b425b6af906746abb35b69c1996c4e6", size = 248554, upload-time = "2025-11-14T20:35:05.699Z" }, + { url = "https://files.pythonhosted.org/packages/20/89/e62cc37b69ad357cc8ecd6e7367f5245f523d3cbb338a66197212bdf6749/alembic-1.19.1-py3-none-any.whl", hash = "sha256:b39018cb3d9413a19cbd54cf3c02ad33998641f0538eb77413a488a21c3e14be", size = 265946, upload-time = "2026-08-08T16:32:03.153Z" }, ] [[package]] @@ -30,20 +30,20 @@ wheels = [ [[package]] name = "attrs" -version = "25.4.0" +version = "26.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] [[package]] name = "autopage" -version = "0.5.2" +version = "0.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9f/9e/559b0cfdba9f3ed6744d8cbcdbda58880d3695c43c053a31773cefcedde3/autopage-0.5.2.tar.gz", hash = "sha256:826996d74c5aa9f4b6916195547312ac6384bac3810b8517063f293248257b72", size = 33031, upload-time = "2023-10-16T09:22:19.54Z" } +sdist = { url = "https://files.pythonhosted.org/packages/75/76/9078d8db91f29af9ac5a359757f63f2d0fa869aba704d5ef0f836db62ea1/autopage-0.6.0.tar.gz", hash = "sha256:42d07de90de63e83762828028bfd56d19906a18f7c951ef6eef3e9ad48a3071d", size = 26797, upload-time = "2026-01-30T03:42:05.676Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/63/f1c3fa431e91a52bad5e3602e9d5df6c94d8d095ac485424efa4eeddb4d2/autopage-0.5.2-py3-none-any.whl", hash = "sha256:f5eae54dd20ccc8b1ff611263fc87bc46608a9cde749bbcfc93339713a429c55", size = 30231, upload-time = "2023-10-16T09:22:17.316Z" }, + { url = "https://files.pythonhosted.org/packages/73/6c/0324d6ed15cfb3ed0d2578fd1486f815e01f7f8d0a32b1522ff3e611e5f9/autopage-0.6.0-py3-none-any.whl", hash = "sha256:87566f08a7d4ba20e346515d26ba1132f2fac4e5619ffba3079e63c28e5df98f", size = 30693, upload-time = "2026-01-30T03:42:04.449Z" }, ] [[package]] @@ -84,75 +84,104 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/44/d2ef5e87509158ad2187f4dd0852df80695bb1ee0cfe0a684727b01a69e0/bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927", size = 144953, upload-time = "2025-09-25T19:50:37.32Z" }, ] +[[package]] +name = "beautifulsoup4" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/65/318323f98dbee45d42dff61d8f047181bc6f2268a9068cfad035a46be5af/beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", size = 632571, upload-time = "2026-06-07T16:44:20.453Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9", size = 109924, upload-time = "2026-06-07T16:44:21.566Z" }, +] + [[package]] name = "cachetools" -version = "6.2.2" +version = "7.1.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fb/44/ca1675be2a83aeee1886ab745b28cda92093066590233cc501890eb8417a/cachetools-6.2.2.tar.gz", hash = "sha256:8e6d266b25e539df852251cfd6f990b4bc3a141db73b939058d809ebd2590fc6", size = 31571, upload-time = "2025-11-13T17:42:51.465Z" } +sdist = { url = "https://files.pythonhosted.org/packages/70/d2/47e8bc06fe2a06d3f5bdf20f1126ab66c4e99dc48d940e7ba873f7ac7131/cachetools-7.1.7.tar.gz", hash = "sha256:a3e2a00b14d8f8a6b70c1dae7b4685e7ad3bc965c5b42124a2d6ce895da6cf50", size = 40680, upload-time = "2026-08-01T21:20:40.434Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/46/eb6eca305c77a4489affe1c5d8f4cae82f285d9addd8de4ec084a7184221/cachetools-6.2.2-py3-none-any.whl", hash = "sha256:6c09c98183bf58560c97b2abfcedcbaf6a896a490f534b031b661d3723b45ace", size = 11503, upload-time = "2025-11-13T17:42:50.232Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d8/767faeda872075724b95dd675466a645f1b92aadcdcf2d1429dcfd76c176/cachetools-7.1.7-py3-none-any.whl", hash = "sha256:ef98ef375ad188819ef2f9b3645e3987f4b8c5b7550e436ad998c2de78296df0", size = 16830, upload-time = "2026-08-01T21:20:38.977Z" }, ] [[package]] name = "certifi" -version = "2025.11.12" +version = "2026.7.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, ] [[package]] name = "cffi" -version = "2.0.0" +version = "2.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser", marker = "implementation_name != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, - { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, - { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, - { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, - { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, - { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, - { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, - { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, - { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, - { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, - { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/10/69/43965eccfdead3b9220015fd1320e117be8c6ed01a62ffab76eeb752f5d5/cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0", size = 184821, upload-time = "2026-08-03T21:19:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/54/7d/16e5a096677b5e313ca80cd5e5170efa3ea44624a82bb111925522da64b1/cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf", size = 184719, upload-time = "2026-08-03T21:19:46.129Z" }, + { url = "https://files.pythonhosted.org/packages/56/e6/8941622732edec876dd17d0453dce07317ae96db34f2ec1436c9d3785986/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", size = 214799, upload-time = "2026-08-03T21:19:47.218Z" }, + { url = "https://files.pythonhosted.org/packages/44/de/f98430906df1545ffde0d543dd124a7a439bc2cd32b36b9c53f805df7333/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", size = 222389, upload-time = "2026-08-03T21:19:48.331Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/717f1526b9957b34456313c31645c5b82b8fb5c3fe9e4752999be7128bfc/cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50", size = 210249, upload-time = "2026-08-03T21:19:49.543Z" }, + { url = "https://files.pythonhosted.org/packages/64/b3/f8aa4f3e34986c7e4ec45072d1b1b9dd295b6b18007b45518d79726dd725/cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e", size = 208775, upload-time = "2026-08-03T21:19:50.918Z" }, + { url = "https://files.pythonhosted.org/packages/b1/db/dceb9dd5b231e1da801793f8acc9f3c52a7e1afe40bb1aae37e02b0faad5/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", size = 221822, upload-time = "2026-08-03T21:19:52.054Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d2/6cd24ae3be000a634109c247d1475d62e5616d0dc78c82770942ec384248/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", size = 225232, upload-time = "2026-08-03T21:19:53.109Z" }, + { url = "https://files.pythonhosted.org/packages/cb/52/3fa190537004dd7f0ab860a6dc7c0175b8667f68d1e618a46f5498d30250/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", size = 223597, upload-time = "2026-08-03T21:19:54.515Z" }, + { url = "https://files.pythonhosted.org/packages/80/fb/0bb75b7039588c074b37ae99f40d9bfddf990ecb2fbc346ebccd2e56b9be/cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e", size = 175292, upload-time = "2026-08-03T21:19:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/d9/79/615cc094e2fb508cade7de88d3b4f6c4ec2bab695c97bce9153dc65aadf5/cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a", size = 185919, upload-time = "2026-08-03T21:19:56.89Z" }, + { url = "https://files.pythonhosted.org/packages/70/c6/d0ea84713fe46b243a436a18fcd47d639732747e21635c8a27191b06dc30/cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80", size = 180093, upload-time = "2026-08-03T21:19:58.155Z" }, ] [[package]] name = "charset-normalizer" -version = "3.4.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, - { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, - { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, - { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, - { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, - { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, - { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, - { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, - { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, - { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, - { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, - { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, - { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, - { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, - { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +version = "3.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/3f/143b048436775b0f76ac3eec145c019e8173ccc2885c8f20319b996d5e83/charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3", size = 171764, upload-time = "2026-08-15T08:20:44.807Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/27/78873dc8b6a56357517b74b6bb9568b80450e7bb4f6ef7e3fa9d22aa0bd7/charset_normalizer-3.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f", size = 344456, upload-time = "2026-08-15T08:17:10.072Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4c/be49ada26b1f0232d57aa89bbebf997a5cc2332a5616b6eca26ff680044d/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa", size = 238530, upload-time = "2026-08-15T08:17:11.563Z" }, + { url = "https://files.pythonhosted.org/packages/76/84/6f1290fa07ae6978d3960caa3eb1b8019bf9284ab7c2297b00c099ef4250/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369", size = 230200, upload-time = "2026-08-15T08:17:12.919Z" }, + { url = "https://files.pythonhosted.org/packages/e7/a0/47b18adeed31c8f16ba9700f32c1b18594cfa09f47eb672a488c273c22bf/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893", size = 262222, upload-time = "2026-08-15T08:17:14.571Z" }, + { url = "https://files.pythonhosted.org/packages/38/fe/341861ac118dae06f3ec0eb487488af52128f2ef2faf0b11003944d22259/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0", size = 258951, upload-time = "2026-08-15T08:17:16.158Z" }, + { url = "https://files.pythonhosted.org/packages/6f/89/bb5108dc6c3651dca963f2b0a3ba19bbcb370c94e1b6d3e0e844a58e6dca/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08", size = 248801, upload-time = "2026-08-15T08:17:17.683Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ba/ef83ae3aca816393decfa3530976f38a79812d707b80b580ac33b83f9877/charset_normalizer-3.5.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada", size = 244070, upload-time = "2026-08-15T08:17:19.191Z" }, + { url = "https://files.pythonhosted.org/packages/f6/0b/c5292a2462d69b7378ea89793bbb5b2b6fcf6f7dd6d1667f9619094ad553/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9", size = 240110, upload-time = "2026-08-15T08:17:20.547Z" }, + { url = "https://files.pythonhosted.org/packages/46/22/111e5be3b740d5c2a5bfcedb3d237b6591e5c2e82ae9d6ffcb121fe0909c/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e", size = 232836, upload-time = "2026-08-15T08:17:21.895Z" }, + { url = "https://files.pythonhosted.org/packages/f9/d2/d2aad6fe0dbb44b194bf3becb60f5a0ac48446ade999a47fe7bb41eb09a7/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6", size = 262712, upload-time = "2026-08-15T08:17:23.727Z" }, + { url = "https://files.pythonhosted.org/packages/35/5a/337e4663a5eae6de99db940ee8066d4145caafb61327db62deda15313cce/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf", size = 242977, upload-time = "2026-08-15T08:17:25.157Z" }, + { url = "https://files.pythonhosted.org/packages/ca/85/f82f8a92e31c7519410e2e1afdc630f28ec47490ce2c09a11c1a43cbb459/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71", size = 260207, upload-time = "2026-08-15T08:17:26.602Z" }, + { url = "https://files.pythonhosted.org/packages/b7/52/643d11ffd60e9ac2fd1fb87e167a19285b9eefeff4a40e63c87cbfbeab36/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573", size = 250562, upload-time = "2026-08-15T08:17:27.971Z" }, + { url = "https://files.pythonhosted.org/packages/62/16/46556278c2168d12df9da7fede5dc6fc70e60301b26a82bbeec238c9cfe3/charset_normalizer-3.5.1-cp312-cp312-win32.whl", hash = "sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2", size = 178507, upload-time = "2026-08-15T08:17:29.277Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7a/4c6c298171e6b3e745633180ff59350fc0ca0db1ffd28df1e369e0579f71/charset_normalizer-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2", size = 200551, upload-time = "2026-08-15T08:17:30.668Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d7/eb95a042f0dd22e304b0b6472b154f3546a1a039a9ee89ccb2a7f61591fc/charset_normalizer-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a", size = 180700, upload-time = "2026-08-15T08:17:32.028Z" }, + { url = "https://files.pythonhosted.org/packages/5b/97/fb4e82231aba271ffd775a1b4993b0defc4e3059f286ae41d9433409fe85/charset_normalizer-3.5.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2", size = 331467, upload-time = "2026-08-15T08:19:50.959Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2f/fe3f187327aac18e2d54e9d2b08e15d27bf9b642d9e51c219f130fc34d1a/charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99", size = 253057, upload-time = "2026-08-15T08:19:52.654Z" }, + { url = "https://files.pythonhosted.org/packages/d7/c7/9e48cee5c161fe24da823b61bf381921d77cb994a0a4de148e95018c1984/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2", size = 240930, upload-time = "2026-08-15T08:19:54.163Z" }, + { url = "https://files.pythonhosted.org/packages/49/e0/716601f3cc69be7b198951150c75ead1ece33c3c8036ff6ffa46029659a0/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235", size = 230822, upload-time = "2026-08-15T08:19:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/d3/05/71bfc5caa0abcc45aea1f6a4d50ac68e59605ddc7666fe8494f4cd229665/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598", size = 260037, upload-time = "2026-08-15T08:19:57.312Z" }, + { url = "https://files.pythonhosted.org/packages/c3/92/de7e32ed05341e7a9c4c877c318418197b7f2d66a3b68d561bf2ac57ca3e/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96", size = 255097, upload-time = "2026-08-15T08:19:59.056Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7b/ade0a122600319dfa0b1000ab0f9731c94a817904cf3c5de408c73a4ede7/charset_normalizer-3.5.1-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962", size = 250166, upload-time = "2026-08-15T08:20:00.612Z" }, + { url = "https://files.pythonhosted.org/packages/75/9c/019fbb9f4834491a160951349b1a3714439376f66e5f7cf18b4f18f0c7aa/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3", size = 241821, upload-time = "2026-08-15T08:20:02.321Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/11d4840bfc99330cc7fbcc2681ee5a044553a6e77655508d8f9b2bff7b34/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950", size = 232529, upload-time = "2026-08-15T08:20:04.008Z" }, + { url = "https://files.pythonhosted.org/packages/18/96/2b3a21492d9f65171ac75d872f5018260013d00bfa0ff70ec9f179148cbd/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8", size = 260348, upload-time = "2026-08-15T08:20:05.877Z" }, + { url = "https://files.pythonhosted.org/packages/d6/aa/a69a2028e8bd052476c245460ab19d7de595de084dd968f2d75cd50c3e25/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031", size = 247234, upload-time = "2026-08-15T08:20:07.487Z" }, + { url = "https://files.pythonhosted.org/packages/35/8a/3d130aeabcaf3d2466af76b7b141c08d9e89c9016ab4b7cdd0f7dc2d1c62/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072", size = 256917, upload-time = "2026-08-15T08:20:09.142Z" }, + { url = "https://files.pythonhosted.org/packages/80/c2/a7379b840292d0c1ab9fbd17d1f3967aa81794dc95bc74be8999d7fedcf7/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d", size = 254846, upload-time = "2026-08-15T08:20:10.727Z" }, + { url = "https://files.pythonhosted.org/packages/01/65/d43b714731bb2f40d4053dfa00ecfc1c5a301f8e3316c5db3a09af59fe94/charset_normalizer-3.5.1-cp37-abi3-win32.whl", hash = "sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc", size = 174216, upload-time = "2026-08-15T08:20:12.334Z" }, + { url = "https://files.pythonhosted.org/packages/35/4f/b911ed898b26a09789eba9c9200c999aff6c61b4bafaf4838e56d1a1e1a3/charset_normalizer-3.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959", size = 199764, upload-time = "2026-08-15T08:20:13.908Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a7/920baf467bfd9bf689f3b318340f37aee4572a71f162bd8db51da55ba4fa/charset_normalizer-3.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e", size = 287318, upload-time = "2026-08-15T08:20:15.551Z" }, + { url = "https://files.pythonhosted.org/packages/cc/61/d01fc49b8dea277640b55a9e15960dbca9fdc8c9fde18e572d39c59f4019/charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6", size = 68658, upload-time = "2026-08-15T08:20:43.306Z" }, ] [[package]] name = "cliff" -version = "4.12.0" +version = "4.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "autopage" }, @@ -161,25 +190,24 @@ dependencies = [ { name = "pyyaml" }, { name = "stevedore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/11/f1/e07262a8812f2553239087f65d4948883234561d2ea90a22262352be721d/cliff-4.12.0.tar.gz", hash = "sha256:3fa9f45f8b216fabbea2a4322d7b0704221455f4ce87492393925cabba743dc6", size = 86665, upload-time = "2025-11-20T15:00:52.597Z" } +sdist = { url = "https://files.pythonhosted.org/packages/83/4a/908e0d2a7d81e3a199c24b8bf787670ba4ec15105785ff3834f1d4e16a84/cliff-4.16.0.tar.gz", hash = "sha256:85314ad49bd62f90a51094d4e31b1cb4d3b92fb83cb3935eeba0236c32839e75", size = 90041, upload-time = "2026-08-24T14:04:07.949Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/63/81/3b9a10658286b4c6564d570862cf16733fa28b53cbe0e6f6ddb9e3594530/cliff-4.12.0-py3-none-any.whl", hash = "sha256:952c84dad995018da1515cb980acb08bdc6b627124ec94d288a0ccdbbab52080", size = 84624, upload-time = "2025-11-20T15:00:51.327Z" }, + { url = "https://files.pythonhosted.org/packages/3a/05/4b62f48717953851b643340922c4173ad176290659d9a4a43ef0d7617e5d/cliff-4.16.0-py3-none-any.whl", hash = "sha256:6739e39df0b0c9424787964141c60eee9a533ed0bc3c364857c716fcb666f871", size = 86908, upload-time = "2026-08-24T14:04:06.859Z" }, ] [[package]] name = "cmd2" -version = "2.7.0" +version = "4.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "gnureadline", marker = "sys_platform == 'darwin'" }, + { name = "prompt-toolkit" }, { name = "pyperclip" }, - { name = "pyreadline3", marker = "sys_platform == 'win32'" }, + { name = "rich" }, { name = "rich-argparse" }, - { name = "wcwidth" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/75/68/4bf43d284e41c01c6011146e5c2824aa6f17a3bb1ef10ba3dbbae5cf31dc/cmd2-2.7.0.tar.gz", hash = "sha256:81d8135b46210e1d03a5a810baf859069a62214788ceeec3588f44eed86fbeeb", size = 593131, upload-time = "2025-06-30T16:54:26.983Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/6f/204e9f84b28fb30561403dd854aa4f3f26f530a45598a6b7d5f2e192b961/cmd2-4.2.1.tar.gz", hash = "sha256:89021603a18ceb41600d2f02b40c5df4d37e411027308e9a58714be1f3a56b6b", size = 854163, upload-time = "2026-08-22T20:16:58.41Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/83/0f65933b7daa436912173f3d63232d158b60686318fccc7cf458ff15bfe8/cmd2-2.7.0-py3-none-any.whl", hash = "sha256:c85faf603e8cfeb4302206f49c0530a83d63386b0d90ff6a957f2c816eb767d7", size = 154309, upload-time = "2025-06-30T16:54:25.039Z" }, + { url = "https://files.pythonhosted.org/packages/7b/50/ea1614261e88b649e0186a3038bad6b948777ba70833002db4c4b0b3cbdb/cmd2-4.2.1-py3-none-any.whl", hash = "sha256:1b4be8851cbc473cdb4f459af5acb0a53ef174b16f5918a378ee8a29f51efcbb", size = 193600, upload-time = "2026-08-22T20:16:56.158Z" }, ] [[package]] @@ -193,98 +221,97 @@ wheels = [ [[package]] name = "cotyledon" -version = "2.1.0" +version = "2.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "setproctitle", marker = "sys_platform != 'win32'" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bb/15/4f47ad3c352568d5adf9aec06279b1fa3a12a02e02da343dfae0512f2403/cotyledon-2.1.0.tar.gz", hash = "sha256:ddf5d3639efd10789c9708a89890e55529b860986e7ca7d602a13c9eb15d8709", size = 26754, upload-time = "2025-08-30T18:56:04.583Z" } +sdist = { url = "https://files.pythonhosted.org/packages/de/9e/e7856beede7f8e6c5616ea0b692b279295febbdf301abb48dcd3b05424fe/cotyledon-2.2.0.tar.gz", hash = "sha256:5f9dcc4cfb8124e6550119bb4f6fbda8628bbe653804015b76c392fa5f208009", size = 29077, upload-time = "2025-12-23T14:52:54.415Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dd/14/baf084e49f818049e111bd221075aa54caa71be134052e880c989efd55ad/cotyledon-2.1.0-py3-none-any.whl", hash = "sha256:6b32f069300832b24a3e1a69ced003d8ed725549e38a8a98adb342864adf17cf", size = 25326, upload-time = "2025-08-30T18:56:03.247Z" }, + { url = "https://files.pythonhosted.org/packages/09/33/fb4a45f4bdde90934955a7c4244d4ffbedccaeaaa838f29714b45a2112e2/cotyledon-2.2.0-py3-none-any.whl", hash = "sha256:130c04c339a2f94f482135c10dcfc83c7f7b4daca3c938a6101578b658b75017", size = 28566, upload-time = "2025-12-23T14:52:53.198Z" }, ] [[package]] name = "coverage" -version = "7.12.0" +version = "7.15.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/89/26/4a96807b193b011588099c3b5c89fbb05294e5b90e71018e065465f34eb6/coverage-7.12.0.tar.gz", hash = "sha256:fc11e0a4e372cb5f282f16ef90d4a585034050ccda536451901abfb19a57f40c", size = 819341, upload-time = "2025-11-18T13:34:20.766Z" } +sdist = { url = "https://files.pythonhosted.org/packages/be/c3/4f2195f512fb172aa425a8803a874b2baa9ba7f80ff7b6080998761fc701/coverage-7.15.4.tar.gz", hash = "sha256:0548198fff07ccf4faf469520bce1c2eceb1ce3e62891921138dec10907f9d00", size = 936952, upload-time = "2026-08-06T13:50:24.442Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/bf/638c0427c0f0d47638242e2438127f3c8ee3cfc06c7fdeb16778ed47f836/coverage-7.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:29644c928772c78512b48e14156b81255000dcfd4817574ff69def189bcb3647", size = 217704, upload-time = "2025-11-18T13:32:28.906Z" }, - { url = "https://files.pythonhosted.org/packages/08/e1/706fae6692a66c2d6b871a608bbde0da6281903fa0e9f53a39ed441da36a/coverage-7.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8638cbb002eaa5d7c8d04da667813ce1067080b9a91099801a0053086e52b736", size = 218064, upload-time = "2025-11-18T13:32:30.161Z" }, - { url = "https://files.pythonhosted.org/packages/a9/8b/eb0231d0540f8af3ffda39720ff43cb91926489d01524e68f60e961366e4/coverage-7.12.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:083631eeff5eb9992c923e14b810a179798bb598e6a0dd60586819fc23be6e60", size = 249560, upload-time = "2025-11-18T13:32:31.835Z" }, - { url = "https://files.pythonhosted.org/packages/e9/a1/67fb52af642e974d159b5b379e4d4c59d0ebe1288677fbd04bbffe665a82/coverage-7.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:99d5415c73ca12d558e07776bd957c4222c687b9f1d26fa0e1b57e3598bdcde8", size = 252318, upload-time = "2025-11-18T13:32:33.178Z" }, - { url = "https://files.pythonhosted.org/packages/41/e5/38228f31b2c7665ebf9bdfdddd7a184d56450755c7e43ac721c11a4b8dab/coverage-7.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e949ebf60c717c3df63adb4a1a366c096c8d7fd8472608cd09359e1bd48ef59f", size = 253403, upload-time = "2025-11-18T13:32:34.45Z" }, - { url = "https://files.pythonhosted.org/packages/ec/4b/df78e4c8188f9960684267c5a4897836f3f0f20a20c51606ee778a1d9749/coverage-7.12.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d907ddccbca819afa2cd014bc69983b146cca2735a0b1e6259b2a6c10be1e70", size = 249984, upload-time = "2025-11-18T13:32:35.747Z" }, - { url = "https://files.pythonhosted.org/packages/ba/51/bb163933d195a345c6f63eab9e55743413d064c291b6220df754075c2769/coverage-7.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b1518ecbad4e6173f4c6e6c4a46e49555ea5679bf3feda5edb1b935c7c44e8a0", size = 251339, upload-time = "2025-11-18T13:32:37.352Z" }, - { url = "https://files.pythonhosted.org/packages/15/40/c9b29cdb8412c837cdcbc2cfa054547dd83affe6cbbd4ce4fdb92b6ba7d1/coverage-7.12.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51777647a749abdf6f6fd8c7cffab12de68ab93aab15efc72fbbb83036c2a068", size = 249489, upload-time = "2025-11-18T13:32:39.212Z" }, - { url = "https://files.pythonhosted.org/packages/c8/da/b3131e20ba07a0de4437a50ef3b47840dfabf9293675b0cd5c2c7f66dd61/coverage-7.12.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:42435d46d6461a3b305cdfcad7cdd3248787771f53fe18305548cba474e6523b", size = 249070, upload-time = "2025-11-18T13:32:40.598Z" }, - { url = "https://files.pythonhosted.org/packages/70/81/b653329b5f6302c08d683ceff6785bc60a34be9ae92a5c7b63ee7ee7acec/coverage-7.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5bcead88c8423e1855e64b8057d0544e33e4080b95b240c2a355334bb7ced937", size = 250929, upload-time = "2025-11-18T13:32:42.915Z" }, - { url = "https://files.pythonhosted.org/packages/a3/00/250ac3bca9f252a5fb1338b5ad01331ebb7b40223f72bef5b1b2cb03aa64/coverage-7.12.0-cp312-cp312-win32.whl", hash = "sha256:dcbb630ab034e86d2a0f79aefd2be07e583202f41e037602d438c80044957baa", size = 220241, upload-time = "2025-11-18T13:32:44.665Z" }, - { url = "https://files.pythonhosted.org/packages/64/1c/77e79e76d37ce83302f6c21980b45e09f8aa4551965213a10e62d71ce0ab/coverage-7.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:2fd8354ed5d69775ac42986a691fbf68b4084278710cee9d7c3eaa0c28fa982a", size = 221051, upload-time = "2025-11-18T13:32:46.008Z" }, - { url = "https://files.pythonhosted.org/packages/31/f5/641b8a25baae564f9e52cac0e2667b123de961985709a004e287ee7663cc/coverage-7.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:737c3814903be30695b2de20d22bcc5428fdae305c61ba44cdc8b3252984c49c", size = 219692, upload-time = "2025-11-18T13:32:47.372Z" }, - { url = "https://files.pythonhosted.org/packages/ce/a3/43b749004e3c09452e39bb56347a008f0a0668aad37324a99b5c8ca91d9e/coverage-7.12.0-py3-none-any.whl", hash = "sha256:159d50c0b12e060b15ed3d39f87ed43d4f7f7ad40b8a534f4dd331adbb51104a", size = 209503, upload-time = "2025-11-18T13:34:18.892Z" }, + { url = "https://files.pythonhosted.org/packages/1d/48/bc8d4ba7b37551a767bd863f15b3f80182b271c2f55975356f5f7dbe94c2/coverage-7.15.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4fedd1f7f428f9fe83b1ead5e7cc87a43427be31aadafbac3ac0636dc7abb22", size = 222543, upload-time = "2026-08-06T13:47:37.562Z" }, + { url = "https://files.pythonhosted.org/packages/20/dd/88d6f83f1fffc974a3691a34a97951c5b12df7512a6782c5963883cbc058/coverage-7.15.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:37e2f0cdf58e2e1fed4e4d5a8f8786ae2f7eb80b478016876667dc4a01d60a97", size = 222905, upload-time = "2026-08-06T13:47:38.927Z" }, + { url = "https://files.pythonhosted.org/packages/bd/5c/54ee0d4748585bb0acab9891cd8d92f2d3593165b4e59fc9de113bfb3140/coverage-7.15.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fb55d0e70bb15f2e81477613627286581414693d74ac7963c93a790dd453ca9d", size = 254407, upload-time = "2026-08-06T13:47:40.488Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3f/f0642a372f494bd0d7dad3b497083b910194a5f1c88be2c94fef707c3b59/coverage-7.15.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:899b9da30f3c6c336566e3707495bb23e8302d39d862f01fa78c48b99b9437e2", size = 257145, upload-time = "2026-08-06T13:47:41.931Z" }, + { url = "https://files.pythonhosted.org/packages/71/17/8b46d0ed68251016002ec972c8fc0119961a765d0984cafb8bf317c43758/coverage-7.15.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d15715e8c46552827e5e4f30a35575a2dbcad14454cf3284c54483946bd16931", size = 258257, upload-time = "2026-08-06T13:47:43.527Z" }, + { url = "https://files.pythonhosted.org/packages/30/b8/8498a0e72d0adbe15477dd07463d2b3bb2c9f6a4815e8589e50939e2c3ae/coverage-7.15.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:002a438859f7b430bc99afeaf01a6d187dad1d0dc907b64cdeffc632a5db8fd8", size = 260517, upload-time = "2026-08-06T13:47:45.121Z" }, + { url = "https://files.pythonhosted.org/packages/41/e1/7dce19c3bdb1e3dd63e769508216500edad81bd5f69a26d724e32aceaf78/coverage-7.15.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e4193a04b518f7968f3099755f5509ee7cccc6dc2b92a6b14841934d22e222c9", size = 254785, upload-time = "2026-08-06T13:47:46.541Z" }, + { url = "https://files.pythonhosted.org/packages/dd/b1/e1494703c675a2561723cd9b89f45c9168782c31280c611b1f767851e57c/coverage-7.15.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e98dcc55d572b38e69d117da7e8e8efb8500f1f5eaf81ecd460a63220790b839", size = 256176, upload-time = "2026-08-06T13:47:48.155Z" }, + { url = "https://files.pythonhosted.org/packages/73/76/a5629d270fb638a43a4b10466f51e2f49d532c1aa4da2913cbbb150bbe0a/coverage-7.15.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:af6c538498ce66c10d3fd541c2a8d5b03da5850355add34e6cba564210cb9e72", size = 254321, upload-time = "2026-08-06T13:47:49.757Z" }, + { url = "https://files.pythonhosted.org/packages/ff/4f/9c44447218435d5766b911534f9d798144a5560f85e9a54ebe5f3f5d19f9/coverage-7.15.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1d10025d96ea89fc2f73714dbc4cbd433fe012c1ac9e23f895d7728b238b6e52", size = 258390, upload-time = "2026-08-06T13:47:51.248Z" }, + { url = "https://files.pythonhosted.org/packages/de/36/c1e127616fb3fa18a9ff71e76c417f2fd7424332a4870015ac224ef4c039/coverage-7.15.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d802e1947603162ded419bff83ac7489820355d2b856dfb09206574e3a37ac0c", size = 253894, upload-time = "2026-08-06T13:47:52.816Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b9/fdb92c8ae7a8bb9b850cc253b7b3b9c8526f68130002048b5671cd510d09/coverage-7.15.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c2de40895718f91951b86712b4c5b694acaf9a0a49be13874896f599a1eed3f4", size = 255763, upload-time = "2026-08-06T13:47:54.296Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/a7d51b2587c7bdb76e71b0896d2565bf7d60436b5122fc83e511adb1f7cd/coverage-7.15.4-cp312-cp312-win32.whl", hash = "sha256:5c3431b2161279b7db5c2a1aa58ae02e5cb8c3c42d93a5094be3f5537bd5b11b", size = 224597, upload-time = "2026-08-06T13:47:56.074Z" }, + { url = "https://files.pythonhosted.org/packages/49/b9/5c5f80cc55f5acaaca6dee677626bfcec8c87204a7809b438b08e84f4571/coverage-7.15.4-cp312-cp312-win_amd64.whl", hash = "sha256:6befeab5fb2b51c958ca4ac6c5d141a1e8240f4f76e46350f1911963deda49cd", size = 225135, upload-time = "2026-08-06T13:47:57.52Z" }, + { url = "https://files.pythonhosted.org/packages/47/e4/2a4561f89ff6bf7c925c287d0f2cce8bdf139c3a33735c87e3203401cf94/coverage-7.15.4-cp312-cp312-win_arm64.whl", hash = "sha256:67bc345491ab55b837277d76f5775d057e8c7f1ac44d890d8c2c82adde258c6f", size = 224515, upload-time = "2026-08-06T13:47:58.977Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d9/e70c286c979378f061d8266e279b686ab0b0b688e1fe0af864684f23a77d/coverage-7.15.4-py3-none-any.whl", hash = "sha256:964730a1e9de9c0cf11be6a1a3c79ce419c34882842abd256086ba4698705e84", size = 214332, upload-time = "2026-08-06T13:50:22.192Z" }, ] [[package]] name = "cryptography" -version = "46.0.3" +version = "50.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" }, - { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, - { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, - { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, - { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, - { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, - { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, - { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, - { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, - { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, - { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, - { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, - { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" }, - { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" }, - { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" }, - { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" }, - { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, - { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, - { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, - { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, - { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, - { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, - { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, - { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, - { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, - { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, - { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" }, - { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" }, - { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/5c/59086b4aac5e879d38ddbcf74e4be7ade89cebc3eb199a55da998c3bb46a/cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03", size = 4001252, upload-time = "2026-07-31T14:23:33.331Z" }, + { url = "https://files.pythonhosted.org/packages/57/ef/8f2df13c7216bcad3e1c74e07f6e193d93e998e114f524a53877c9af27ad/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645", size = 4719554, upload-time = "2026-07-31T14:23:35.611Z" }, + { url = "https://files.pythonhosted.org/packages/d9/41/029086c34d91052fc3b88bcc8056f709a7c915c7a23b235a54eb800b1c97/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7", size = 4702130, upload-time = "2026-07-31T14:23:37.635Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ff/b6ce0954962e7f7b969f850a883744197bb3910bdfd7b6da162eab7d9f68/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3", size = 4725244, upload-time = "2026-07-31T14:23:39.471Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/63a1027cb7fec360a182208e1b7767d5aa1fe57be3d6aa856e69a321edc0/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f", size = 5342265, upload-time = "2026-07-31T14:23:41.286Z" }, + { url = "https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae", size = 4734609, upload-time = "2026-07-31T14:23:43.139Z" }, + { url = "https://files.pythonhosted.org/packages/15/37/36a9c479bbe49acea2636c7fd3360d20f7b7e079c300352011c44850b181/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a", size = 4356517, upload-time = "2026-07-31T14:23:44.939Z" }, + { url = "https://files.pythonhosted.org/packages/32/98/8a151d64367204cbc63ec65d37502f1d9c53cf4bfc6ec3c532614dbec60d/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987", size = 4724529, upload-time = "2026-07-31T14:23:46.93Z" }, + { url = "https://files.pythonhosted.org/packages/22/f6/ec13b470172126464a86bf54d2294a46d29837fc51ba3e45d4047946fb5e/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169", size = 5299852, upload-time = "2026-07-31T14:23:48.851Z" }, + { url = "https://files.pythonhosted.org/packages/da/3a/f05e32c99d440c9bb891ea0e36c9091891e36be5a9a87ab2ee6ea20729f6/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f", size = 4734462, upload-time = "2026-07-31T14:23:50.861Z" }, + { url = "https://files.pythonhosted.org/packages/ca/dc/bd72b26be8953f80625f63151efd38eee71c76ca6cf591c08ff34615a79e/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105", size = 4852708, upload-time = "2026-07-31T14:23:52.715Z" }, + { url = "https://files.pythonhosted.org/packages/27/20/c930314a2ab476d15dec966ec87e2e9637bb02b06106b12c0396c57bb603/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef", size = 5004179, upload-time = "2026-07-31T14:23:54.887Z" }, + { url = "https://files.pythonhosted.org/packages/32/2e/c9db68a0c4bfa28e310707527c0ee3a2bd254104d2e02e68f368e197aa4c/cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30", size = 3840395, upload-time = "2026-07-31T14:23:56.677Z" }, + { url = "https://files.pythonhosted.org/packages/03/37/73d005be173aff344af30e9fd2a576575cb2391a7101d9cd3842e1fa8cce/cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07", size = 4036009, upload-time = "2026-07-31T14:24:24.122Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c6/7a6202a534e32103a285b7834a120869557fe198d51d7cfe59754c8bda9c/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3", size = 4745252, upload-time = "2026-07-31T14:24:26.118Z" }, + { url = "https://files.pythonhosted.org/packages/85/4f/0fa8c2f4428198f15d9ff8d63400e27afbf94ce833f6108da1eb3753f945/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f", size = 4728939, upload-time = "2026-07-31T14:24:27.994Z" }, + { url = "https://files.pythonhosted.org/packages/d1/63/54dd723490ba2dc09b299682c10b38db38f159728bcaae8c591b8af2f22d/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5", size = 4748483, upload-time = "2026-07-31T14:24:30.254Z" }, + { url = "https://files.pythonhosted.org/packages/1d/dd/7c77d26285cc7f6991efce64a0f5b4f9383bfa5dd8c5033003eaf7db4cdb/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f", size = 5367599, upload-time = "2026-07-31T14:24:32.457Z" }, + { url = "https://files.pythonhosted.org/packages/46/c9/f60aed34c013f317f92817b6c171c2d22a78270fa41109bd4b08af26b194/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025", size = 4762647, upload-time = "2026-07-31T14:24:34.599Z" }, + { url = "https://files.pythonhosted.org/packages/be/f3/f9a0173b139372c3a48ed98154b45cc6b9de17c789d5ab552e621c293609/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a", size = 4385197, upload-time = "2026-07-31T14:24:36.647Z" }, + { url = "https://files.pythonhosted.org/packages/d8/36/83bb81f6e569bc38e1e4a7bc80f29b46bb9601920bc455fc8e888f5d5742/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b", size = 4748095, upload-time = "2026-07-31T14:24:39.493Z" }, + { url = "https://files.pythonhosted.org/packages/6b/16/d3008eff98c764979865834c3d386d4fd041b5f52e7f34fc29ac1a5eb515/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708", size = 5325948, upload-time = "2026-07-31T14:24:41.556Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f8/d97f9603efda3888187bfdb893f26c41be4735c10631d05d284ee6b047c4/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47", size = 4762400, upload-time = "2026-07-31T14:24:43.636Z" }, + { url = "https://files.pythonhosted.org/packages/64/a2/4615c8f7d81a00b1d6e6afe19f694e1543582349fb5f4076f6cb5dc36485/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9", size = 4878208, upload-time = "2026-07-31T14:24:45.522Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/57/30/4a22984d4f1bdfb8c054f07a92bc176b97a3134cc1d6c4b3bffb1f3688b4/cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba", size = 3874135, upload-time = "2026-07-31T14:24:50.085Z" }, ] [[package]] name = "debtcollector" -version = "3.0.0" +version = "3.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/31/e2/a45b5a620145937529c840df5e499c267997e85de40df27d54424a158d3c/debtcollector-3.0.0.tar.gz", hash = "sha256:2a8917d25b0e1f1d0d365d3c1c6ecfc7a522b1e9716e8a1a4a915126f7ccea6f", size = 31322, upload-time = "2024-02-22T15:39:20.674Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/57/1bbe02be744995408d944cf46b8c818cf072873064b1cd3c79c11618b216/debtcollector-3.1.0.tar.gz", hash = "sha256:278a45608cf16e79c0ae10851d869185c6b78f86610df8f27a451a18c1fec732", size = 32951, upload-time = "2026-03-24T10:07:38.202Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/ca/863ed8fa66d6f986de6ad7feccc5df96e37400845b1eeb29889a70feea99/debtcollector-3.0.0-py3-none-any.whl", hash = "sha256:46f9dacbe8ce49c47ebf2bf2ec878d50c9443dfae97cc7b8054be684e54c3e91", size = 23035, upload-time = "2024-02-22T15:39:18.99Z" }, + { url = "https://files.pythonhosted.org/packages/99/05/3f36aed56f0e1815fdc2ed4a9f2bd680a7bfe8819f21eacded2dc00fe283/debtcollector-3.1.0-py3-none-any.whl", hash = "sha256:c64e49a66c0b71289620fc2fdf89c03d740bddb20576ddd4f04ddc01da946668", size = 24408, upload-time = "2026-03-24T10:07:37.218Z" }, ] [[package]] name = "decorator" -version = "5.2.1" +version = "5.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +sdist = { url = "https://files.pythonhosted.org/packages/60/8b/32f9823da46cde7df2087faa08cd98d01b908f8dcab982cdba9c84e85355/decorator-5.3.1.tar.gz", hash = "sha256:4cbcdd55a6efadb9dbea26b858f4fb3264567b52d69ca0d25b721b553f60ea82", size = 58084, upload-time = "2026-05-18T06:03:28.057Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, + { url = "https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl", hash = "sha256:f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c", size = 10365, upload-time = "2026-05-18T06:03:26.517Z" }, ] [[package]] @@ -311,15 +338,15 @@ wheels = [ [[package]] name = "eventlet" -version = "0.40.3" +version = "0.41.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "dnspython" }, { name = "greenlet" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ce/4f/1e5227b23aa77d9ea05056b98cf0bf187cca994991060245002b640f9830/eventlet-0.40.3.tar.gz", hash = "sha256:290852db0065d78cec17a821b78c8a51cafb820a792796a354592ae4d5fceeb0", size = 565741, upload-time = "2025-08-27T09:56:16.085Z" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/e8/6a3a23a3b85ed129b21309c6eca873d833855e63879cd72b9dac20d9b76a/eventlet-0.41.2.tar.gz", hash = "sha256:721b86b77fca33a735598292022ac6feef99747bf48f52defdada6b572acd5af", size = 566270, upload-time = "2026-08-14T07:51:00.331Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/b4/981362608131dc4ee8de9fdca6a38ef19e3da66ab6a13937bd158882db91/eventlet-0.40.3-py3-none-any.whl", hash = "sha256:e681cae6ee956cfb066a966b5c0541e734cc14879bda6058024104790595ac9d", size = 364333, upload-time = "2025-08-27T09:56:10.774Z" }, + { url = "https://files.pythonhosted.org/packages/23/75/9681fa59c4b27d9e34e8be682d2fff3f5be87bf6552477f9a5d395a068c3/eventlet-0.41.2-py3-none-any.whl", hash = "sha256:6cae50e67fe6ae8bb7013e7fd4d8e0d0d20aeb9b3259b93f023c93eb6749631f", size = 364641, upload-time = "2026-08-14T07:50:58.838Z" }, ] [[package]] @@ -333,73 +360,62 @@ wheels = [ [[package]] name = "fixtures" -version = "4.2.6" +version = "4.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/12/ff/2d7d262a2ac563a3c976eae210057d68271f5080f0411792d8e2aa84d9a6/fixtures-4.2.6.tar.gz", hash = "sha256:95472b15b145063a672fbe33b1244ccff829fbec97d530d862d26f416d16c90b", size = 46938, upload-time = "2025-08-03T17:20:52.304Z" } +sdist = { url = "https://files.pythonhosted.org/packages/79/fc/da17efe78bb35b945441170363328ad11d953d69e6273bceb668b1d68c34/fixtures-4.3.2.tar.gz", hash = "sha256:b7db64014732513f23d12b0c49ae527ba0c0c1f2667ec97d9136d67157ebce5d", size = 47173, upload-time = "2026-03-25T17:25:40.51Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/45/49/d92f66a79fe0da4b55b77597a2cfb6630c131d702a6b4a87ddd2ee277c3d/fixtures-4.2.6-py3-none-any.whl", hash = "sha256:58266d3646b7bda07ed463c6fd26a1b18a0273ee02de2b3f8ddec9e6e4359239", size = 66181, upload-time = "2025-08-03T17:20:51.155Z" }, + { url = "https://files.pythonhosted.org/packages/de/58/b7efb4b7c01cefd47efb53e50c7f1eb2cdc4aff3d98268bdc46c7f628f2c/fixtures-4.3.2-py3-none-any.whl", hash = "sha256:c71ad55a9139bd20b3265c1d44c1a67fefb9efdddd79614096f538de63fbf4d8", size = 41625, upload-time = "2026-03-25T17:25:38.898Z" }, ] [[package]] name = "futurist" -version = "3.2.1" +version = "3.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "debtcollector" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/af/12/786f4aaf9d396d67b1b7b90f248ff994e916605d0751d08a0344a4a785a6/futurist-3.2.1.tar.gz", hash = "sha256:01dd4f30acdfbb2e2eb6091da565eded82d8cbaf6c48a36cc7f73c11cfa7fb3f", size = 49326, upload-time = "2025-08-29T15:06:57.733Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/82/eb917bf6aa03197ac5bb3ce8a95bee2243a938426ec60747a52cbed9344e/futurist-3.5.0.tar.gz", hash = "sha256:97d01f6144bbe3abe1b2155cb2b42dab4ec33e09f4f7f7df7569f35d9a29aaf7", size = 61471, upload-time = "2026-08-20T15:20:26.774Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/5b/a4418215b594fa44dea7deae61fa406139e2e8acc6442d25f93d80c52c84/futurist-3.2.1-py3-none-any.whl", hash = "sha256:c76a1e7b2c6b264666740c3dffbdcf512bd9684b4b253a3068a0135b43729745", size = 40485, upload-time = "2025-08-29T15:06:56.476Z" }, -] - -[[package]] -name = "gnureadline" -version = "8.2.13" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cb/92/20723aa239b9a8024e6f8358c789df8859ab1085a1ae106e5071727ad20f/gnureadline-8.2.13.tar.gz", hash = "sha256:c9b9e1e7ba99a80bb50c12027d6ce692574f77a65bf57bc97041cf81c0f49bd1", size = 3224991, upload-time = "2024-10-18T14:03:11.727Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/68/bd/df8fd060e43efd3dbdd3b210bf558ce3ef854843cd093f910f4115ebe2e9/gnureadline-8.2.13-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9c152a82613fa012ab4331bb9a0ffddb415e37561d376b910bf9e7d535607faf", size = 160504, upload-time = "2024-10-18T14:03:49.725Z" }, - { url = "https://files.pythonhosted.org/packages/97/ee/322e5340c8cdfa40e71bd0485a82404ad4cf9aed2260cca090f3c1a3a032/gnureadline-8.2.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85e362d2d0e85e45f0affae7bbfaf998b00167c55a78d31ee0f214de9ff429d2", size = 162380, upload-time = "2024-10-18T14:03:53.129Z" }, + { url = "https://files.pythonhosted.org/packages/f4/64/14c24c98b7ab804bef1bdcbbe0b6f11510366b98faba34a9cedefa309582/futurist-3.5.0-py3-none-any.whl", hash = "sha256:77c37e074fb94e2c7cc748b1b711ab4b998bf30aa76c91fbaed0206d14dd8a96", size = 50730, upload-time = "2026-08-20T15:20:25.783Z" }, ] [[package]] name = "greenlet" -version = "3.2.4" +version = "3.5.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/03/b8/704d753a5a45507a7aab61f18db9509302ed3d0a27ac7e0359ec2905b1a6/greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", size = 188260, upload-time = "2025-08-07T13:24:33.51Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/d8/7cc97c142388aef03f622e001c572c4f84e9252a439549d483f555771970/greenlet-3.5.5.tar.gz", hash = "sha256:adb4bae02e91a8e863e48b177e4014bdcac8a6b5e047ea1df687a61534b85e6c", size = 207585, upload-time = "2026-08-10T15:09:36.136Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/44/69/9b804adb5fd0671f367781560eb5eb586c4d495277c93bde4307b9e28068/greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", size = 274079, upload-time = "2025-08-07T13:15:45.033Z" }, - { url = "https://files.pythonhosted.org/packages/46/e9/d2a80c99f19a153eff70bc451ab78615583b8dac0754cfb942223d2c1a0d/greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", size = 640997, upload-time = "2025-08-07T13:42:56.234Z" }, - { url = "https://files.pythonhosted.org/packages/3b/16/035dcfcc48715ccd345f3a93183267167cdd162ad123cd93067d86f27ce4/greenlet-3.2.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968", size = 655185, upload-time = "2025-08-07T13:45:27.624Z" }, - { url = "https://files.pythonhosted.org/packages/31/da/0386695eef69ffae1ad726881571dfe28b41970173947e7c558d9998de0f/greenlet-3.2.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9", size = 649926, upload-time = "2025-08-07T13:53:15.251Z" }, - { url = "https://files.pythonhosted.org/packages/68/88/69bf19fd4dc19981928ceacbc5fd4bb6bc2215d53199e367832e98d1d8fe/greenlet-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6", size = 651839, upload-time = "2025-08-07T13:18:30.281Z" }, - { url = "https://files.pythonhosted.org/packages/19/0d/6660d55f7373b2ff8152401a83e02084956da23ae58cddbfb0b330978fe9/greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", size = 607586, upload-time = "2025-08-07T13:18:28.544Z" }, - { url = "https://files.pythonhosted.org/packages/8e/1a/c953fdedd22d81ee4629afbb38d2f9d71e37d23caace44775a3a969147d4/greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", size = 1123281, upload-time = "2025-08-07T13:42:39.858Z" }, - { url = "https://files.pythonhosted.org/packages/3f/c7/12381b18e21aef2c6bd3a636da1088b888b97b7a0362fac2e4de92405f97/greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f", size = 1151142, upload-time = "2025-08-07T13:18:22.981Z" }, - { url = "https://files.pythonhosted.org/packages/27/45/80935968b53cfd3f33cf99ea5f08227f2646e044568c9b1555b58ffd61c2/greenlet-3.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee7a6ec486883397d70eec05059353b8e83eca9168b9f3f9a361971e77e0bcd0", size = 1564846, upload-time = "2025-11-04T12:42:15.191Z" }, - { url = "https://files.pythonhosted.org/packages/69/02/b7c30e5e04752cb4db6202a3858b149c0710e5453b71a3b2aec5d78a1aab/greenlet-3.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:326d234cbf337c9c3def0676412eb7040a35a768efc92504b947b3e9cfc7543d", size = 1633814, upload-time = "2025-11-04T12:42:17.175Z" }, - { url = "https://files.pythonhosted.org/packages/e9/08/b0814846b79399e585f974bbeebf5580fbe59e258ea7be64d9dfb253c84f/greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02", size = 299899, upload-time = "2025-08-07T13:38:53.448Z" }, + { url = "https://files.pythonhosted.org/packages/2e/7e/9ecd0285e3153532ae07aeb88063c43c72b4221cf0d4d123b02f3682e3ff/greenlet-3.5.5-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:49520f0c95a48b42cf55414b8e8479beb274ea70431afc33e3f79903c71f4380", size = 295809, upload-time = "2026-08-10T13:25:34.023Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/60e4bbcc89252037b18087f2ec16405d5b2d5be42dde191bbf3667e96102/greenlet-3.5.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55272212cbc5f43d1d723725ab931f1939969b7e9523882ca58b55061769d053", size = 611910, upload-time = "2026-08-10T14:14:35.18Z" }, + { url = "https://files.pythonhosted.org/packages/a4/17/cd5134be659cd4a443e7a61ae670dabec165a814c51162916d637b6dd38e/greenlet-3.5.5-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655bca754a2ef4efcb0eb48a94d3f4593536d0f3d48f8ed44343c01d16a92f95", size = 624198, upload-time = "2026-08-10T14:27:25.229Z" }, + { url = "https://files.pythonhosted.org/packages/9b/30/87c212b5c684d0e72974f1063b7a9687631e8985902c06e1016542c874e7/greenlet-3.5.5-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ca5d6ae0739e5764f2cfcfaa562ac5a990cbdaedca93251c5e3cf07c362371f", size = 629504, upload-time = "2026-08-10T14:30:07.967Z" }, + { url = "https://files.pythonhosted.org/packages/78/ac/5c5b959999b6f09c3026b5dfe171575bc3121c5236ce74f495096f25b203/greenlet-3.5.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:147b25a42e5ca5be3d42356e8f608b37af715a1c196e9bf9d1627f3341adfe1d", size = 621439, upload-time = "2026-08-10T13:40:49.391Z" }, + { url = "https://files.pythonhosted.org/packages/63/2c/eb487fafc9f50ffff2b1e0b697f70fb34bf150821c08ab225aacf5583a7e/greenlet-3.5.5-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:1b5ed9162c0c098e0bbc2cf88a94f433c1b8926f831745252e099e5d83e17759", size = 432462, upload-time = "2026-08-10T14:30:02.309Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8b/6acf112ed8aee499f25b4d6949820fb02ac950ff9c1f3d793bd5be0599f2/greenlet-3.5.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:27493374cff1d1b7919dc8126547f2aea582737e3046147b434b1e12de56389b", size = 1581342, upload-time = "2026-08-10T14:15:05.653Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d7/734e5f198888876b42d7616ff6644c075baf6b8a2412deadd6b0e1b8b20c/greenlet-3.5.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:12e2ee66c2aba86133f10fd99d6a8856c6d351ffb7be0e4d52ef2cc5fbb705b2", size = 1645744, upload-time = "2026-08-10T13:40:30.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/30/1f42b88dc587b5899ee50616ad56ee40cafaf225df4fb829f10183c62a5c/greenlet-3.5.5-cp312-cp312-win_amd64.whl", hash = "sha256:49ddacd36af37735fab103846f4ee4d18a492dde72730d1699c0c8ebe30d9f18", size = 324171, upload-time = "2026-08-10T13:28:44.472Z" }, + { url = "https://files.pythonhosted.org/packages/76/e5/4dee4d8d2e603fe5fdd7b444e63219f7b9bd852c60c6214511c7157cbe88/greenlet-3.5.5-cp312-cp312-win_arm64.whl", hash = "sha256:5f1b1ff4828cdc1aba4266aff814085d04a1d07959287219af021b838b265d52", size = 308362, upload-time = "2026-08-10T13:26:46.839Z" }, ] [[package]] name = "httplib2" -version = "0.31.0" +version = "0.32.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyparsing" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/52/77/6653db69c1f7ecfe5e3f9726fdadc981794656fcd7d98c4209fecfea9993/httplib2-0.31.0.tar.gz", hash = "sha256:ac7ab497c50975147d4f7b1ade44becc7df2f8954d42b38b3d69c515f531135c", size = 250759, upload-time = "2025-09-11T12:16:03.403Z" } +sdist = { url = "https://files.pythonhosted.org/packages/84/f5/ccf58de92d61e3ad921119668f54ed36ca1d0cf5dcc5c1657dfb164fd78b/httplib2-0.32.0.tar.gz", hash = "sha256:48a0ef30a42db65d8f3399045e1d09ab0ba66e3b9efc360d07f80ea55d286025", size = 254283, upload-time = "2026-06-26T10:13:56.265Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/a2/0d269db0f6163be503775dc8b6a6fa15820cc9fdc866f6ba608d86b721f2/httplib2-0.31.0-py3-none-any.whl", hash = "sha256:b9cd78abea9b4e43a7714c6e0f8b6b8561a6fc1e95d5dbd367f5bf0ef35f5d24", size = 91148, upload-time = "2025-09-11T12:16:01.803Z" }, + { url = "https://files.pythonhosted.org/packages/33/a0/550eec327e5f5c7b732531c489f5307efec41f047b0d703bd4ca1e5ad2db/httplib2-0.32.0-py3-none-any.whl", hash = "sha256:dc6705cacdf3fb0a2aba7629fa33c90fd93e30035db0c157325826be177e4816", size = 93148, upload-time = "2026-06-26T10:13:54.985Z" }, ] [[package]] name = "idna" -version = "3.11" +version = "3.19" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15", size = 215237, upload-time = "2026-08-18T05:14:24.27Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" }, ] [[package]] @@ -413,11 +429,11 @@ wheels = [ [[package]] name = "invoke" -version = "2.2.1" +version = "3.0.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/de/bd/b461d3424a24c80490313fd77feeb666ca4f6a28c7e72713e3d9095719b4/invoke-2.2.1.tar.gz", hash = "sha256:515bf49b4a48932b79b024590348da22f39c4942dff991ad1fb8b8baea1be707", size = 304762, upload-time = "2025-10-11T00:36:35.172Z" } +sdist = { url = "https://files.pythonhosted.org/packages/33/f6/227c48c5fe47fa178ccf1fda8f047d16c97ba926567b661e9ce2045c600c/invoke-3.0.3.tar.gz", hash = "sha256:437b6a622223824380bfb4e64f612711a6b648c795f565efc8625af66fb57f0c", size = 343419, upload-time = "2026-04-07T15:17:48.307Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/4b/b99e37f88336009971405cbb7630610322ed6fbfa31e1d7ab3fbf3049a2d/invoke-2.2.1-py3-none-any.whl", hash = "sha256:2413bc441b376e5cd3f55bb5d364f973ad8bdd7bf87e53c79de3c11bf3feecc8", size = 160287, upload-time = "2025-10-11T00:36:33.703Z" }, + { url = "https://files.pythonhosted.org/packages/5a/de/bbc12563bbf979618d17625a4e753ff7a078523e28d870d3626daa97261a/invoke-3.0.3-py3-none-any.whl", hash = "sha256:f11327165e5cbb89b2ad1d88d3292b5113332c43b8553b494da435d6ec6f5053", size = 160958, upload-time = "2026-04-07T15:17:46.875Z" }, ] [[package]] @@ -443,11 +459,11 @@ wheels = [ [[package]] name = "jmespath" -version = "1.0.1" +version = "1.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843, upload-time = "2022-06-17T18:00:12.224Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" }, + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, ] [[package]] @@ -464,16 +480,16 @@ wheels = [ [[package]] name = "jsonpointer" -version = "3.0.0" +version = "3.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114, upload-time = "2024-06-10T19:24:42.462Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/c7/af399a2e7a67fd18d63c40c5e62d3af4e67b836a2107468b6a5ea24c4304/jsonpointer-3.1.1.tar.gz", hash = "sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900", size = 9068, upload-time = "2026-03-23T22:32:32.458Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl", hash = "sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca", size = 7659, upload-time = "2026-03-23T22:32:31.568Z" }, ] [[package]] name = "jsonschema" -version = "4.25.1" +version = "4.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -481,9 +497,9 @@ dependencies = [ { name = "referencing" }, { name = "rpds-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, ] [[package]] @@ -500,24 +516,23 @@ wheels = [ [[package]] name = "keystoneauth1" -version = "5.12.0" +version = "5.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "iso8601" }, { name = "os-service-types" }, - { name = "pbr" }, { name = "requests" }, { name = "stevedore" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e5/16/b96df223ca7ea4bfa78034b205e0eaf4875bfecb2f119f375fc5232d2061/keystoneauth1-5.12.0.tar.gz", hash = "sha256:dd113c2f3dcb418d9f761c73b8cd43a96ddfa8a612b51c576822381f39ca4ae8", size = 288504, upload-time = "2025-08-21T09:34:10.767Z" } +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/627b01cde69d0ece2fd552b8c7c34af06acf13a0a77d1829ff0b46a3b45f/keystoneauth1-5.15.0.tar.gz", hash = "sha256:ce2cacdfd028e65bd23ff403d6572ebfab3b006d6d2dde3aa85c263675a9fbb5", size = 288544, upload-time = "2026-07-03T16:21:31.434Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/16/8a/803a45dc660770ac7e2d74fc1260a15ade29d2234120854747491b4a7a02/keystoneauth1-5.12.0-py3-none-any.whl", hash = "sha256:2e514b03615e2d9162f0c07c823a61a636e6d4df38ff4a34b7511a04e8a4166a", size = 343402, upload-time = "2025-08-21T09:34:09.38Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/6552fed59449604d982ffe9fe202b82f0724d333319812d1f544a95d3a09/keystoneauth1-5.15.0-py3-none-any.whl", hash = "sha256:9e2a4cf45cc2e2164093157091bc073eda1f334f755e8fab9fd25420c6f6acef", size = 342364, upload-time = "2026-07-03T16:21:29.866Z" }, ] [[package]] name = "keystonemiddleware" -version = "10.12.0" +version = "13.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "keystoneauth1" }, @@ -535,14 +550,14 @@ dependencies = [ { name = "requests" }, { name = "webob" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/09/20/99aa84ef91b9ed7365562a9fd9a4e31d7cc68201162094a618446c67fc39/keystonemiddleware-10.12.0.tar.gz", hash = "sha256:0da92b4af5178410e15a1b99f56d9cdeb2546eed088c69bc39e666fe09f869bf", size = 215566, upload-time = "2025-08-21T09:35:55.573Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/9c/26b7ad82227571d1267aac6f03a2625967f5b8a7ddf7a9ebb72b6e595dc0/keystonemiddleware-13.0.0.tar.gz", hash = "sha256:2b52b2f840bee72568666e69f9cf82b9ce1558d6124ce25f0f262083494b4b19", size = 205173, upload-time = "2026-05-13T09:21:00.647Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/20/410d86b83bfdcb70308d40994958c329afb44587e42a8402d747cb7ced05/keystonemiddleware-10.12.0-py3-none-any.whl", hash = "sha256:459c0c0811e60d13c4fbb7c2065ba83d8bfc288c0f322a301157322ea127f075", size = 152090, upload-time = "2025-08-21T09:35:54.055Z" }, + { url = "https://files.pythonhosted.org/packages/a8/eb/7f3688da851d0adeec6b78f6a49ba1a6073727f220a64839d2dffba60122/keystonemiddleware-13.0.0-py3-none-any.whl", hash = "sha256:d2102b1dbac26f985a6bfdb003e0dc84fb193106ee1c99bd7470af1d1269ec94", size = 135376, upload-time = "2026-05-13T09:20:59.296Z" }, ] [[package]] name = "kombu" -version = "5.6.1" +version = "5.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "amqp" }, @@ -550,59 +565,59 @@ dependencies = [ { name = "tzdata" }, { name = "vine" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ac/05/749ada8e51718445d915af13f1d18bc4333848e8faa0cb234028a3328ec8/kombu-5.6.1.tar.gz", hash = "sha256:90f1febb57ad4f53ca327a87598191b2520e0c793c75ea3b88d98e3b111282e4", size = 471548, upload-time = "2025-11-25T11:07:33.504Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/a5/607e533ed6c83ae1a696969b8e1c137dfebd5759a2e9682e26ff1b97740b/kombu-5.6.2.tar.gz", hash = "sha256:8060497058066c6f5aed7c26d7cd0d3b574990b09de842a8c5aaed0b92cc5a55", size = 472594, upload-time = "2025-12-29T20:30:07.779Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/d6/943cf84117cd9ddecf6e1707a3f712a49fc64abdb8ac31b19132871af1dd/kombu-5.6.1-py3-none-any.whl", hash = "sha256:b69e3f5527ec32fc5196028a36376501682973e9620d6175d1c3d4eaf7e95409", size = 214141, upload-time = "2025-11-25T11:07:31.54Z" }, + { url = "https://files.pythonhosted.org/packages/fb/0f/834427d8c03ff1d7e867d3db3d176470c64871753252b21b4f4897d1fa45/kombu-5.6.2-py3-none-any.whl", hash = "sha256:efcfc559da324d41d61ca311b0c64965ea35b4c55cc04ee36e55386145dace93", size = 214219, upload-time = "2025-12-29T20:30:05.74Z" }, ] [[package]] name = "lxml" -version = "6.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/c8/8ff2bc6b920c84355146cd1ab7d181bc543b89241cfb1ebee824a7c81457/lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456", size = 8661887, upload-time = "2025-09-22T04:01:17.265Z" }, - { url = "https://files.pythonhosted.org/packages/37/6f/9aae1008083bb501ef63284220ce81638332f9ccbfa53765b2b7502203cf/lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924", size = 4667818, upload-time = "2025-09-22T04:01:19.688Z" }, - { url = "https://files.pythonhosted.org/packages/f1/ca/31fb37f99f37f1536c133476674c10b577e409c0a624384147653e38baf2/lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f", size = 4950807, upload-time = "2025-09-22T04:01:21.487Z" }, - { url = "https://files.pythonhosted.org/packages/da/87/f6cb9442e4bada8aab5ae7e1046264f62fdbeaa6e3f6211b93f4c0dd97f1/lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534", size = 5109179, upload-time = "2025-09-22T04:01:23.32Z" }, - { url = "https://files.pythonhosted.org/packages/c8/20/a7760713e65888db79bbae4f6146a6ae5c04e4a204a3c48896c408cd6ed2/lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564", size = 5023044, upload-time = "2025-09-22T04:01:25.118Z" }, - { url = "https://files.pythonhosted.org/packages/a2/b0/7e64e0460fcb36471899f75831509098f3fd7cd02a3833ac517433cb4f8f/lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f", size = 5359685, upload-time = "2025-09-22T04:01:27.398Z" }, - { url = "https://files.pythonhosted.org/packages/b9/e1/e5df362e9ca4e2f48ed6411bd4b3a0ae737cc842e96877f5bf9428055ab4/lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0", size = 5654127, upload-time = "2025-09-22T04:01:29.629Z" }, - { url = "https://files.pythonhosted.org/packages/c6/d1/232b3309a02d60f11e71857778bfcd4acbdb86c07db8260caf7d008b08f8/lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192", size = 5253958, upload-time = "2025-09-22T04:01:31.535Z" }, - { url = "https://files.pythonhosted.org/packages/35/35/d955a070994725c4f7d80583a96cab9c107c57a125b20bb5f708fe941011/lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0", size = 4711541, upload-time = "2025-09-22T04:01:33.801Z" }, - { url = "https://files.pythonhosted.org/packages/1e/be/667d17363b38a78c4bd63cfd4b4632029fd68d2c2dc81f25ce9eb5224dd5/lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092", size = 5267426, upload-time = "2025-09-22T04:01:35.639Z" }, - { url = "https://files.pythonhosted.org/packages/ea/47/62c70aa4a1c26569bc958c9ca86af2bb4e1f614e8c04fb2989833874f7ae/lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f", size = 5064917, upload-time = "2025-09-22T04:01:37.448Z" }, - { url = "https://files.pythonhosted.org/packages/bd/55/6ceddaca353ebd0f1908ef712c597f8570cc9c58130dbb89903198e441fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8", size = 4788795, upload-time = "2025-09-22T04:01:39.165Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e8/fd63e15da5e3fd4c2146f8bbb3c14e94ab850589beab88e547b2dbce22e1/lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f", size = 5676759, upload-time = "2025-09-22T04:01:41.506Z" }, - { url = "https://files.pythonhosted.org/packages/76/47/b3ec58dc5c374697f5ba37412cd2728f427d056315d124dd4b61da381877/lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6", size = 5255666, upload-time = "2025-09-22T04:01:43.363Z" }, - { url = "https://files.pythonhosted.org/packages/19/93/03ba725df4c3d72afd9596eef4a37a837ce8e4806010569bedfcd2cb68fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322", size = 5277989, upload-time = "2025-09-22T04:01:45.215Z" }, - { url = "https://files.pythonhosted.org/packages/c6/80/c06de80bfce881d0ad738576f243911fccf992687ae09fd80b734712b39c/lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849", size = 3611456, upload-time = "2025-09-22T04:01:48.243Z" }, - { url = "https://files.pythonhosted.org/packages/f7/d7/0cdfb6c3e30893463fb3d1e52bc5f5f99684a03c29a0b6b605cfae879cd5/lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f", size = 4011793, upload-time = "2025-09-22T04:01:50.042Z" }, - { url = "https://files.pythonhosted.org/packages/ea/7b/93c73c67db235931527301ed3785f849c78991e2e34f3fd9a6663ffda4c5/lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6", size = 3672836, upload-time = "2025-09-22T04:01:52.145Z" }, +version = "6.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/a9/970b8fa0ecc4fbf1dfaed0d89bbc1fc1421b25ec26a2038c91e872dc6c8e/lxml-6.1.2.tar.gz", hash = "sha256:1055241852f2b02068af4a625a5d32c087db193c12251928af2562ecd2239f18", size = 4210626, upload-time = "2026-08-19T04:58:15.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/a4/55eb54507073089ab27743c5da2113c84f0d0b1715b33175fdd943c9652d/lxml-6.1.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7d506bdba580ecb1a6ad2e2b5c49445e66d3e1f95894885739094393a1aad237", size = 8602111, upload-time = "2026-08-19T04:58:28.017Z" }, + { url = "https://files.pythonhosted.org/packages/bc/bf/6332f45d78da385bb01d5cac3fe4acda19f025d1307cbc7ad538355fecbb/lxml-6.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12acd337d2821cb8b9247dfe4b7aa2f2769a3df5ae8511b7e550df42b8f4d3c3", size = 4638376, upload-time = "2026-08-19T04:58:41.181Z" }, + { url = "https://files.pythonhosted.org/packages/68/e0/21fba0fe74d417fbe976903ae6bc77e92cdce01aae7b636abd87756f4588/lxml-6.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5078ff51e6316c0f75ea8127c2cd24374747fb351f62fb93d1761f8ae5a04a40", size = 4939689, upload-time = "2026-08-19T04:58:48.526Z" }, + { url = "https://files.pythonhosted.org/packages/de/e5/ce3e885264fdd0bdcb6b49c1ea1842f94281b39e4ff956099e8d57532c60/lxml-6.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9477e14217c212e6023c994a71a1a349db19b0e10fd5bf189666b281ae63b1fd", size = 5105185, upload-time = "2026-08-19T04:59:15.533Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b6/990a8446c488c70fa25681e150de94b7bf2eaaf387e374d195ab3c8faafb/lxml-6.1.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:261d98065326676d7253882db0198d0aa06748d7ee0443367acf10b148273f99", size = 5011863, upload-time = "2026-08-19T04:59:50.58Z" }, + { url = "https://files.pythonhosted.org/packages/bb/6a/f70f41363dae27e3bfd6224b128f5ba150874bd32ca4938552930ffa33b0/lxml-6.1.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0666943ee1576fa890a6dc6316ef42e8241b5dd56f67bc5475acb2ac298c6ca9", size = 5638234, upload-time = "2026-08-19T05:00:00.802Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/a65b64f34d556925faef2c4f14167d58c571bc15a3e1f2bba71138830562/lxml-6.1.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04cf9e3f4ee9cab9d9ba05401bef8668840fa9620fcd4d8e85a2d2fd0b0fa960", size = 5244532, upload-time = "2026-08-19T05:00:07.516Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a9/471552e015e954fc9d960aa27c3d67ebf489683d03f033399a790417c67c/lxml-6.1.2-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:9429d2371d406344ed1da5b5686d9412e74137c07b0171278368ff704f470ed5", size = 5358194, upload-time = "2026-08-19T05:00:22.747Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0f/bc6248fbec2cc416f102b1267f1567e07510f6fa909bbe8cd2a22d6fb78e/lxml-6.1.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:eff128ffdc093cc6317955934ad9751105d37ed8dbca3ff4ccd751af6be37185", size = 4704432, upload-time = "2026-08-19T05:00:51.115Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3f/cec859f50e63f1fa338fab43d2362d7543e1237f2475960d8ab0769de0eb/lxml-6.1.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ba58574d710b82ead7cbedea01cac3e110bc3ef82d4731519b74a2c11f7cf5e9", size = 5255038, upload-time = "2026-08-19T05:00:58.895Z" }, + { url = "https://files.pythonhosted.org/packages/7c/d9/2ced0cf2967115f92a1b8b3ae6bd18763abc3ebef88c98cf25145fda396c/lxml-6.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:52f6d4dff133c9778a24e9a2cfc1608930b15869866171aacc5131b5a418a003", size = 5054481, upload-time = "2026-08-19T05:01:10.096Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f5/4f07386d3c88673daeec3b8cc09a2a4d39fa01c1fc49009791b0746d97fa/lxml-6.1.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:8807998c1023d1e9d60e02500f90e85a0752dbc0b670989806bba87b82dd5b42", size = 4785535, upload-time = "2026-08-19T05:01:18.909Z" }, + { url = "https://files.pythonhosted.org/packages/9a/5a/f4fe3ecbc189f48fba2547c5db5c940a10151d3e86b856a60a533a77e816/lxml-6.1.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2170d0a280c877b6e2dc6738217db947be35dd8cf09ca458b355aa1bab2a9e70", size = 5655337, upload-time = "2026-08-19T05:01:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/f586aa1bf27bfbace2dfdbb704da5c52f0bdece8ee440c8fb4946c940b2e/lxml-6.1.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c67f3c1278f942e97d8665c2a690324aaea5137de16f056583a21f0ac706177f", size = 5245778, upload-time = "2026-08-19T05:01:45.227Z" }, + { url = "https://files.pythonhosted.org/packages/18/a1/677494bbaef4d6db5e4633af817414f478865850b55c03ae4bf70fa7b8ca/lxml-6.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:093fbf547d0f3ca02705381f795a050fbb58988be4aac7f79f99f280c4082313", size = 5267274, upload-time = "2026-08-19T05:01:57.687Z" }, + { url = "https://files.pythonhosted.org/packages/5a/71/b71425b8764d4cb7c92eb970483be7d5610dce2a6316242b5aaae7d260be/lxml-6.1.2-cp312-cp312-win32.whl", hash = "sha256:be365ce8d2d411cf2fb573747684b4fd470fa6224e0094d9d5a21155acc369d3", size = 3602563, upload-time = "2026-08-19T05:02:01.837Z" }, + { url = "https://files.pythonhosted.org/packages/1b/fb/909584e16d2148c1a252cc2c32dd99fe0e2682459c586d3d7a192e74a0ae/lxml-6.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:b97153ca609b434b712ddfb92cd6af101a7045a7724c542258bd4727a344472f", size = 4005965, upload-time = "2026-08-19T05:02:07.157Z" }, + { url = "https://files.pythonhosted.org/packages/5f/8d/41207c9212caad0b52749e34739fb9bfab67486729f52a8fe9bd9266fee6/lxml-6.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:7feb72424f19a893ae4f3373c7aae821b1aacb6076b708915c651f0683a97c49", size = 3666641, upload-time = "2026-08-19T05:02:11.3Z" }, ] [[package]] name = "mako" -version = "1.3.10" +version = "1.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/12/b5fa2353e2754cd67fb9f83793fa48ff42c213a5da7e719869d2301f6ab8/mako-1.4.1.tar.gz", hash = "sha256:d7904710b662996425a21627710c4777c45053146942cf8a7aebf757c92b8c27", size = 410165, upload-time = "2026-08-05T06:10:56.611Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" }, + { url = "https://files.pythonhosted.org/packages/a5/54/12ed58d458474aaab5c3d180173e745a4fe131bb330370596876d19ff60f/mako-1.4.1-py3-none-any.whl", hash = "sha256:a359d9a94a541213958742b2698d0a7757bb83551767bc468a74b9905aba9617", size = 80010, upload-time = "2026-08-05T06:10:58.248Z" }, ] [[package]] name = "markdown-it-py" -version = "4.0.0" +version = "4.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, ] [[package]] @@ -635,30 +650,35 @@ wheels = [ [[package]] name = "msgpack" -version = "1.1.2" +version = "1.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" } +sdist = { url = "https://files.pythonhosted.org/packages/31/f9/c0a1c127f9049db9155afc316952ea571720dd01833ff5e4d7e8e6352dbb/msgpack-1.2.1.tar.gz", hash = "sha256:04c721c2c7448767e9e3f2520a475663d8ee0f09c31890f6d2bd70fd636a9647", size = 183960, upload-time = "2026-06-18T16:13:52.594Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" }, - { url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" }, - { url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" }, - { url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556, upload-time = "2025-10-08T09:15:06.837Z" }, - { url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920, upload-time = "2025-10-08T09:15:08.179Z" }, - { url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013, upload-time = "2025-10-08T09:15:09.83Z" }, - { url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" }, - { url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" }, - { url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" }, + { url = "https://files.pythonhosted.org/packages/bc/dd/9e8cbd8f5582ca4b590336f2b91ee5662f6a6ca562b565abaf696a0f81ff/msgpack-1.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2ef59c659f289eddf8aa6623823f19fa2f40a4029266889eac7a2505dd210c35", size = 83531, upload-time = "2026-06-18T16:12:58.249Z" }, + { url = "https://files.pythonhosted.org/packages/50/2e/ebdb85a8da151397a2790363676b7ed7c125924fe618e4c6d8befb0cc62c/msgpack-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3567748a5107cb40cdf66a275430c2f87c07777698f4bfd25c35f44d533258c", size = 82657, upload-time = "2026-06-18T16:12:59.396Z" }, + { url = "https://files.pythonhosted.org/packages/26/aa/753ad8b007b464e1d8aa0c8e650b9c5f4f725e658fc5ac8a7635c55b7f6e/msgpack-1.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60926b75d00c8e816ef98f3034f484a8bc64242d66839cef4cf7e503142316a0", size = 410634, upload-time = "2026-06-18T16:13:00.383Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/6adabd4f6d5e686f97dd02ce7fce3fe4cf672cbac36b8f67ff4040e8ad8b/msgpack-1.2.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:020e881a764b20d8d7ca1a54fc01b8175519d108e3c3f194fddc200bda95951a", size = 419989, upload-time = "2026-06-18T16:13:01.776Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cc/85039b7b0eb168aaad7383a23c97e291a11f08351cb45a606ce865e4e3f1/msgpack-1.2.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4202c74688ca06591f78cb18988228bd4cca2cc75d57b60008372892d2f1e6e6", size = 377544, upload-time = "2026-06-18T16:13:03.637Z" }, + { url = "https://files.pythonhosted.org/packages/ed/bf/35963899493b32030c85fc513b723ae66144ac70c11ebc52e889e16e3d99/msgpack-1.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8b267ce94efb76fbd1b3373511420074ee3187f0f7811bf394531de13294735a", size = 400842, upload-time = "2026-06-18T16:13:05.012Z" }, + { url = "https://files.pythonhosted.org/packages/a6/df/8e2ac970c8f99264cd9997d1c73df5466bc19da3301d7dc5500862a9b089/msgpack-1.2.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f1d0f8f98ade9634e01fb704a408f9336c0a8f1117b369f5db83dc7551d8b1", size = 374108, upload-time = "2026-06-18T16:13:06.232Z" }, + { url = "https://files.pythonhosted.org/packages/17/dd/fa8bd265110dfa51c20cb529f9e6d240a16fafe7e645004c6af2d01353ba/msgpack-1.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f02cf17a6ca1abe29b5f980644f7551f94d71f2011509b26d8625ce038f0df64", size = 414939, upload-time = "2026-06-18T16:13:07.478Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b9/8377a5ad8953fc0437c70cc98d9ae29f27fe5ac5109fbec0812085865735/msgpack-1.2.1-cp312-cp312-win32.whl", hash = "sha256:0c0d9802354507bcba62af19c17918e3eb437cc25e6f50657d511b5856a77aac", size = 64504, upload-time = "2026-06-18T16:13:08.822Z" }, + { url = "https://files.pythonhosted.org/packages/57/7f/ce1e377df7e62461fefd9eb23bfb93a4a523f40a517b377b8f844d836828/msgpack-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:5c24aa15d5963051e1a5c62b12c50cd705992502b5ec1f3bece6046f33c9fc24", size = 71421, upload-time = "2026-06-18T16:13:09.828Z" }, + { url = "https://files.pythonhosted.org/packages/8f/32/ebfe84c9929f08f188d56c7a2fd913406a9ddad76a634697c1c43b8112e6/msgpack-1.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:4227224aaec8f7fbcbfbd4272319347b2bb4030366502600f8c45588c5187b07", size = 64775, upload-time = "2026-06-18T16:13:11.056Z" }, ] [[package]] name = "ncclient" -version = "0.7.0" +version = "0.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "lxml" }, { name = "paramiko" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c2/fb/29dbb4987f81d7932d8018079bf3e6d1d5cce320d7c2c90e84d3c7dba40c/ncclient-0.7.0.tar.gz", hash = "sha256:318e8e3e72b1d2a766f3665cabef33436fd25b607da5f15657a199c648a68435", size = 116221, upload-time = "2025-08-25T15:29:24.172Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/92/e36995ee7ce151814335583571bed95f1a5a491b7ea4d5487cb67377800a/ncclient-0.7.1.tar.gz", hash = "sha256:60dabb6ac1a2d84fbc3349cb104ef7d0c5d12cf8eee43fefc715410d70410ddc", size = 123086, upload-time = "2026-03-15T16:05:49.317Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/7c/c0c06d1696b03b901fa5a854b03e16f35c5c5cc8f00a0f43217dc467cd39/ncclient-0.7.1-py3-none-any.whl", hash = "sha256:47beeeee6074bd70a9215c4d353b51c7237af3c5c15269d81692810f2aa15147", size = 94833, upload-time = "2026-03-15T16:05:47.879Z" }, +] [[package]] name = "netaddr" @@ -671,8 +691,8 @@ wheels = [ [[package]] name = "neutron" -version = "28.0.0" -source = { registry = "https://pypi.org/simple" } +version = "28.0.0.0b3.dev316" +source = { git = "https://github.com/rackerlabs/neutron.git?rev=b0678e56e9c5a7ec2f335c4271465fc1c3202e2a#b0678e56e9c5a7ec2f335c4271465fc1c3202e2a" } dependencies = [ { name = "alembic" }, { name = "debtcollector" }, @@ -715,6 +735,7 @@ dependencies = [ { name = "pecan" }, { name = "psutil" }, { name = "pyopenssl" }, + { name = "pyroute2" }, { name = "python-designateclient" }, { name = "python-neutronclient" }, { name = "python-novaclient" }, @@ -726,15 +747,11 @@ dependencies = [ { name = "tooz" }, { name = "webob" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c8/df/071c7f72f683f83fd30f1d6d20071efafe95f2fc0ce2a9de2fb9011c4706/neutron-28.0.0.tar.gz", hash = "sha256:c99063615e66f55d02b2a7700385bb89851204942d5150058d18fa01a6b31776", size = 12314767, upload-time = "2026-04-01T10:57:14.322Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/9a/1b19053a48bef10fe1b9ae8c1b3296c5c08d73834dabab3df312b8544778/neutron-28.0.0-py3-none-any.whl", hash = "sha256:efd6d38497e74e3f532253595c478aef49225aee1fba1615075337314bf1b4e2", size = 4437292, upload-time = "2026-04-01T10:57:11.78Z" }, -] [[package]] name = "neutron-lib" -version = "3.25.0" -source = { registry = "https://pypi.org/simple" } +version = "3.24.1.dev8" +source = { git = "https://github.com/rackerlabs/neutron-lib.git?rev=6d0367183cc92656ba449acefd830cb4e11f2f5b#6d0367183cc92656ba449acefd830cb4e11f2f5b" } dependencies = [ { name = "debtcollector" }, { name = "keystoneauth1" }, @@ -761,10 +778,6 @@ dependencies = [ { name = "stevedore" }, { name = "webob" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cb/35/3e9162c20a0de0edb8965adfb033118229e2970885bc5d0fbab324989586/neutron_lib-3.25.0.tar.gz", hash = "sha256:2d160440f4e572939ca2f157b1546536f3ff0ffb20514c0d3f7119e216fd6172", size = 561912, upload-time = "2026-04-20T12:23:28.971Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/17/b9dea87fda8d984b329d6a48423683f88a6035a38e79fa053af3ae080a3c/neutron_lib-3.25.0-py3-none-any.whl", hash = "sha256:619a2886a06e661a3225b94e875ddd8a8ef064614d45e5958eb1c21b105ea1c5", size = 639833, upload-time = "2026-04-20T12:23:27.225Z" }, -] [[package]] name = "neutron-understack" @@ -782,17 +795,23 @@ dependencies = [ [package.dev-dependencies] test = [ + { name = "fixtures" }, + { name = "oslotest" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-mock" }, { name = "sqlalchemy" }, + { name = "testresources" }, + { name = "testscenarios" }, + { name = "testtools" }, + { name = "webtest" }, ] [package.metadata] requires-dist = [ { name = "keystoneauth1", specifier = ">=3.14.0" }, - { name = "neutron", specifier = ">=27,<29" }, - { name = "neutron-lib", specifier = ">=3,<5" }, + { name = "neutron", git = "https://github.com/rackerlabs/neutron.git?rev=b0678e56e9c5a7ec2f335c4271465fc1c3202e2a" }, + { name = "neutron-lib", git = "https://github.com/rackerlabs/neutron-lib.git?rev=6d0367183cc92656ba449acefd830cb4e11f2f5b" }, { name = "openstacksdk", specifier = ">=4.9.0" }, { name = "oslo-config", specifier = ">=9.7.1" }, { name = "oslo-log", specifier = ">=7.1.0" }, @@ -801,15 +820,21 @@ requires-dist = [ [package.metadata.requires-dev] test = [ + { name = "fixtures" }, + { name = "oslotest" }, { name = "pytest", specifier = ">=9.0.1,<10" }, { name = "pytest-cov", specifier = ">=7,<8" }, { name = "pytest-mock", specifier = ">=3.14.0,<4" }, { name = "sqlalchemy" }, + { name = "testresources" }, + { name = "testscenarios" }, + { name = "testtools" }, + { name = "webtest" }, ] [[package]] name = "openstacksdk" -version = "4.16.0" +version = "4.18.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, @@ -825,27 +850,14 @@ dependencies = [ { name = "psutil" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b8/43/49b126e9ccfa19647d2f0aff26321caded607522d29c3d9495d44f4b9471/openstacksdk-4.16.0.tar.gz", hash = "sha256:466640c6d2b813b782d4ad58a4f2960633e8e58f147ec0baa2019454de190d30", size = 1397114, upload-time = "2026-06-17T08:43:22.774Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/ea/60a4a274c4991847a085ac4a36b19d09f44f04a22adcfda6ca631763322b/openstacksdk-4.16.0-py3-none-any.whl", hash = "sha256:0469939bd7fc1b80979e4440dd8f8026eea302bdd979fac5437b56ada3780a62", size = 1982817, upload-time = "2026-06-17T08:43:21.148Z" }, -] - -[[package]] -name = "os-client-config" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "openstacksdk" }, - { name = "pbr" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/50/cd/352f6f18d1fb90780b95fdc3a668a279bd41d89905d70ee06076b529077c/os_client_config-2.3.0.tar.gz", hash = "sha256:e16a260f2fd500af14f157b9b7b7d69292ce83b0f8a461ec68ce6a8a42967cbd", size = 49928, upload-time = "2025-07-08T09:00:46.701Z" } +sdist = { url = "https://files.pythonhosted.org/packages/60/d0/514c38d0b7f4d3652321baf0c5136ac29ce9360a2094fcdcd78f865cb7c9/openstacksdk-4.18.0.tar.gz", hash = "sha256:466f2f869bcf6dec717a5e6c65c0522b1bd061d53310c7bf11982e0d9244f70c", size = 1403478, upload-time = "2026-08-03T12:27:08.152Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/d1/0f6fe5650516fd5113ca64f3efbdd35c91be14ead96065b835207a9cd75a/os_client_config-2.3.0-py3-none-any.whl", hash = "sha256:bac8486bbafc0dd92704c29d5581d65030912804e8e675d13bc43b76ce051c82", size = 30898, upload-time = "2025-07-08T09:00:45.265Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/1c68b79bc71b1d9ea1d7acfd9a90f52bddfc2485ac3f8e1d1be488fcbcb8/openstacksdk-4.18.0-py3-none-any.whl", hash = "sha256:17d033c19a07df90116b67c0750b2218acdc45cdcf659d4c137f084667b26993", size = 1993600, upload-time = "2026-08-03T12:27:06.589Z" }, ] [[package]] name = "os-ken" -version = "4.2.0" +version = "4.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "eventlet" }, @@ -859,9 +871,9 @@ dependencies = [ { name = "routes" }, { name = "webob" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ca/58/a3faf90cd877695018cf03d4a0ea935c050afa293ecb24cebd440335563e/os_ken-4.2.0.tar.gz", hash = "sha256:c7120748e93bca060427e3e5624a110a5c5b7bb907f8bb3ba4c1f77a07c5cb19", size = 1399545, upload-time = "2026-05-08T11:39:31.143Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/81/3dc912cca417f80d5b0871904c6e1b7b7002de867b5a7981f2a8d1d45c43/os_ken-4.2.2.tar.gz", hash = "sha256:bf6a640c0e67c377d4e6c0014f1de36bbf53d16eb7ecb080e5f9b5ede9bbf836", size = 1402804, upload-time = "2026-08-20T15:13:06.089Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/1e/577ef857ddefa74d503d525922fe7b086c09f1e324fab972aa2d5d9e2a95/os_ken-4.2.0-py3-none-any.whl", hash = "sha256:eb688d599f193258940ab50a55a59c4ab1243bc126bcef6d04d8bfaab04bdeec", size = 2022300, upload-time = "2026-05-08T11:39:29.451Z" }, + { url = "https://files.pythonhosted.org/packages/7b/02/9758ad2b9b27e2a4ba616ab66d5d5dcc31d18780c0afaac37997b7b6ce03/os_ken-4.2.2-py3-none-any.whl", hash = "sha256:d61b016b5e753a64f3652014e080ce9e216540a620666fc22835f880dbf63d3b", size = 2024694, upload-time = "2026-08-20T15:13:04.443Z" }, ] [[package]] @@ -878,32 +890,32 @@ wheels = [ [[package]] name = "os-service-types" -version = "1.8.2" +version = "1.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pbr" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/51/62/31e39aa8f2ac5bff0b061ce053f0610c9fe659e12aeca20bfb26d1665024/os_service_types-1.8.2.tar.gz", hash = "sha256:ab7648d7232849943196e1bb00a30e2e25e600fa3b57bb241d15b7f521b5b575", size = 27476, upload-time = "2025-11-21T13:55:47.726Z" } +sdist = { url = "https://files.pythonhosted.org/packages/86/ae/fe7ac23155ae0b4b9779e06e9c5bb4070f2315dc4ca886a88fa3230d344b/os_service_types-1.9.0.tar.gz", hash = "sha256:1f2e5fb71d1f6f4ff31d8992674f2368465bc2f25cd94018015c3ddbfc5c617f", size = 28087, upload-time = "2026-08-04T13:07:43.171Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/29/26/0937af7b4383f1eba5bca789b8d191c0e09e59bb64962b18f4a14534ce41/os_service_types-1.8.2-py3-none-any.whl", hash = "sha256:f78890d71814deffabf0ed4358288ec2ced579bc4d0bb87a79ae806cbb4deb6e", size = 24876, upload-time = "2025-11-21T13:55:46.093Z" }, + { url = "https://files.pythonhosted.org/packages/e3/79/f8ae99ae9bb54cd63e7ab89251b2a4ff4ff07f02847403948b6a6f50f816/os_service_types-1.9.0-py3-none-any.whl", hash = "sha256:f268545c896434177bf55c43f7d5129e65210d5d456df3f00a1c9832a71c3a4b", size = 25282, upload-time = "2026-08-04T13:07:42.258Z" }, ] [[package]] name = "os-traits" -version = "3.5.0" +version = "3.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pbr" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/97/05/a483b40340af7d65f9c76424521f198ad4229032641905b7d5bdcc96a478/os_traits-3.5.0.tar.gz", hash = "sha256:65f0944bc885c1ff4454ab8e74844194863df8b05cf78c7c8c803ef295c8761d", size = 32513, upload-time = "2025-05-15T05:24:22.621Z" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/54/68070c55d96437e5189e46f81781b8e5e7113f9bd0505480bc9c023bb7e1/os_traits-3.9.0.tar.gz", hash = "sha256:4c68ea8de3dc8088f4f87f382558291a0b1c679e63be796332611539fc11ff03", size = 33656, upload-time = "2026-08-21T19:27:30.006Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/02/1cc710a536994ba27263a33dea7befb638f16f9e5fce577fd46897681b00/os_traits-3.5.0-py3-none-any.whl", hash = "sha256:238cb8de19c383a61c7682c90ddc202311f3bde4f92687325f2cc26dac174973", size = 43668, upload-time = "2025-05-15T05:24:21.133Z" }, + { url = "https://files.pythonhosted.org/packages/99/3d/67908de48b0c5f204b00b73e83a3217d50fa9ac7d78279861634ca39f976/os_traits-3.9.0-py3-none-any.whl", hash = "sha256:442a30cdd4187f04758956a3432e51b89ba0cb07049d96c39ec0dd34387da81a", size = 44744, upload-time = "2026-08-21T19:27:28.89Z" }, ] [[package]] name = "os-vif" -version = "4.2.1" +version = "5.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "debtcollector" }, @@ -917,17 +929,16 @@ dependencies = [ { name = "oslo-versionedobjects" }, { name = "ovsdbapp" }, { name = "pbr" }, - { name = "pyroute2", marker = "sys_platform != 'win32'" }, { name = "stevedore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0d/8a/3f0e417a1456583a3b7e1a2974a7cfe24b2a2539c96045e6cbbd5624421a/os_vif-4.2.1.tar.gz", hash = "sha256:372ddfd471a955ecb813543829be91869fd51c51d3a1f6866a96b56954de4c52", size = 104317, upload-time = "2025-08-25T13:03:28.882Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/35/3eb7a6825f8dbcdb50f7dd2c093f6e2f4900e564faf854c008a1d96a54c9/os_vif-5.2.0.tar.gz", hash = "sha256:7fa8e32a05b3cd1a12369b93c90968c4de290106af826d1d67bb7209e7a940c9", size = 104043, upload-time = "2026-08-21T19:40:30.673Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/09/e48d406ecf4eb73bda077c6ed2f8e92d2b5923ecf55e3b2213ab4f3b403c/os_vif-4.2.1-py3-none-any.whl", hash = "sha256:7c9b47285b7dcb715956053a09d46b0a101bf3304fd0a59e99ecbaaafa7963ac", size = 110971, upload-time = "2025-08-25T13:03:27.545Z" }, + { url = "https://files.pythonhosted.org/packages/c8/dd/ff8039231559894d8d3603a44f47fc4824cf6eb5b9a049d3e5abcf6f94fd/os_vif-5.2.0-py3-none-any.whl", hash = "sha256:8fa38309a2ba23db384f095eb30872cceee18bc7c5d88f5e75efe4fe94968d76", size = 104556, upload-time = "2026-08-21T19:40:29.461Z" }, ] [[package]] name = "osc-lib" -version = "4.2.0" +version = "4.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cliff" }, @@ -939,14 +950,14 @@ dependencies = [ { name = "requests" }, { name = "stevedore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7c/63/ea0eda39e1b2c0ca3e9d87798568f881ea7aa90e4f94affb78676215f72d/osc_lib-4.2.0.tar.gz", hash = "sha256:99718f06a990c1ad6fb9034bbed9655390a2ea83cef71a53781e7e9abd9f20ce", size = 101802, upload-time = "2025-08-21T09:35:30.372Z" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/80/37ac2a46cc3ea9348ea670fd76dad72888726191f5875278a3d85034a9d8/osc_lib-4.7.0.tar.gz", hash = "sha256:5b896de12ed69fb1111d2971467d403b838d414fc27df2024fc50e8652a53f2b", size = 105896, upload-time = "2026-07-10T12:56:56.413Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/9a/5943674892bda78e4671a553e68406d6e7f2ff278d253d6c991b1fe040cf/osc_lib-4.2.0-py3-none-any.whl", hash = "sha256:951d25c0975765242816c9e8f86a8e223dfae78a23ed8eb5a8e1c9c8dd688d58", size = 92898, upload-time = "2025-08-21T09:35:29.053Z" }, + { url = "https://files.pythonhosted.org/packages/2b/3c/adce48d55f5688d5c46d716162ab9a38794f09bc0c87a3cf32383cfa42f1/osc_lib-4.7.0-py3-none-any.whl", hash = "sha256:a6b14d8a1d9a487334925d377b2b249d5f7317eb57e53e89de3a2627fe268d6d", size = 97314, upload-time = "2026-07-10T12:56:55.13Z" }, ] [[package]] name = "oslo-cache" -version = "4.0.0" +version = "4.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "dogpile-cache" }, @@ -955,14 +966,14 @@ dependencies = [ { name = "oslo-log" }, { name = "oslo-utils" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/13/ee/82c4545847fc0c738028a49cbe57b0f00ddae17e6a1728b5a509a27c0cef/oslo_cache-4.0.0.tar.gz", hash = "sha256:b4568ea8d12a291bb3ea2346d74bfbd50ca8f86b5394014ba31e90d9e6f03b79", size = 70605, upload-time = "2025-11-19T19:32:12.563Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/19/e89474be0b62e2f61c089ad9c437ddf8fb1fc482c61ca7e7e29e19f0c086/oslo_cache-4.3.0.tar.gz", hash = "sha256:c747ada25e20925746da36a208eec50f0d4c310fcaac10b436b69393ee8140fe", size = 71016, upload-time = "2026-07-10T13:53:32.579Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/37/ee/c15127ab0c4fe68d53b903e2248a56939c555870c435d714ced653e77394/oslo_cache-4.0.0-py3-none-any.whl", hash = "sha256:b1cec8f48e230d94dba30d0efffe7482062f321b983676612110f2f0ca182a1e", size = 65919, upload-time = "2025-11-19T19:32:11.246Z" }, + { url = "https://files.pythonhosted.org/packages/1b/e2/c694e9f9300e796d8a0f2c62bf3ea657de2dbb90d87a1bfda1b2a028beff/oslo_cache-4.3.0-py3-none-any.whl", hash = "sha256:16ceeb61e07eb5bc010ad29d3a273ab0806c4a4a61c46ea8a1b0320a7739e1a1", size = 65296, upload-time = "2026-07-10T13:53:31.587Z" }, ] [[package]] name = "oslo-concurrency" -version = "7.2.0" +version = "7.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "debtcollector" }, @@ -972,14 +983,14 @@ dependencies = [ { name = "oslo-utils" }, { name = "pbr" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/29/bd/727bf8de6ef870b400702cd57826e8e298f2d906ab2f8dcc3890c421711f/oslo_concurrency-7.2.0.tar.gz", hash = "sha256:160e814c8549f6a464e77a04265ed996d581bdd3fd3bd4ebf311d3446bb7e1aa", size = 60285, upload-time = "2025-08-25T12:53:05.619Z" } +sdist = { url = "https://files.pythonhosted.org/packages/90/d8/cd9a472611c8ce11c858195885c3e9383c054cca52e1f66ce287f6988d00/oslo_concurrency-7.6.1.tar.gz", hash = "sha256:8dcd3a6e931fc24d53832e887a124e3872e44e1af015cab47783bc67ad5b85e7", size = 64052, upload-time = "2026-07-16T08:58:12.581Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/80/ab/7afe0703bc50081d6e82b00dce95285735926245b21b323018410cc84e72/oslo_concurrency-7.2.0-py3-none-any.whl", hash = "sha256:9bf8953572ba8755aaec6f0bcca6ee2a5e9128bef1827da5f49caa78caf71fae", size = 47066, upload-time = "2025-08-25T12:53:04.006Z" }, + { url = "https://files.pythonhosted.org/packages/f0/fd/c3d55e23b7de17704563364b40613b63a244fa9535bb5dc41f67067d8d54/oslo_concurrency-7.6.1-py3-none-any.whl", hash = "sha256:37cc117c5d0600542d3d88ce03efa96f53fc9324cdf3c056695674a680c488b2", size = 49130, upload-time = "2026-07-16T08:58:10.843Z" }, ] [[package]] name = "oslo-config" -version = "10.4.0" +version = "10.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "netaddr" }, @@ -989,27 +1000,27 @@ dependencies = [ { name = "rfc3986" }, { name = "stevedore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/25/a9/a1295eceb3a79ad46f32d145bade3119dc20636e2fda62adaba19c61195c/oslo_config-10.4.0.tar.gz", hash = "sha256:2ae3e02593474ecd7b64ec4eb11482adb4c928a78267bc820f5c3f80240b197a", size = 168943, upload-time = "2026-05-18T09:31:19.554Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/cd/e9e312ca216eaa9e0fa76f8d8fb9aeb14ea56f136136ec6753a11df24237/oslo_config-10.7.0.tar.gz", hash = "sha256:4f0fd4ca7ecfe511fbc0e012d3c63fce57009f360199e9e9cc2329ed55ec911e", size = 177161, upload-time = "2026-08-07T11:14:20.894Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/49/87743b93ea01f9cf667e78c1ceea1058f816ef733399f4e45469472cf2fe/oslo_config-10.4.0-py3-none-any.whl", hash = "sha256:0429c7b312114fb796005bdb718bba51e66de9c0814fb2cee22e754afbfe253b", size = 137504, upload-time = "2026-05-18T09:31:17.809Z" }, + { url = "https://files.pythonhosted.org/packages/55/12/bec8c6f23dc1e0eb1adffe28a4450ac24c3b1b7ae89a09be4e1764492dcc/oslo_config-10.7.0-py3-none-any.whl", hash = "sha256:d4e80b535fb208cb15cf0ce6f6fd976eebd694f21dc37e3f3d68a360cbe7eab5", size = 143839, upload-time = "2026-08-07T11:14:19.729Z" }, ] [[package]] name = "oslo-context" -version = "6.2.0" +version = "6.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pbr" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/17/56/07d71967a0b0ed3e4409df64b9aaa023ffc2cf3452e042bcd6559e329da9/oslo_context-6.2.0.tar.gz", hash = "sha256:465c67da4d92dc2960deec148dee469d7ae7736af869581e2e3b73ce2cfa27a8", size = 34983, upload-time = "2025-11-20T15:18:32.437Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/6b/71f00290f6fb7302178422d3478093aacf972ab3e6e0f4b9a91026f533f8/oslo_context-6.5.0.tar.gz", hash = "sha256:7e1fb03c6a97167959f37d930300154e0ee837ecdb85798c2bbe8878b56caaaf", size = 35642, upload-time = "2026-07-10T13:48:26.139Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/dd/a76cb78fd6d08eb07b613fb4dc68259beeaa922d9907371542462d8bdde6/oslo_context-6.2.0-py3-none-any.whl", hash = "sha256:6ea0b5d04c3577090d6513ce4533a74cdcee958e34a6fc54d820267b799f9d29", size = 19853, upload-time = "2025-11-20T15:18:31.356Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ab/be9bc57dbc54f07083a025736f6ba835ce2d89278200dbc9293010cade63/oslo_context-6.5.0-py3-none-any.whl", hash = "sha256:6f3f038917711c03fa34bae1867076df24bd7a5ab5d3bf0bd0407869c5274d7e", size = 20097, upload-time = "2026-07-10T13:48:25.238Z" }, ] [[package]] name = "oslo-db" -version = "17.4.0" +version = "18.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "alembic" }, @@ -1017,31 +1028,31 @@ dependencies = [ { name = "oslo-config" }, { name = "oslo-i18n" }, { name = "oslo-utils" }, - { name = "sqlalchemy" }, + { name = "sqlalchemy", extra = ["asyncio"] }, { name = "stevedore" }, { name = "testresources" }, { name = "testscenarios" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c5/99/9bbfc917740b79bc4a96a07938cff8792c47963a63fa9a8a36f216a6ced6/oslo_db-17.4.0.tar.gz", hash = "sha256:14b62f58c416330cbb188a5329b14d95017661ef8aea1d72a0ff924eecf910a9", size = 172000, upload-time = "2025-08-07T13:58:18.372Z" } +sdist = { url = "https://files.pythonhosted.org/packages/33/dd/3ed5c23e1f92882664fc65c55cd6b3756b4679f93289760d567df0685561/oslo_db-18.1.1.tar.gz", hash = "sha256:5239ba059b94c381c61b5abe607adcd15eaee8df115deaddb8a36dcb366e44fa", size = 168774, upload-time = "2026-08-24T13:58:03.741Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/b5/81aee8626a22b3e656ea55a2d7adaa9eafe156dc42159eda96ef246ecdd6/oslo_db-17.4.0-py3-none-any.whl", hash = "sha256:c63cf2f91e3403a29f068ea10d7ed1769db3664c349a805291fb3a069cddca4d", size = 157434, upload-time = "2025-08-07T13:58:16.581Z" }, + { url = "https://files.pythonhosted.org/packages/f6/e7/3d4aacb5ce83f0173d29ca60bd8e93b63cff4328974f4a1f278f019adb40/oslo_db-18.1.1-py3-none-any.whl", hash = "sha256:d8ba86515e454404a98a6755bc88da116b40b12927840b46fd7b25fbe8d453f9", size = 152466, upload-time = "2026-08-24T13:58:02.547Z" }, ] [[package]] name = "oslo-i18n" -version = "6.7.1" +version = "6.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pbr" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4d/58/9d4a11a838f174180606f71a8eeeb0a443e98e28798d87c150be732c0d32/oslo_i18n-6.7.1.tar.gz", hash = "sha256:7dc879089056fe287a6fb46fa2e73ad88f8d4b989bd63f00486f494435b24ced", size = 49721, upload-time = "2025-11-20T15:14:32.85Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/69/72b03bb4d33f51a157c02d5297227bae48b9c359103856942b8774b608df/oslo_i18n-6.9.0.tar.gz", hash = "sha256:574bcf21873b185068bcec951de1ec093158ffdff05a8055fd18ddcb69f69e65", size = 50369, upload-time = "2026-07-10T13:44:34.301Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/12/6215fd01b902973a8e27c8fc8d1c215fabc12ee0211ae4fe60ae0d0f79dd/oslo_i18n-6.7.1-py3-none-any.whl", hash = "sha256:36f5f88e05415f6bef28c3d7ee0eca634a6b3ada2d38df2c0f8d675dc1bfd5aa", size = 47671, upload-time = "2025-11-20T15:14:31.858Z" }, + { url = "https://files.pythonhosted.org/packages/fa/3a/f9f9be0fd3b5c180b64f08daef70ca20fbbdccab47d0a21b65f0e4b5b8d1/oslo_i18n-6.9.0-py3-none-any.whl", hash = "sha256:354b6cd3ba1acf1f04231f690028fd5b58bd8b7cda7bd9859126bb97152c717b", size = 47847, upload-time = "2026-07-10T13:44:33.205Z" }, ] [[package]] name = "oslo-log" -version = "7.2.1" +version = "8.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "debtcollector" }, @@ -1053,14 +1064,14 @@ dependencies = [ { name = "pbr" }, { name = "python-dateutil" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/81/6b/a7f1c1daeadd36f71633fb3ebac9817fcb1f8edfd06d6bdd71384f39010f/oslo_log-7.2.1.tar.gz", hash = "sha256:01aebabdcf06b62df00e479db99df0c23f6cd24c6500ab3110e604bd059fa8d5", size = 98147, upload-time = "2025-08-25T12:56:15.605Z" } +sdist = { url = "https://files.pythonhosted.org/packages/05/c5/f7ac6a80bd298630574fc77bd33603960781b9502115992536e9c1be1ec5/oslo_log-8.3.0.tar.gz", hash = "sha256:8cdd7f082ab51e29453b9cc9fe6faf7eada6ffc2173e4df5d4424ba94191d2a5", size = 101332, upload-time = "2026-07-10T13:10:06.858Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/ee/6b92193f6deb3183caa8386e23489f6199c0d5251cbfc5360c3308d5092e/oslo_log-7.2.1-py3-none-any.whl", hash = "sha256:77a2d8dd2a06074484e9e33fa1d0e31dd4d5dd1e81b34efe2b45762e4db3c996", size = 75065, upload-time = "2025-08-25T12:56:14.379Z" }, + { url = "https://files.pythonhosted.org/packages/70/ae/eb498708694690f50ed9ead614839489dc179b68b5b220493bfa53d5bde7/oslo_log-8.3.0-py3-none-any.whl", hash = "sha256:de5bf314181fea25957d398484bf878ab164353568bc890243e64998af4b9cdd", size = 77269, upload-time = "2026-07-10T13:10:05.708Z" }, ] [[package]] name = "oslo-messaging" -version = "17.1.0" +version = "18.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "amqp" }, @@ -1081,14 +1092,14 @@ dependencies = [ { name = "stevedore" }, { name = "webob" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a8/42/b3ad17c818c2c83ee96055f8f3923bbc66df998b94c8582e225eebe6932e/oslo_messaging-17.1.0.tar.gz", hash = "sha256:84c7e0ebafed29a301f6a2c9ca61e9175923df6af08ee5ed36a419f7070a8bdc", size = 230478, upload-time = "2025-07-11T14:24:02.078Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/33/eda37a341dad350cc0790180ddce4666287974a842bef07defc9b19d9074/oslo_messaging-18.3.0.tar.gz", hash = "sha256:a468bee8226e3e79878cab1904a8959fd4f924f3a6474470343f51b0818cb745", size = 234370, upload-time = "2026-08-24T14:01:19.704Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/df/50e2dc3245b36e840d525a72243856ef358a878f8a3c5995246472cb6bb6/oslo_messaging-17.1.0-py3-none-any.whl", hash = "sha256:f6ee06859e2969aa1bdbc85bbfe9c332db932a239b6e27f150d19eafee04c500", size = 202463, upload-time = "2025-07-11T14:24:00.427Z" }, + { url = "https://files.pythonhosted.org/packages/e9/c1/a367ac077352a861cc3440a0f986586cf3f8e6509f720fd74c1142974f32/oslo_messaging-18.3.0-py3-none-any.whl", hash = "sha256:1d83784ddd2a209a6fd1450a4e4dd57b0b86d3522b7a726309b7345944827957", size = 204484, upload-time = "2026-08-24T14:01:18.386Z" }, ] [[package]] name = "oslo-metrics" -version = "0.14.0" +version = "0.17.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "oslo-config" }, @@ -1097,14 +1108,14 @@ dependencies = [ { name = "pbr" }, { name = "prometheus-client" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/27/3e/a2109f559449007e49d0e42598c9fae1d664c39e1e5c340a04c1de17fece/oslo_metrics-0.14.0.tar.gz", hash = "sha256:7d05b7480ae866517753ab4ca5e8819db403dcad7632bf892a42f4b49b9ceaff", size = 21639, upload-time = "2025-11-18T09:43:59.629Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/ee/e09d6eb519c497217a17a99b4f5ff131d5360527eb6e600fabd1b2ad5674/oslo_metrics-0.17.0.tar.gz", hash = "sha256:3fa3d6d6209efdbdcf88840a2016efa53fa6c1a3e98847e60ded0d91d074216b", size = 22966, upload-time = "2026-08-24T14:06:06.716Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/30/d95958e9e116e76d280e351eb88ddd738c760f5e6ac2be4bc778653ff067/oslo_metrics-0.14.0-py3-none-any.whl", hash = "sha256:a7cfae800ef9de350c2c17728ba8435539d7e0266b52b382a3b052776cbb1b8f", size = 14105, upload-time = "2025-11-18T09:43:57.688Z" }, + { url = "https://files.pythonhosted.org/packages/aa/0d/fe3e62eb7eb0c890d6672740fbc7e188b7e42f3e80e8adf3d3a3524012c7/oslo_metrics-0.17.0-py3-none-any.whl", hash = "sha256:484a06c4767e39a5897919fd1ee21a805e5e350c24564cc50b1c12d00d3f2d30", size = 14623, upload-time = "2026-08-24T14:06:05.684Z" }, ] [[package]] name = "oslo-middleware" -version = "7.0.0" +version = "8.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "bcrypt" }, @@ -1119,14 +1130,14 @@ dependencies = [ { name = "typing-extensions" }, { name = "webob" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/36/83/55eaed35158c44bfe2384f435aa1d5ddb5dc3f19852a53f977d26c56895f/oslo_middleware-7.0.0.tar.gz", hash = "sha256:3b94fccd32a30e233f150158b80e0cca715366d79c00b0623c394b414a5c8b30", size = 67093, upload-time = "2025-11-19T19:19:01.86Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/24/6e725c446a4e187141dde846dbe96c11c0e24916055fb5bd58955e81bb93/oslo_middleware-8.2.0.tar.gz", hash = "sha256:dc53a609c59bb3d92f7364d9b6db8f704e672f5b7ea904c5824ccc1dc189b92b", size = 75602, upload-time = "2026-07-10T13:03:43.712Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/47/b0515778b0a4da8eb7cdfbae42d58651e20ecd1b915af970dd6c18c31afd/oslo_middleware-7.0.0-py3-none-any.whl", hash = "sha256:dcb32f7d50f7951fe367987636404f13180707703b9abe854b8c9eaeb699c255", size = 65879, upload-time = "2025-11-19T19:19:00.558Z" }, + { url = "https://files.pythonhosted.org/packages/b3/fe/2de47424cc03e013e20f0d5679150ce540ebc810e2c243b6870a781520a0/oslo_middleware-8.2.0-py3-none-any.whl", hash = "sha256:cd5af811982a40a1d3c4e4eacc213033c1a231a1893bf498113da4bd45936b31", size = 74880, upload-time = "2026-07-10T13:03:42.475Z" }, ] [[package]] name = "oslo-policy" -version = "4.7.0" +version = "6.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "oslo-config" }, @@ -1138,34 +1149,32 @@ dependencies = [ { name = "requests" }, { name = "stevedore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/f8/fe58fb492637d3100b4e77965991e0e80650d35144007f4b12a7b06829e5/oslo_policy-4.7.0.tar.gz", hash = "sha256:984f2414ea805394b1e3af65b172ec1f7c67deafa181b039d02e7408f5dc27cf", size = 120673, upload-time = "2025-11-13T13:51:05.146Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c8/1e/71f9edfe473164f185adc3f9c9795678fb74dbf4dd0036468ef3b9b3d2e1/oslo_policy-6.0.1.tar.gz", hash = "sha256:0983259d3919b15caa3755b22fba1451904f60ac20a48656bc61afaf420f7a67", size = 121527, upload-time = "2026-08-24T14:03:35.7Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/25/82ed04c26fe511483fdc07336594194ad1446530b614de3c3efd6795a4d5/oslo_policy-4.7.0-py3-none-any.whl", hash = "sha256:65441442b61ee2fda19129fab70866c74813001edff0e7652a757e4feffc2158", size = 88613, upload-time = "2025-11-13T13:51:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/43/d9/a8f1ccf1b706ade6cf3d7fa229651a023efa6d1b62b950954134f91e8076/oslo_policy-6.0.1-py3-none-any.whl", hash = "sha256:9509b6c824b3da378e5f11014c05c188791508ad92acb68feb05272b09dd2056", size = 89020, upload-time = "2026-08-24T14:03:34.583Z" }, ] [[package]] name = "oslo-privsep" -version = "3.8.0" +version = "3.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi" }, { name = "debtcollector" }, - { name = "eventlet" }, - { name = "greenlet" }, { name = "msgpack" }, { name = "oslo-config" }, { name = "oslo-i18n" }, { name = "oslo-log" }, { name = "oslo-utils" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/ce/075ec43d7c7081e761e5b50429b592af8e6077397bc6f62c5f590d9ee05a/oslo_privsep-3.8.0.tar.gz", hash = "sha256:73c4e0656bddb23f620f916a21b6413ca0034483144a5968bcacedd3a34447f6", size = 49502, upload-time = "2025-08-25T12:47:19.485Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/ba/745a51d54e6c716dfecf97e179885aa3ed52a1f97d7b51f34920d0e672b4/oslo_privsep-3.12.0.tar.gz", hash = "sha256:166edcd4a3e982f4ea851308df6408bc6ad5323fbdd83ad6b2a79faed50b9617", size = 52332, upload-time = "2026-07-10T13:56:42.34Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/52/0a/613759825c6ea89bba08f9a564ab20e7c27313c39e2591255651c94b0819/oslo_privsep-3.8.0-py3-none-any.whl", hash = "sha256:657554a3c5fc625cd2bd69b7a840d33cc30ce03e651faf0959c8073114747f83", size = 38747, upload-time = "2025-08-25T12:47:16.466Z" }, + { url = "https://files.pythonhosted.org/packages/4e/17/55c5873d2660431c5fb9978336503b6adeb6bda1451f2680ef7b4558d200/oslo_privsep-3.12.0-py3-none-any.whl", hash = "sha256:ee3eaa5b3fb25e2a5199c5bfb1e3c0f90a0d50644a1cf997281d76400e6a2627", size = 41495, upload-time = "2026-07-10T13:56:41.311Z" }, ] [[package]] name = "oslo-reports" -version = "3.6.0" +version = "3.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jinja2" }, @@ -1176,40 +1185,40 @@ dependencies = [ { name = "pbr" }, { name = "psutil" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cb/b7/af820b313ff8e77e6cf5d2d873ac2dd8081be88f6a5939ea15c4249f6d91/oslo_reports-3.6.0.tar.gz", hash = "sha256:3dc915adf2843154fceef187ae5e1813c0a38dff17faf3ab8849d4da67e94998", size = 54005, upload-time = "2025-08-25T12:48:06.343Z" } +sdist = { url = "https://files.pythonhosted.org/packages/57/d8/6918517b6703189482522c9384955e237c2e845484b4d1fb8f8c4d8e1777/oslo_reports-3.9.0.tar.gz", hash = "sha256:66e0563d5cb5afc0facba08f48906493d44a9a0487d14a3b02b12fb60ed95513", size = 55809, upload-time = "2026-07-10T12:44:07.832Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/a0/87fb7b10d461a5991d0529cc71959a9be6fe89d8af132d2ef1974d3a36ff/oslo_reports-3.6.0-py3-none-any.whl", hash = "sha256:4ed0349319f9062ca50a26ab2e4fd233d75235ff57e3447e853f6f38ea4337b9", size = 53159, upload-time = "2025-08-25T12:48:05.036Z" }, + { url = "https://files.pythonhosted.org/packages/72/79/a9f8012f9d162708d8a734efdd4a36d92196398727c9b5a9df2db1f0d684/oslo_reports-3.9.0-py3-none-any.whl", hash = "sha256:6417d4ac55c59d3d6efa7a188b6d2eec6d8f01d971efb1853c2adb4ba7725e3b", size = 54733, upload-time = "2026-07-10T12:44:05.614Z" }, ] [[package]] name = "oslo-rootwrap" -version = "7.8.0" +version = "7.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "debtcollector" }, + { name = "pbr" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cc/ab/77de0eb0cea79b9995a8d980faf7ea796d32067c0213e975b5991b377414/oslo_rootwrap-7.8.0.tar.gz", hash = "sha256:955aa59d5dbeba4d7c2ccada80139298a3b750ec01805cb19c86a41c959f8ed9", size = 50942, upload-time = "2025-11-13T13:51:20.214Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/c7/f9aea00df0b93d36948d7e8b82e7e7de626e03ee80115cedf523d54c96c7/oslo_rootwrap-7.10.0.tar.gz", hash = "sha256:99c24f763c3f1f0be96652e416a099b486115d9709dcfa73310f48168cf89a44", size = 53469, upload-time = "2026-07-10T13:11:59.641Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/21/01/561b9175e570570ce013b6b98404e2dbba7111d23de4d4611b0a1c9b3243/oslo_rootwrap-7.8.0-py3-none-any.whl", hash = "sha256:1ebde5fc2977644dce97ce26247027669a7988121eaa193e4bc90ea032b30ee8", size = 38173, upload-time = "2025-11-13T13:51:19.2Z" }, + { url = "https://files.pythonhosted.org/packages/3d/f9/761b04324c12e516b35776803396d51d7f66646e043a2cca5df07f6b852b/oslo_rootwrap-7.10.0-py3-none-any.whl", hash = "sha256:0e1beb11610f4c0453358d5f5b130f805ead9eb3344f5a51472ea52225ee76b4", size = 40880, upload-time = "2026-07-10T13:11:58.698Z" }, ] [[package]] name = "oslo-serialization" -version = "5.8.0" +version = "5.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "msgpack" }, { name = "oslo-utils" }, - { name = "tzdata" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d4/ac/119c430df3a86dc6a664fa864f777b4fd5cc16c50caa1ba3dd3bf10f43ae/oslo_serialization-5.8.0.tar.gz", hash = "sha256:5871a62b23f98cacd5518482941ae6d2a983e2936ed52d543ad08685dc6d2343", size = 35227, upload-time = "2025-08-25T12:50:39.333Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/f5/2611fb291898fa5f3b41c68e916cb060305cb718a043fdbff25026491fc4/oslo_serialization-5.11.0.tar.gz", hash = "sha256:8326e85a80856c1068007423fcf6fe29dd2fe57a32f5d87fff0120b555b4b67c", size = 37298, upload-time = "2026-07-10T12:47:47.913Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/4c/269503bc1355798e33e56e6c602ed672d1e40bd65485d771da9595779606/oslo_serialization-5.8.0-py3-none-any.whl", hash = "sha256:451e19b9c72e4e7167063eea29e24ad07742d393ff2b54bb8783b9601c155b2c", size = 25766, upload-time = "2025-08-25T12:50:38.253Z" }, + { url = "https://files.pythonhosted.org/packages/2e/12/c62e721591107d15aaab271ed97d93564f77a37225c15148b6c85029d492/oslo_serialization-5.11.0-py3-none-any.whl", hash = "sha256:34d47104d94296488fad23f280ad09b7000102636b3e6f4f120517678ba1b64f", size = 27617, upload-time = "2026-07-10T12:47:46.867Z" }, ] [[package]] name = "oslo-service" -version = "4.4.1" +version = "4.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "debtcollector" }, @@ -1226,9 +1235,9 @@ dependencies = [ { name = "webob" }, { name = "yappi" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/61/f9/794b71269484f0fb776bd53bd80bc6b39500745c26ee8d0c03c889144b6b/oslo_service-4.4.1.tar.gz", hash = "sha256:2f0e306e934f6210c8cabfa68cf86339167df199cb084cf01a9800427086e725", size = 107926, upload-time = "2025-11-19T14:54:22.432Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/9b/ad4836f44713a6bf3f36f4f1965ac1ab756a124dfb40844d5a4b94342bcc/oslo_service-4.8.0.tar.gz", hash = "sha256:5f82357f4c8b20bf40b530262c87b829da4ab1631dfe6e5801ecf6e5d86c60a9", size = 130092, upload-time = "2026-07-31T14:39:01.937Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/70/76ec072c4afd07b9be2d114a9bce02a12b2065ff03fbefd5b26d73f21a86/oslo_service-4.4.1-py3-none-any.whl", hash = "sha256:6e5922bc0875b1aa98c8fe3dbd7f92725ef28a3100a793e69d813fdcf13fc1bf", size = 102176, upload-time = "2025-11-19T14:54:20.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/47/1d0ee8fc415c183724b5631b1db83fcc5f147900ace1a01e6ace84f43d6e/oslo_service-4.8.0-py3-none-any.whl", hash = "sha256:81501deb989f9801aada393d3e425486cc72295b228bcc7f49bb54caabfd47d5", size = 125105, upload-time = "2026-07-31T14:39:00.691Z" }, ] [package.optional-dependencies] @@ -1239,7 +1248,7 @@ threading = [ [[package]] name = "oslo-upgradecheck" -version = "2.6.0" +version = "2.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "oslo-config" }, @@ -1248,17 +1257,16 @@ dependencies = [ { name = "oslo-utils" }, { name = "prettytable" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/26/5d/32e01be549bfd63c55efc9610973ad8eecd0a2195d881755f4739d222dce/oslo_upgradecheck-2.6.0.tar.gz", hash = "sha256:189246e7ef9aa8fa57e5a4df6b2168ff69572571b43bded63faaff676a8542d6", size = 19855, upload-time = "2025-08-25T13:03:03.446Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/a3/3a5fe5f6e870de5c1a350ec019369b1ee50a99b45973ed318207e4139e8c/oslo_upgradecheck-2.8.0.tar.gz", hash = "sha256:7656ecc54a08191f86d9a21eb9bdf43fb7dc305b9cef4c9aa577a027f2ee3740", size = 21144, upload-time = "2026-07-10T12:47:56.525Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/8d/47873e44a142d625b1371fcdec9f412adaa7aa2d01306b704040ddbb3592/oslo_upgradecheck-2.6.0-py3-none-any.whl", hash = "sha256:460e7400e4963daf627f54c2809af20a122f158255c4d8be579d150ad66eb24e", size = 14425, upload-time = "2025-08-25T13:03:02.609Z" }, + { url = "https://files.pythonhosted.org/packages/c1/04/c5e11e46d38f1557f1ad480f77ca63d219c7a636d1c4ab75446f7d39eecc/oslo_upgradecheck-2.8.0-py3-none-any.whl", hash = "sha256:41462a597102ddf51d2169bdf5d85d1a3eb7be4f2ae483dd4074f8a9965f02a8", size = 14920, upload-time = "2026-07-10T12:47:55.451Z" }, ] [[package]] name = "oslo-utils" -version = "9.2.0" +version = "10.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "debtcollector" }, { name = "iso8601" }, { name = "netaddr" }, { name = "oslo-i18n" }, @@ -1267,37 +1275,46 @@ dependencies = [ { name = "psutil" }, { name = "pyparsing" }, { name = "pyyaml" }, - { name = "tzdata" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/12/fe/22b08a1234826f3baace1d46494f764625a6e6753b8e4b3809aa93ae35b3/oslo_utils-9.2.0.tar.gz", hash = "sha256:1f42006de6b9291a0305025f5ccdb8907120d88ed7ebc3784a1472a3328048ab", size = 142524, upload-time = "2025-11-20T15:10:12.268Z" } +sdist = { url = "https://files.pythonhosted.org/packages/47/fd/7915fc0e2c959bcb3dbbe608deeeefe4264d03ba89a4fd75d74524326652/oslo_utils-10.2.0.tar.gz", hash = "sha256:ba839dea2c1eb415e3ee151c4cc688f52e283f59bc1f77cc86772190a8b34259", size = 159551, upload-time = "2026-08-24T13:57:03.827Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/79/e8f828d0743240979b3a85aa6d77ce1250b25f62f0c273f7049a4d8b055c/oslo_utils-9.2.0-py3-none-any.whl", hash = "sha256:2d80bae493b1c3b3b1f43d25410e251579fa839bb1177b820af7a7b9450a7d47", size = 138034, upload-time = "2025-11-20T15:10:11.131Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b4/f2b480738b33cf9e8c04326116d1c6557cf17a87e4414edcefad70bfc4dc/oslo_utils-10.2.0-py3-none-any.whl", hash = "sha256:f33abd2f3c638c00f8736dfdff83a34f433bdb71e3b448d4380809a329917919", size = 154352, upload-time = "2026-08-24T13:57:02.7Z" }, ] [[package]] name = "oslo-versionedobjects" -version = "3.8.0" +version = "3.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "netaddr" }, - { name = "oslo-concurrency" }, { name = "oslo-config" }, { name = "oslo-context" }, { name = "oslo-i18n" }, { name = "oslo-log" }, { name = "oslo-messaging" }, - { name = "oslo-serialization" }, { name = "oslo-utils" }, - { name = "webob" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bb/7e/d349326371ff424ed58ae7201fe2e153557216df82dc28bbcd2038a0dc1a/oslo_versionedobjects-3.8.0.tar.gz", hash = "sha256:b66e45f62e1d7852183ce570a7f927149b8bf909f218f48ec12d43dcbdb27ffa", size = 154983, upload-time = "2025-08-25T12:48:55.446Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/de/2881d6845b0a72f54e5a30d90dbe0404193819249eb73179f6d5053673c7/oslo_versionedobjects-3.12.0.tar.gz", hash = "sha256:7bf14e5845d2f27670ef3b6d9c8757b00363f7f9f1404d23ee969ade70f6ab99", size = 177349, upload-time = "2026-08-24T13:56:52.562Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/24/f8/a40f5180b07c303715077af7e94479fc94e07cfb0738abc5a1b14be08b0e/oslo_versionedobjects-3.8.0-py3-none-any.whl", hash = "sha256:9b0fc25b23e1011ceefa365d0490b47048d077ab6e9d6e8955ff00186491803f", size = 86279, upload-time = "2025-08-25T12:48:54.482Z" }, + { url = "https://files.pythonhosted.org/packages/77/38/2009e4173dedb4d1bb6ea6849284a4b391ec63141dbc85649b0ee6edf70e/oslo_versionedobjects-3.12.0-py3-none-any.whl", hash = "sha256:d5e7c5f6bcde2e8510dfc25764dac2f4325739a059ca1aebd5f160bf783b971a", size = 108861, upload-time = "2026-08-24T13:56:51.258Z" }, +] + +[[package]] +name = "oslotest" +version = "6.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fixtures" }, + { name = "testtools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/32/0ed2a2bc04500d0cc6ec9170a3f120a2574a21a3ed921fab54adece2495b/oslotest-6.1.1.tar.gz", hash = "sha256:5ce97347734f08ca626bb496962a8bc7adbae9693730f5859d9c5293d4a2fd80", size = 34131, upload-time = "2026-04-20T07:55:51.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/65/ff3900e9a80c6d32143e9410d4410435f90ae217eed3b1ac34ae7640637c/oslotest-6.1.1-py3-none-any.whl", hash = "sha256:572a2f2689ecab86062a761951f634a66721d43d8306b38b1cbe65cea7fddedc", size = 30090, upload-time = "2026-04-20T07:55:49.887Z" }, ] [[package]] name = "osprofiler" -version = "4.3.0" +version = "4.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "netaddr" }, @@ -1309,24 +1326,23 @@ dependencies = [ { name = "requests" }, { name = "webob" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/a7/1fede5a569035e237f87decb737a2e615b565888a7db7e8d05eb1c483e8f/osprofiler-4.3.0.tar.gz", hash = "sha256:77a8da2b23bb5f9048054bd5ccc45d0ac84574ca8a88ef120b8fac6e8864e24c", size = 95940, upload-time = "2025-05-28T17:58:34.49Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/71/9bd056d116d97edbf137afa4b4575bf4e069ec6be8abf69328ac296b1322/osprofiler-4.4.0.tar.gz", hash = "sha256:6e08df7eef6afc0d2d027813d40e43cb228f0eb3eb3441d61495c2800ebfa320", size = 96231, upload-time = "2026-05-05T14:10:59.309Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/07/01e28443724ddebe1f99d5ffb4bc634ff4651272c9c513db1d74c43b419c/osprofiler-4.3.0-py3-none-any.whl", hash = "sha256:762e6b127a8e198c7121f804a0d44f7d4b5c6310bff76bc4775c75a8445c9bb6", size = 95217, upload-time = "2025-05-28T17:58:33.29Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e0/a9a8222cb1733f33008e582cde5e0c3ed5109225301de7eaa6aa3c4ec75a/osprofiler-4.4.0-py3-none-any.whl", hash = "sha256:a7bbcfd4abe9521b31355b04a2ad43ff5d64bfcb2860b48f124e8f92e7168383", size = 95161, upload-time = "2026-05-05T14:10:57.875Z" }, ] [[package]] name = "ovs" -version = "3.6.1" +version = "4.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pywin32", marker = "sys_platform == 'win32'" }, { name = "sortedcontainers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c3/dc/46b8b62cc9e70ab7a369916d8db089382254a81c2a69dea59652b87f1fe8/ovs-3.6.1.tar.gz", hash = "sha256:d4a214df1d92c80a132b622573c2e760ffc73faf25950247fb2222e02057023d", size = 161786, upload-time = "2025-11-14T21:16:52.992Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dd/45/145eefde089bb3e9e7211e4705053f2e572c73255e059a9c7180f1fd73ca/ovs-4.0.0.tar.gz", hash = "sha256:c5fcb3f59383bf055368df6739db9f38c61c94c24086b7936a6b78668b9693cf", size = 155127, upload-time = "2026-08-17T20:01:24.336Z" } [[package]] name = "ovsdbapp" -version = "2.14.1" +version = "2.19.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fixtures" }, @@ -1334,23 +1350,23 @@ dependencies = [ { name = "ovs" }, { name = "pbr" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/39/70/863d8344ec892912922a7de72692236752f30ca92fdba84b0d7355057b9b/ovsdbapp-2.14.1.tar.gz", hash = "sha256:37eb3a1cb742c455507a5222b203341d6cf28830c79569dd8536cfe070718409", size = 128204, upload-time = "2025-11-24T17:21:03.136Z" } +sdist = { url = "https://files.pythonhosted.org/packages/13/03/08abd8115f35b9b3f4ba31e6022f312ee1e5c594adf3bebc194931aa8b62/ovsdbapp-2.19.0.tar.gz", hash = "sha256:554d091d52553a801756f178d91d108f29fe9936d13955de853c4cdc142d196d", size = 138978, upload-time = "2026-08-20T15:16:18.365Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/c9/fe6d067f601a8febece82f9ef70f6d470d3496e85349ada6b90de1ccacbb/ovsdbapp-2.14.1-py3-none-any.whl", hash = "sha256:b10ac74543e850c93ea4cb560a94ca530456700f0f7e94faece4c0ed6540c455", size = 145380, upload-time = "2025-11-24T17:21:02.036Z" }, + { url = "https://files.pythonhosted.org/packages/84/15/6567506e3cfa128b1e9058967c65fe9d954874156d5ccead936b5ec3c6f6/ovsdbapp-2.19.0-py3-none-any.whl", hash = "sha256:9c59fdc3ce029564294913d6aa8aa22e37b6c72f762dfe878f915c24d94dc67d", size = 158411, upload-time = "2026-08-20T15:16:17.059Z" }, ] [[package]] name = "packaging" -version = "25.0" +version = "26.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, ] [[package]] name = "paramiko" -version = "4.0.0" +version = "5.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "bcrypt" }, @@ -1358,9 +1374,9 @@ dependencies = [ { name = "invoke" }, { name = "pynacl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1f/e7/81fdcbc7f190cdb058cffc9431587eb289833bdd633e2002455ca9bb13d4/paramiko-4.0.0.tar.gz", hash = "sha256:6a25f07b380cc9c9a88d2b920ad37167ac4667f8d9886ccebd8f90f654b5d69f", size = 1630743, upload-time = "2025-08-04T01:02:03.711Z" } +sdist = { url = "https://files.pythonhosted.org/packages/62/93/dcc25d52f49022ae6175d15e6bd751f1acc99b98bc61fc55e5155a7be2e7/paramiko-5.0.0.tar.gz", hash = "sha256:36763b5b95c2a0dcfdf1abc48e48156ee425b21efe2f0e787c2dd5a95c0e5e79", size = 1548586, upload-time = "2026-05-09T18:28:52.256Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/90/a744336f5af32c433bd09af7854599682a383b37cfd78f7de263de6ad6cb/paramiko-4.0.0-py3-none-any.whl", hash = "sha256:0e20e00ac666503bf0b4eda3b6d833465a2b7aff2e2b3d79a8bba5ef144ee3b9", size = 223932, upload-time = "2025-08-04T01:02:02.029Z" }, + { url = "https://files.pythonhosted.org/packages/82/5b/eadf6d45de38d30ab603f49393b6cd2cbe7e233af8cf90197e32782b68a9/paramiko-5.0.0-py3-none-any.whl", hash = "sha256:b7044611c30140d9a75261653210e2002977b71a0497ff3ba0d98d7edbf62f7c", size = 208919, upload-time = "2026-05-09T18:28:50.295Z" }, ] [[package]] @@ -1386,37 +1402,37 @@ wheels = [ [[package]] name = "pbr" -version = "7.0.3" +version = "7.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "setuptools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5e/ab/1de9a4f730edde1bdbbc2b8d19f8fa326f036b4f18b2f72cfbea7dc53c26/pbr-7.0.3.tar.gz", hash = "sha256:b46004ec30a5324672683ec848aed9e8fc500b0d261d40a3229c2d2bbfcedc29", size = 135625, upload-time = "2025-11-03T17:04:56.274Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/58/74a362548619561fca4d29dd934b8ad295ba8b6a6cbc09612555a04ecafb/pbr-7.1.1.tar.gz", hash = "sha256:973da2aee9961d7cb278aa74e1829f6daafec9dd8ee8c1655cbf3f5843bfe083", size = 139972, upload-time = "2026-08-24T09:12:04.666Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/db/61efa0d08a99f897ef98256b03e563092d36cc38dc4ebe4a85020fe40b31/pbr-7.0.3-py2.py3-none-any.whl", hash = "sha256:ff223894eb1cd271a98076b13d3badff3bb36c424074d26334cd25aebeecea6b", size = 131898, upload-time = "2025-11-03T17:04:54.875Z" }, + { url = "https://files.pythonhosted.org/packages/3d/05/01750d3f53966e7b73ce6361f4d6ea99ae01b98f8ec23d142ba00b3e428d/pbr-7.1.1-py2.py3-none-any.whl", hash = "sha256:8e36e7b36cd0d081cca95c7b50d5760b17d7dd7d58a86044186d916e8be1f09d", size = 136517, upload-time = "2026-08-24T09:12:03.443Z" }, ] [[package]] name = "pecan" -version = "1.7.0" +version = "1.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mako" }, { name = "setuptools" }, { name = "webob" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2b/57/0eb9798962c4bf98819efca45293fb3d481ba748f1e83af165aa342cf2af/pecan-1.7.0.tar.gz", hash = "sha256:7de15bf4a2600dc584bed0f25431dfed6bf20a7a5bc935a48d2ce50063339815", size = 123025, upload-time = "2025-07-07T16:04:43.433Z" } +sdist = { url = "https://files.pythonhosted.org/packages/65/7a/1a69bbc37a5150d17d2955322797f0b3f1f2ca9757e53761c339caa8a4df/pecan-1.8.0.tar.gz", hash = "sha256:2f9fbcea86e8dbf1a2d28954225547634a289db7201e7764516a5dce86c116de", size = 122667, upload-time = "2026-03-12T21:57:27.11Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/3b/fbf52f6b1c57a92fd0c0fe60d25dce8219e368f7a35d2fec0becb2cf90cb/pecan-1.7.0-py3-none-any.whl", hash = "sha256:3dae9015f5a4e02375651b6b7d6110209c70aea0c827f309e421ea1c2fbb4ec2", size = 144590, upload-time = "2025-07-07T16:04:42.358Z" }, + { url = "https://files.pythonhosted.org/packages/c8/75/df5f578fd3b6b422e2edd5c9401ed3b533778d66e3d5761fe5138a851d17/pecan-1.8.0-py3-none-any.whl", hash = "sha256:eb3e2833d4c0295163d9cce57adc2736f5f8116946a4e68c2e4e6297fc1865de", size = 144352, upload-time = "2026-03-12T21:57:26.112Z" }, ] [[package]] name = "platformdirs" -version = "4.5.0" +version = "4.11.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" } +sdist = { url = "https://files.pythonhosted.org/packages/50/bb/ebc6636e1ae41314f796ebb7215fd28febb45f9aac72f2b04cb74b5071dc/platformdirs-4.11.4.tar.gz", hash = "sha256:f3373be828247211d0febabea97e238c3dfde8a60b3c90c32756fb52cb21556d", size = 34079, upload-time = "2026-08-24T14:53:49.676Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" }, + { url = "https://files.pythonhosted.org/packages/28/be/0ff05fcd2938fb58ad9219bd54135968342d214737e012d62d43f06a2dd6/platformdirs-4.11.4-py3-none-any.whl", hash = "sha256:e34ff91a24bcddc6d939b878bdf3f5c437c9c46fe9e212b1bf455fdf1ee57586", size = 23741, upload-time = "2026-08-24T14:53:48.406Z" }, ] [[package]] @@ -1430,124 +1446,136 @@ wheels = [ [[package]] name = "prettytable" -version = "3.17.0" +version = "3.18.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wcwidth" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/79/45/b0847d88d6cfeb4413566738c8bbf1e1995fad3d42515327ff32cc1eb578/prettytable-3.17.0.tar.gz", hash = "sha256:59f2590776527f3c9e8cf9fe7b66dd215837cca96a9c39567414cbc632e8ddb0", size = 67892, upload-time = "2025-11-14T17:33:20.212Z" } +sdist = { url = "https://files.pythonhosted.org/packages/81/74/ba08d81e668ccfe8658d7520a307e63c19862c08eb4ccb26f356c5239a7a/prettytable-3.18.0.tar.gz", hash = "sha256:439217116152244369caf3d9f1caf2f9fe29b03bd79e88d2928c8e718c95d680", size = 76373, upload-time = "2026-06-22T16:07:50.174Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/8c/83087ebc47ab0396ce092363001fa37c17153119ee282700c0713a195853/prettytable-3.17.0-py3-none-any.whl", hash = "sha256:aad69b294ddbe3e1f95ef8886a060ed1666a0b83018bbf56295f6f226c43d287", size = 34433, upload-time = "2025-11-14T17:33:19.093Z" }, + { url = "https://files.pythonhosted.org/packages/fe/be/2e6798ace5cc036f5d05d36b7b2fd85346f1a708c87060890b070d0ec607/prettytable-3.18.0-py3-none-any.whl", hash = "sha256:b3346e0e6f79180833aebaac088ae926340586cf6d7d991b9eb125b65f72313a", size = 37357, upload-time = "2026-06-22T16:07:48.595Z" }, ] [[package]] name = "prometheus-client" -version = "0.23.1" +version = "0.26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/73/f1334c29c2af4cd9dba6c7817e61b611bd0215e2eb5565c6064a4de18802/prometheus_client-0.26.0.tar.gz", hash = "sha256:04a91bcf94e2cf74a44a1a874d651a2e853ed354b6e822f3b7487751465d5c2b", size = 92910, upload-time = "2026-07-24T19:36:41.893Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/a3/b69efbf4143b5b9859b977770bbbabcc2796b702fa69dc40271e45cd5a56/prometheus_client-0.26.0-py3-none-any.whl", hash = "sha256:fa93d06737aa02bacd05794768508bb97d2fbee28cb3bca04eaae92f0ca953d6", size = 64494, upload-time = "2026-07-24T19:36:40.854Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.53" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/23/53/3edb5d68ecf6b38fcbcc1ad28391117d2a322d9a1a3eff04bfdb184d8c3b/prometheus_client-0.23.1.tar.gz", hash = "sha256:6ae8f9081eaaaf153a2e959d2e6c4f4fb57b12ef76c8c7980202f1e57b48b2ce", size = 80481, upload-time = "2025-09-18T20:47:25.043Z" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/ea/39b988c938f75cb75d7045b5c69f8bfed47ee2152c8837fb403de29d6fb8/prompt_toolkit-3.0.53.tar.gz", hash = "sha256:9ec8a0ad96d5c56148b3f914aa79c1564c3fde5d2e6b876e7bc327e353cf8fa6", size = 435492, upload-time = "2026-07-26T20:56:14.758Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl", hash = "sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99", size = 61145, upload-time = "2025-09-18T20:47:23.875Z" }, + { url = "https://files.pythonhosted.org/packages/54/6f/84908cad2d6aa5144abcf7b42709fe4fdb459bc640ec7ac5786e7693dabc/prompt_toolkit-3.0.53-py3-none-any.whl", hash = "sha256:01c0891d7f9237d5e339f7d3e42cdae80b7534abb1c7c0e3352efba6231492f2", size = 392288, upload-time = "2026-07-26T20:56:12.512Z" }, ] [[package]] name = "psutil" -version = "7.1.3" +version = "7.2.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e1/88/bdd0a41e5857d5d703287598cbf08dad90aed56774ea52ae071bae9071b6/psutil-7.1.3.tar.gz", hash = "sha256:6c86281738d77335af7aec228328e944b30930899ea760ecf33a4dba66be5e74", size = 489059, upload-time = "2025-11-02T12:25:54.619Z" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/94/46b9154a800253e7ecff5aaacdf8ebf43db99de4a2dfa18575b02548654e/psutil-7.1.3-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2bdbcd0e58ca14996a42adf3621a6244f1bb2e2e528886959c72cf1e326677ab", size = 238359, upload-time = "2025-11-02T12:26:25.284Z" }, - { url = "https://files.pythonhosted.org/packages/68/3a/9f93cff5c025029a36d9a92fef47220ab4692ee7f2be0fba9f92813d0cb8/psutil-7.1.3-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:bc31fa00f1fbc3c3802141eede66f3a2d51d89716a194bf2cd6fc68310a19880", size = 239171, upload-time = "2025-11-02T12:26:27.23Z" }, - { url = "https://files.pythonhosted.org/packages/ce/b1/5f49af514f76431ba4eea935b8ad3725cdeb397e9245ab919dbc1d1dc20f/psutil-7.1.3-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb428f9f05c1225a558f53e30ccbad9930b11c3fc206836242de1091d3e7dd3", size = 263261, upload-time = "2025-11-02T12:26:29.48Z" }, - { url = "https://files.pythonhosted.org/packages/e0/95/992c8816a74016eb095e73585d747e0a8ea21a061ed3689474fabb29a395/psutil-7.1.3-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56d974e02ca2c8eb4812c3f76c30e28836fffc311d55d979f1465c1feeb2b68b", size = 264635, upload-time = "2025-11-02T12:26:31.74Z" }, - { url = "https://files.pythonhosted.org/packages/55/4c/c3ed1a622b6ae2fd3c945a366e64eb35247a31e4db16cf5095e269e8eb3c/psutil-7.1.3-cp37-abi3-win_amd64.whl", hash = "sha256:f39c2c19fe824b47484b96f9692932248a54c43799a84282cfe58d05a6449efd", size = 247633, upload-time = "2025-11-02T12:26:33.887Z" }, - { url = "https://files.pythonhosted.org/packages/c9/ad/33b2ccec09bf96c2b2ef3f9a6f66baac8253d7565d8839e024a6b905d45d/psutil-7.1.3-cp37-abi3-win_arm64.whl", hash = "sha256:bd0d69cee829226a761e92f28140bec9a5ee9d5b4fb4b0cc589068dbfff559b1", size = 244608, upload-time = "2025-11-02T12:26:36.136Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, ] [[package]] name = "pycadf" -version = "4.0.1" +version = "4.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "oslo-config" }, { name = "oslo-serialization" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/97/09/c22a17ecfde3c6af5d5d2c5ebcd0ba81d1b6cdf9736a99ea7341ffa3fb33/pycadf-4.0.1.tar.gz", hash = "sha256:b015f1e4cfb6bd968bb8e23f903593328f0d82e1922902447197f4862f259d2f", size = 252343, upload-time = "2025-01-29T14:56:07.956Z" } +sdist = { url = "https://files.pythonhosted.org/packages/24/e7/dc9d02c4c2fe2570552273c778de7fdab86993ad2bea1bac82f576dd1218/pycadf-4.1.0.tar.gz", hash = "sha256:7f9be03e774ce3cc97675aac7c807c017cad6775961da7e9508f75bcc9f43267", size = 249070, upload-time = "2026-07-09T12:09:57.023Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/80/26/73c8a7b6be652a704de57bcc0811d1b9717b3ce693b99d8dc48c102a7cf0/pycadf-4.0.1-py3-none-any.whl", hash = "sha256:eb15577f6771392fa285ec0b2996434b1a91e91e347659878817927b92dbad0f", size = 44176, upload-time = "2025-01-29T14:56:06.2Z" }, + { url = "https://files.pythonhosted.org/packages/de/3b/83b1a93195da99b4b9320d4599f3e677003ea07f2110d3ded2744d24ce17/pycadf-4.1.0-py3-none-any.whl", hash = "sha256:505031bf95065f9b54c280772ff8fa6c5dd12b58357e625cea886b3e6828096f", size = 44280, upload-time = "2026-07-09T12:09:55.829Z" }, ] [[package]] name = "pycparser" -version = "2.23" +version = "3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] [[package]] name = "pygments" -version = "2.19.2" +version = "2.21.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, ] [[package]] name = "pyjwt" -version = "2.10.1" +version = "2.13.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, ] [[package]] name = "pynacl" -version = "1.6.1" +version = "1.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b2/46/aeca065d227e2265125aea590c9c47fbf5786128c9400ee0eb7c88931f06/pynacl-1.6.1.tar.gz", hash = "sha256:8d361dac0309f2b6ad33b349a56cd163c98430d409fa503b10b70b3ad66eaa1d", size = 3506616, upload-time = "2025-11-10T16:02:13.195Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/9a/4019b524b03a13438637b11538c82781a5eda427394380381af8f04f467a/pynacl-1.6.2.tar.gz", hash = "sha256:018494d6d696ae03c7e656e5e74cdfd8ea1326962cc401bcf018f1ed8436811c", size = 3511692, upload-time = "2026-01-01T17:48:10.851Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/49/41/3cfb3b4f3519f6ff62bf71bf1722547644bcfb1b05b8fdbdc300249ba113/pynacl-1.6.1-cp38-abi3-macosx_10_10_universal2.whl", hash = "sha256:a6f9fd6d6639b1e81115c7f8ff16b8dedba1e8098d2756275d63d208b0e32021", size = 387591, upload-time = "2025-11-10T16:01:49.1Z" }, - { url = "https://files.pythonhosted.org/packages/18/21/b8a6563637799f617a3960f659513eccb3fcc655d5fc2be6e9dc6416826f/pynacl-1.6.1-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e49a3f3d0da9f79c1bec2aa013261ab9fa651c7da045d376bd306cf7c1792993", size = 798866, upload-time = "2025-11-10T16:01:55.688Z" }, - { url = "https://files.pythonhosted.org/packages/e8/6c/dc38033bc3ea461e05ae8f15a81e0e67ab9a01861d352ae971c99de23e7c/pynacl-1.6.1-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7713f8977b5d25f54a811ec9efa2738ac592e846dd6e8a4d3f7578346a841078", size = 1398001, upload-time = "2025-11-10T16:01:57.101Z" }, - { url = "https://files.pythonhosted.org/packages/9f/05/3ec0796a9917100a62c5073b20c4bce7bf0fea49e99b7906d1699cc7b61b/pynacl-1.6.1-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a3becafc1ee2e5ea7f9abc642f56b82dcf5be69b961e782a96ea52b55d8a9fc", size = 834024, upload-time = "2025-11-10T16:01:50.228Z" }, - { url = "https://files.pythonhosted.org/packages/f0/b7/ae9982be0f344f58d9c64a1c25d1f0125c79201634efe3c87305ac7cb3e3/pynacl-1.6.1-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4ce50d19f1566c391fedc8dc2f2f5be265ae214112ebe55315e41d1f36a7f0a9", size = 1436766, upload-time = "2025-11-10T16:01:51.886Z" }, - { url = "https://files.pythonhosted.org/packages/b4/51/b2ccbf89cf3025a02e044dd68a365cad593ebf70f532299f2c047d2b7714/pynacl-1.6.1-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:543f869140f67d42b9b8d47f922552d7a967e6c116aad028c9bfc5f3f3b3a7b7", size = 817275, upload-time = "2025-11-10T16:01:53.351Z" }, - { url = "https://files.pythonhosted.org/packages/a8/6c/dd9ee8214edf63ac563b08a9b30f98d116942b621d39a751ac3256694536/pynacl-1.6.1-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a2bb472458c7ca959aeeff8401b8efef329b0fc44a89d3775cffe8fad3398ad8", size = 1401891, upload-time = "2025-11-10T16:01:54.587Z" }, - { url = "https://files.pythonhosted.org/packages/0f/c1/97d3e1c83772d78ee1db3053fd674bc6c524afbace2bfe8d419fd55d7ed1/pynacl-1.6.1-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:3206fa98737fdc66d59b8782cecc3d37d30aeec4593d1c8c145825a345bba0f0", size = 772291, upload-time = "2025-11-10T16:01:58.111Z" }, - { url = "https://files.pythonhosted.org/packages/4d/ca/691ff2fe12f3bb3e43e8e8df4b806f6384593d427f635104d337b8e00291/pynacl-1.6.1-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:53543b4f3d8acb344f75fd4d49f75e6572fce139f4bfb4815a9282296ff9f4c0", size = 1370839, upload-time = "2025-11-10T16:01:59.252Z" }, - { url = "https://files.pythonhosted.org/packages/30/27/06fe5389d30391fce006442246062cc35773c84fbcad0209fbbf5e173734/pynacl-1.6.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:319de653ef84c4f04e045eb250e6101d23132372b0a61a7acf91bac0fda8e58c", size = 791371, upload-time = "2025-11-10T16:02:01.075Z" }, - { url = "https://files.pythonhosted.org/packages/2c/7a/e2bde8c9d39074a5aa046c7d7953401608d1f16f71e237f4bef3fb9d7e49/pynacl-1.6.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:262a8de6bba4aee8a66f5edf62c214b06647461c9b6b641f8cd0cb1e3b3196fe", size = 1363031, upload-time = "2025-11-10T16:02:02.656Z" }, - { url = "https://files.pythonhosted.org/packages/dd/b6/63fd77264dae1087770a1bb414bc604470f58fbc21d83822fc9c76248076/pynacl-1.6.1-cp38-abi3-win32.whl", hash = "sha256:9fd1a4eb03caf8a2fe27b515a998d26923adb9ddb68db78e35ca2875a3830dde", size = 226585, upload-time = "2025-11-10T16:02:07.116Z" }, - { url = "https://files.pythonhosted.org/packages/12/c8/b419180f3fdb72ab4d45e1d88580761c267c7ca6eda9a20dcbcba254efe6/pynacl-1.6.1-cp38-abi3-win_amd64.whl", hash = "sha256:a569a4069a7855f963940040f35e87d8bc084cb2d6347428d5ad20550a0a1a21", size = 238923, upload-time = "2025-11-10T16:02:04.401Z" }, - { url = "https://files.pythonhosted.org/packages/35/76/c34426d532e4dce7ff36e4d92cb20f4cbbd94b619964b93d24e8f5b5510f/pynacl-1.6.1-cp38-abi3-win_arm64.whl", hash = "sha256:5953e8b8cfadb10889a6e7bd0f53041a745d1b3d30111386a1bb37af171e6daf", size = 183970, upload-time = "2025-11-10T16:02:05.786Z" }, + { url = "https://files.pythonhosted.org/packages/be/7b/4845bbf88e94586ec47a432da4e9107e3fc3ce37eb412b1398630a37f7dd/pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl", hash = "sha256:c949ea47e4206af7c8f604b8278093b674f7c79ed0d4719cc836902bf4517465", size = 388458, upload-time = "2026-01-01T17:32:16.829Z" }, + { url = "https://files.pythonhosted.org/packages/1e/b4/e927e0653ba63b02a4ca5b4d852a8d1d678afbf69b3dbf9c4d0785ac905c/pynacl-1.6.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8845c0631c0be43abdd865511c41eab235e0be69c81dc66a50911594198679b0", size = 800020, upload-time = "2026-01-01T17:32:18.34Z" }, + { url = "https://files.pythonhosted.org/packages/7f/81/d60984052df5c97b1d24365bc1e30024379b42c4edcd79d2436b1b9806f2/pynacl-1.6.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22de65bb9010a725b0dac248f353bb072969c94fa8d6b1f34b87d7953cf7bbe4", size = 1399174, upload-time = "2026-01-01T17:32:20.239Z" }, + { url = "https://files.pythonhosted.org/packages/68/f7/322f2f9915c4ef27d140101dd0ed26b479f7e6f5f183590fd32dfc48c4d3/pynacl-1.6.2-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46065496ab748469cdd999246d17e301b2c24ae2fdf739132e580a0e94c94a87", size = 835085, upload-time = "2026-01-01T17:32:22.24Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d0/f301f83ac8dbe53442c5a43f6a39016f94f754d7a9815a875b65e218a307/pynacl-1.6.2-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a66d6fb6ae7661c58995f9c6435bda2b1e68b54b598a6a10247bfcdadac996c", size = 1437614, upload-time = "2026-01-01T17:32:23.766Z" }, + { url = "https://files.pythonhosted.org/packages/c4/58/fc6e649762b029315325ace1a8c6be66125e42f67416d3dbd47b69563d61/pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:26bfcd00dcf2cf160f122186af731ae30ab120c18e8375684ec2670dccd28130", size = 818251, upload-time = "2026-01-01T17:32:25.69Z" }, + { url = "https://files.pythonhosted.org/packages/c9/a8/b917096b1accc9acd878819a49d3d84875731a41eb665f6ebc826b1af99e/pynacl-1.6.2-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c8a231e36ec2cab018c4ad4358c386e36eede0319a0c41fed24f840b1dac59f6", size = 1402859, upload-time = "2026-01-01T17:32:27.215Z" }, + { url = "https://files.pythonhosted.org/packages/85/42/fe60b5f4473e12c72f977548e4028156f4d340b884c635ec6b063fe7e9a5/pynacl-1.6.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:68be3a09455743ff9505491220b64440ced8973fe930f270c8e07ccfa25b1f9e", size = 791926, upload-time = "2026-01-01T17:32:29.314Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f9/e40e318c604259301cc091a2a63f237d9e7b424c4851cafaea4ea7c4834e/pynacl-1.6.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8b097553b380236d51ed11356c953bf8ce36a29a3e596e934ecabe76c985a577", size = 1363101, upload-time = "2026-01-01T17:32:31.263Z" }, + { url = "https://files.pythonhosted.org/packages/48/47/e761c254f410c023a469284a9bc210933e18588ca87706ae93002c05114c/pynacl-1.6.2-cp38-abi3-win32.whl", hash = "sha256:5811c72b473b2f38f7e2a3dc4f8642e3a3e9b5e7317266e4ced1fba85cae41aa", size = 227421, upload-time = "2026-01-01T17:32:33.076Z" }, + { url = "https://files.pythonhosted.org/packages/41/ad/334600e8cacc7d86587fe5f565480fde569dfb487389c8e1be56ac21d8ac/pynacl-1.6.2-cp38-abi3-win_amd64.whl", hash = "sha256:62985f233210dee6548c223301b6c25440852e13d59a8b81490203c3227c5ba0", size = 239754, upload-time = "2026-01-01T17:32:34.557Z" }, + { url = "https://files.pythonhosted.org/packages/29/7d/5945b5af29534641820d3bd7b00962abbbdfee84ec7e19f0d5b3175f9a31/pynacl-1.6.2-cp38-abi3-win_arm64.whl", hash = "sha256:834a43af110f743a754448463e8fd61259cd4ab5bbedcf70f9dabad1d28a394c", size = 184801, upload-time = "2026-01-01T17:32:36.309Z" }, ] [[package]] name = "pyopenssl" -version = "25.3.0" +version = "26.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/80/be/97b83a464498a79103036bc74d1038df4a7ef0e402cfaf4d5e113fb14759/pyopenssl-25.3.0.tar.gz", hash = "sha256:c981cb0a3fd84e8602d7afc209522773b94c1c2446a3c710a75b06fe1beae329", size = 184073, upload-time = "2025-09-17T00:32:21.037Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/e8/7325d258199b159eb2c03fe32107533e2832e70e63f4fb88a6aa00023201/pyopenssl-26.4.0.tar.gz", hash = "sha256:28dfcce0162b9211413e26dfbfdf1d24317fbeba18fc93c12400a1856b2a0bc7", size = 182046, upload-time = "2026-08-01T19:50:50.512Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/81/ef2b1dfd1862567d573a4fdbc9f969067621764fbb74338496840a1d2977/pyopenssl-25.3.0-py3-none-any.whl", hash = "sha256:1fda6fc034d5e3d179d39e59c1895c9faeaf40a79de5fc4cbbfbe0d36f4a77b6", size = 57268, upload-time = "2025-09-17T00:32:19.474Z" }, + { url = "https://files.pythonhosted.org/packages/51/ad/2cf6d3fa2fae5c79e1ed9960c0d42badd0f94d81dd12b50604cdc839e648/pyopenssl-26.4.0-py3-none-any.whl", hash = "sha256:f0eb0cb2d581d3ad2b9c489468485e7f2ab6727d08401bcf9d824c3caddf3c1c", size = 56026, upload-time = "2026-08-01T19:50:48.94Z" }, ] [[package]] name = "pyparsing" -version = "3.2.5" +version = "3.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/a5/181488fc2b9d093e3972d2a472855aae8a03f000592dbfce716a512b3359/pyparsing-3.2.5.tar.gz", hash = "sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6", size = 1099274, upload-time = "2025-09-21T04:11:06.277Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e", size = 113890, upload-time = "2025-09-21T04:11:04.117Z" }, + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, ] [[package]] @@ -1559,22 +1587,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" }, ] -[[package]] -name = "pyreadline3" -version = "3.5.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/49/4cea918a08f02817aabae639e3d0ac046fef9f9180518a3ad394e22da148/pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7", size = 99839, upload-time = "2024-09-19T02:40:10.062Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178, upload-time = "2024-09-19T02:40:08.598Z" }, -] - [[package]] name = "pyroute2" -version = "0.9.5" +version = "0.9.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/1019ddc278549fb7e64a16d7775e0f7d981135f762b8706e583414d655e3/pyroute2-0.9.5.tar.gz", hash = "sha256:a198ccbe545b031b00b10da4b44df33d548db04af944be8107c05a215ba03872", size = 969492, upload-time = "2025-11-02T18:42:49.603Z" } +dependencies = [ + { name = "win-inet-pton", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/3c/cae3aa8a07522d4fd625958f690ab6eb4ffbd9c94e30e2995f585fded630/pyroute2-0.9.6.tar.gz", hash = "sha256:6bc5e2ea9a372ded682b4ede4028ba00236bd6e35b42d833f39a96b219ef1db2", size = 478486, upload-time = "2026-04-15T18:26:07.408Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/66/995c65e7566543e941705f9b1b99ebfd7f9d3ed67c8eb7cb47f30168dc72/pyroute2-0.9.5-py3-none-any.whl", hash = "sha256:e7d485ce8274cbf473e9092fa65585faf8a3df5fe05ecf497cb9c5b1516ba09f", size = 480641, upload-time = "2025-11-02T18:42:45.75Z" }, + { url = "https://files.pythonhosted.org/packages/14/f5/77292e847cb2bcd94f0e7be214ad09972de5db6a6914e47117293ed0f4a8/pyroute2-0.9.6-py3-none-any.whl", hash = "sha256:3334091326e560a506635449af03b26920d22d4e5a7996aed354363d106fcef8", size = 483440, upload-time = "2026-04-15T18:26:03.14Z" }, ] [[package]] @@ -1595,16 +1617,16 @@ wheels = [ [[package]] name = "pytest-cov" -version = "7.0.0" +version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage" }, { name = "pluggy" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, ] [[package]] @@ -1633,7 +1655,7 @@ wheels = [ [[package]] name = "python-designateclient" -version = "6.3.0" +version = "7.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cliff" }, @@ -1647,14 +1669,14 @@ dependencies = [ { name = "requests" }, { name = "stevedore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/31/03/96a90b0a83c8e033a8d8d679e457acad18439f5de4a8a1b4a826ff02c412/python_designateclient-6.3.0.tar.gz", hash = "sha256:9aabf25e5ff69b15fb6209f9851c507d9499e63c6926245672ff0e495986e778", size = 72386, upload-time = "2025-05-16T13:29:09.286Z" } +sdist = { url = "https://files.pythonhosted.org/packages/92/66/9e39949850a7f086641508805ebeab1553451eabde353e05788288e3e80b/python_designateclient-7.0.0.tar.gz", hash = "sha256:d9a1086e7bf81f4034ca0ec7a243cbd8b344bfb6095e2903c553cc3807d2bed2", size = 75902, upload-time = "2026-07-01T11:50:13.876Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/91/dccb030a135eb2d766717a3fca33fa8979540157e54fc8efc32f793d1603/python_designateclient-6.3.0-py3-none-any.whl", hash = "sha256:e551e73b9eb0a76d9f75b5870d8f5a2d5eb1bba6e536c433822e35f661b98f8b", size = 95717, upload-time = "2025-05-16T13:29:07.871Z" }, + { url = "https://files.pythonhosted.org/packages/1b/c5/5385cdef7fb130495cb4a8ee1f16496cba32fff48f4c5b2f6aa3fc4837bd/python_designateclient-7.0.0-py3-none-any.whl", hash = "sha256:ab587ffb24f1bab7b5f36339c52ad4694c302c3fdaf54950b79564083ca94f4c", size = 99837, upload-time = "2026-07-01T11:50:12.777Z" }, ] [[package]] name = "python-keystoneclient" -version = "5.7.0" +version = "5.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "debtcollector" }, @@ -1668,14 +1690,14 @@ dependencies = [ { name = "requests" }, { name = "stevedore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/94/29/3775d7a722924a72208753a8aa5ddb0a58de24f5a5dd287cc9a0f66038e4/python_keystoneclient-5.7.0.tar.gz", hash = "sha256:8ce7bf1c8cddca6d7140fc76918b44eddf1d64040a60cb8ff7059136104d4ceb", size = 322387, upload-time = "2025-08-28T09:50:01.249Z" } +sdist = { url = "https://files.pythonhosted.org/packages/97/ef/c8c68219a2bf9f296ad18cb0b9804c45adfdceee72d51684225488746262/python_keystoneclient-5.8.0.tar.gz", hash = "sha256:3ca87c67c404298ce862310b569f545a58acf75cd5685094c82f35320b3a355d", size = 322844, upload-time = "2026-02-27T15:36:24.33Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/90/d1/eb39a6e544f5f789fd44c09274a8b1b08a86e3e7ac0e016d07ceb94c0b42/python_keystoneclient-5.7.0-py3-none-any.whl", hash = "sha256:2ec19994ba8b8b16fe6cf4ac45c3f2180a22e203d1b3aa977a34a0160caae0b6", size = 397216, upload-time = "2025-08-28T09:49:59.798Z" }, + { url = "https://files.pythonhosted.org/packages/be/35/483db648710bd0aeefd505db288490c229fef0e1fb7b78d8f277e2dc375b/python_keystoneclient-5.8.0-py3-none-any.whl", hash = "sha256:0cc96cc719bbd8b1ca8fda71e38017dba3e87ebe3b479fcc9261a67e30b21b85", size = 397325, upload-time = "2026-02-27T15:36:23.21Z" }, ] [[package]] name = "python-neutronclient" -version = "11.6.0" +version = "13.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cliff" }, @@ -1683,7 +1705,6 @@ dependencies = [ { name = "keystoneauth1" }, { name = "netaddr" }, { name = "openstacksdk" }, - { name = "os-client-config" }, { name = "osc-lib" }, { name = "oslo-i18n" }, { name = "oslo-log" }, @@ -1693,14 +1714,14 @@ dependencies = [ { name = "python-keystoneclient" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/57/15/96f2f42df4c1d6873c89a0cae2ba3b98f83273e965421eb11b7dbb257b4d/python_neutronclient-11.6.0.tar.gz", hash = "sha256:3c6958088d18c8676a10abf9d94b8dbf1a984741cbb988554f216880797e072f", size = 212450, upload-time = "2025-07-07T11:11:58.341Z" } +sdist = { url = "https://files.pythonhosted.org/packages/27/c4/e4eb2270cc288875f63339e32be98ac47bcb86e7913232149a9d483e7dbc/python_neutronclient-13.0.0.tar.gz", hash = "sha256:c5fd856adf3dc02cc5f31f9a76f4591d50af48cdfaad25b6e99b1291b543f95a", size = 179680, upload-time = "2026-07-09T08:41:21.028Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/76/487c81f1e07049faa110dd22cfa27f68644e933154340a6c750958dd73a9/python_neutronclient-11.6.0-py3-none-any.whl", hash = "sha256:df37f80a61ed0fb068551a120bb26ead0cefefdf101bf59693c556f4a2828229", size = 296139, upload-time = "2025-07-07T11:11:56.572Z" }, + { url = "https://files.pythonhosted.org/packages/da/27/f37c6ef70518438f6f1a9f28430c71616c2289024011b7d2b0ade69c205d/python_neutronclient-13.0.0-py3-none-any.whl", hash = "sha256:b1a64ef5f274a1fdbf5acf96b981847ed31ec8ada022e137a5953efa91bf4af9", size = 235780, upload-time = "2026-07-09T08:41:19.757Z" }, ] [[package]] name = "python-novaclient" -version = "18.11.0" +version = "18.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "iso8601" }, @@ -1712,19 +1733,9 @@ dependencies = [ { name = "prettytable" }, { name = "stevedore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5c/8a/ac14136fe0a549416b69e6111d6e45c7d821508037922baefa2d54b661f0/python_novaclient-18.11.0.tar.gz", hash = "sha256:0a31ae20779d4cd171bb29c1fe4e6b22b9c7d97c79670db5149bbdfac03f57dc", size = 340209, upload-time = "2025-09-01T13:31:59.62Z" } +sdist = { url = "https://files.pythonhosted.org/packages/23/86/6c6d5230eade1cdd10456d08108f783b00394edace0f1f20e5177169ba98/python_novaclient-18.13.0.tar.gz", hash = "sha256:812272fa36fae042c5c195aaeeaf0e5df2ca77edcb9c9e1fd66943f39d32bc4b", size = 340695, upload-time = "2026-05-13T09:20:40.796Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/1f/f944e5f61083af25176b46296522085b0faa17381d0251fd9c0f9f6288d0/python_novaclient-18.11.0-py3-none-any.whl", hash = "sha256:fc4ce032a6ce0fb911f4c640d260318d770ee171eedce7c12f887307fc24dc0c", size = 335871, upload-time = "2025-09-01T13:31:57.87Z" }, -] - -[[package]] -name = "pywin32" -version = "311" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, - { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/96/36/f070b4e8ce76888855a9b405bfd7f07a97c43db155d01524f0802bba2ff1/python_novaclient-18.13.0-py3-none-any.whl", hash = "sha256:e46abb6d2a77c8f547691a9f49e66216dc03847bc2b0d1884cefba58a0bca750", size = 336073, upload-time = "2026-05-13T09:20:39.308Z" }, ] [[package]] @@ -1761,16 +1772,16 @@ wheels = [ [[package]] name = "repoze-lru" -version = "0.7" +version = "0.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/12/bc/595a77c4b5e204847fdf19268314ef59c85193a9dc9f83630fc459c0fee5/repoze.lru-0.7.tar.gz", hash = "sha256:0429a75e19380e4ed50c0694e26ac8819b4ea7851ee1fc7583c8572db80aff77", size = 19591, upload-time = "2017-09-07T05:01:00.949Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/19/727d5c904ea513a6a9044bf43beaa2c5e632e017b1e0ab71e9d111d20967/repoze_lru-0.8.tar.gz", hash = "sha256:a252408cd93fe670c88d6665b96fe5d42e071dba2507a1f21a1e609ae4fa891a", size = 22169, upload-time = "2026-05-16T16:57:28.315Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/30/6cc0c95f0b59ad4b3b9163bff7cdcf793cc96fac64cf398ff26271f5cf5e/repoze.lru-0.7-py3-none-any.whl", hash = "sha256:f77bf0e1096ea445beadd35f3479c5cff2aa1efe604a133e67150bc8630a62ea", size = 10985, upload-time = "2017-09-07T05:01:00.007Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a1/cc0848069e3937651582c74db0fdddda96f853060a25617754a264913afe/repoze_lru-0.8-py3-none-any.whl", hash = "sha256:979a30d2e567e31f292009ba4467aa444c89ee0da3e3013980c35f1fb4f19d99", size = 6379, upload-time = "2026-05-16T16:57:27.257Z" }, ] [[package]] name = "requests" -version = "2.33.1" +version = "2.34.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -1778,9 +1789,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] [[package]] @@ -1794,27 +1805,27 @@ wheels = [ [[package]] name = "rich" -version = "14.2.0" +version = "15.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, ] [[package]] name = "rich-argparse" -version = "1.7.2" +version = "1.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "rich" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4c/f7/1c65e0245d4c7009a87ac92908294a66e7e7635eccf76a68550f40c6df80/rich_argparse-1.7.2.tar.gz", hash = "sha256:64fd2e948fc96e8a1a06e0e72c111c2ce7f3af74126d75c0f5f63926e7289cd1", size = 38500, upload-time = "2025-11-01T10:35:44.232Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/e5/1064c43203a357d668cd42435f7a15fe6af51512d85b2104fecb937aa861/rich_argparse-1.8.0.tar.gz", hash = "sha256:679df3d832fa94ad6e4bdb07ded088cd7ea2dddc58ae9b2b46346a40b06cbc0c", size = 38940, upload-time = "2026-05-01T15:18:43.604Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/80/97b6f357ac458d9ad9872cc3183ca09ef7439ac89e030ea43053ba1294b6/rich_argparse-1.7.2-py3-none-any.whl", hash = "sha256:0559b1f47a19bbeb82bf15f95a057f99bcbbc98385532f57937f9fc57acc501a", size = 25476, upload-time = "2025-11-01T10:35:42.681Z" }, + { url = "https://files.pythonhosted.org/packages/0b/35/1cceccc5fcb50fa2ed53e2aa278cd032f3902682a73e763fb1ac3be8e6fa/rich_argparse-1.8.0-py3-none-any.whl", hash = "sha256:d2a3ce7854654e2253c578763ab0a32f05016f23a55fadba7b9a91b6c0e92142", size = 25616, upload-time = "2026-05-01T15:18:42.395Z" }, ] [[package]] @@ -1832,25 +1843,25 @@ wheels = [ [[package]] name = "rpds-py" -version = "0.29.0" +version = "2026.6.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/33/23b3b3419b6a3e0f559c7c0d2ca8fc1b9448382b25245033788785921332/rpds_py-0.29.0.tar.gz", hash = "sha256:fe55fe686908f50154d1dc599232016e50c243b438c3b7432f24e2895b0e5359", size = 69359, upload-time = "2025-11-16T14:50:39.532Z" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/50/bc0e6e736d94e420df79be4deb5c9476b63165c87bb8f19ef75d100d21b3/rpds_py-0.29.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0891cfd8db43e085c0ab93ab7e9b0c8fee84780d436d3b266b113e51e79f954", size = 376000, upload-time = "2025-11-16T14:48:19.141Z" }, - { url = "https://files.pythonhosted.org/packages/3e/3a/46676277160f014ae95f24de53bed0e3b7ea66c235e7de0b9df7bd5d68ba/rpds_py-0.29.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3897924d3f9a0361472d884051f9a2460358f9a45b1d85a39a158d2f8f1ad71c", size = 360575, upload-time = "2025-11-16T14:48:20.443Z" }, - { url = "https://files.pythonhosted.org/packages/75/ba/411d414ed99ea1afdd185bbabeeaac00624bd1e4b22840b5e9967ade6337/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a21deb8e0d1571508c6491ce5ea5e25669b1dd4adf1c9d64b6314842f708b5d", size = 392159, upload-time = "2025-11-16T14:48:22.12Z" }, - { url = "https://files.pythonhosted.org/packages/8f/b1/e18aa3a331f705467a48d0296778dc1fea9d7f6cf675bd261f9a846c7e90/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9efe71687d6427737a0a2de9ca1c0a216510e6cd08925c44162be23ed7bed2d5", size = 410602, upload-time = "2025-11-16T14:48:23.563Z" }, - { url = "https://files.pythonhosted.org/packages/2f/6c/04f27f0c9f2299274c76612ac9d2c36c5048bb2c6c2e52c38c60bf3868d9/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:40f65470919dc189c833e86b2c4bd21bd355f98436a2cef9e0a9a92aebc8e57e", size = 515808, upload-time = "2025-11-16T14:48:24.949Z" }, - { url = "https://files.pythonhosted.org/packages/83/56/a8412aa464fb151f8bc0d91fb0bb888adc9039bd41c1c6ba8d94990d8cf8/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:def48ff59f181130f1a2cb7c517d16328efac3ec03951cca40c1dc2049747e83", size = 416015, upload-time = "2025-11-16T14:48:26.782Z" }, - { url = "https://files.pythonhosted.org/packages/04/4c/f9b8a05faca3d9e0a6397c90d13acb9307c9792b2bff621430c58b1d6e76/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad7bd570be92695d89285a4b373006930715b78d96449f686af422debb4d3949", size = 395325, upload-time = "2025-11-16T14:48:28.055Z" }, - { url = "https://files.pythonhosted.org/packages/34/60/869f3bfbf8ed7b54f1ad9a5543e0fdffdd40b5a8f587fe300ee7b4f19340/rpds_py-0.29.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:5a572911cd053137bbff8e3a52d31c5d2dba51d3a67ad902629c70185f3f2181", size = 410160, upload-time = "2025-11-16T14:48:29.338Z" }, - { url = "https://files.pythonhosted.org/packages/91/aa/e5b496334e3aba4fe4c8a80187b89f3c1294c5c36f2a926da74338fa5a73/rpds_py-0.29.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d583d4403bcbf10cffc3ab5cee23d7643fcc960dff85973fd3c2d6c86e8dbb0c", size = 425309, upload-time = "2025-11-16T14:48:30.691Z" }, - { url = "https://files.pythonhosted.org/packages/85/68/4e24a34189751ceb6d66b28f18159922828dd84155876551f7ca5b25f14f/rpds_py-0.29.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:070befbb868f257d24c3bb350dbd6e2f645e83731f31264b19d7231dd5c396c7", size = 574644, upload-time = "2025-11-16T14:48:31.964Z" }, - { url = "https://files.pythonhosted.org/packages/8c/cf/474a005ea4ea9c3b4f17b6108b6b13cebfc98ebaff11d6e1b193204b3a93/rpds_py-0.29.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fc935f6b20b0c9f919a8ff024739174522abd331978f750a74bb68abd117bd19", size = 601605, upload-time = "2025-11-16T14:48:33.252Z" }, - { url = "https://files.pythonhosted.org/packages/f4/b1/c56f6a9ab8c5f6bb5c65c4b5f8229167a3a525245b0773f2c0896686b64e/rpds_py-0.29.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8c5a8ecaa44ce2d8d9d20a68a2483a74c07f05d72e94a4dff88906c8807e77b0", size = 564593, upload-time = "2025-11-16T14:48:34.643Z" }, - { url = "https://files.pythonhosted.org/packages/b3/13/0494cecce4848f68501e0a229432620b4b57022388b071eeff95f3e1e75b/rpds_py-0.29.0-cp312-cp312-win32.whl", hash = "sha256:ba5e1aeaf8dd6d8f6caba1f5539cddda87d511331714b7b5fc908b6cfc3636b7", size = 223853, upload-time = "2025-11-16T14:48:36.419Z" }, - { url = "https://files.pythonhosted.org/packages/1f/6a/51e9aeb444a00cdc520b032a28b07e5f8dc7bc328b57760c53e7f96997b4/rpds_py-0.29.0-cp312-cp312-win_amd64.whl", hash = "sha256:b5f6134faf54b3cb83375db0f113506f8b7770785be1f95a631e7e2892101977", size = 239895, upload-time = "2025-11-16T14:48:37.956Z" }, - { url = "https://files.pythonhosted.org/packages/d1/d4/8bce56cdad1ab873e3f27cb31c6a51d8f384d66b022b820525b879f8bed1/rpds_py-0.29.0-cp312-cp312-win_arm64.whl", hash = "sha256:b016eddf00dca7944721bf0cd85b6af7f6c4efaf83ee0b37c4133bd39757a8c7", size = 230321, upload-time = "2025-11-16T14:48:39.71Z" }, + { url = "https://files.pythonhosted.org/packages/5c/be/2e8974163072e7bab7df1a5acd54c4498e75e35d6d18b864d3a9d5dadc92/rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0", size = 343691, upload-time = "2026-06-30T07:15:14.96Z" }, + { url = "https://files.pythonhosted.org/packages/a4/73/319dfa745dd668efe89309141ded489126461fcecd2b8f3a3cda185129b6/rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf", size = 338542, upload-time = "2026-06-30T07:15:16.267Z" }, + { url = "https://files.pythonhosted.org/packages/21/63/4239893be1c4d09b709b1a8f6be4188f0870084ff547f46606b8a75f1b03/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24", size = 368180, upload-time = "2026-06-30T07:15:17.62Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ca/9c5de382225234ceb37b1844ebdb140db12b2a278bb9efe2fcd19f6c82ce/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e", size = 375067, upload-time = "2026-06-30T07:15:18.952Z" }, + { url = "https://files.pythonhosted.org/packages/87/dc/863f69d1bf04ade34b7fe0d59b9fdf6f0135fe2d7cbca74f1d665589559d/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975", size = 490509, upload-time = "2026-06-30T07:15:20.434Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ef/eac16a12048b45ec7c7fa94f2be3438a5f26bf9cc8580b18a1cfd609b7f6/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680", size = 382754, upload-time = "2026-06-30T07:15:21.831Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6", size = 366189, upload-time = "2026-06-30T07:15:23.371Z" }, + { url = "https://files.pythonhosted.org/packages/e3/29/41a7b0e98a4b44cd676ab7598419623373eb43b20be68c084935c1a8cf88/rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a", size = 377750, upload-time = "2026-06-30T07:15:24.659Z" }, + { url = "https://files.pythonhosted.org/packages/2e/05/ecda0bec46f9a1565090bcdc941d023f6a25aff85fda28f89f8d19878152/rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4", size = 395576, upload-time = "2026-06-30T07:15:25.987Z" }, + { url = "https://files.pythonhosted.org/packages/68/a8/6ed52f03ee6cb854ce78785cc9a9a672eb880e83fd7224d471f667d151f1/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa", size = 543807, upload-time = "2026-06-30T07:15:27.356Z" }, + { url = "https://files.pythonhosted.org/packages/8f/d6/156c0d3eea27ba09b92562ba2364ba124c0a061b199e17eac637cd25a5e2/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc", size = 611187, upload-time = "2026-06-30T07:15:28.931Z" }, + { url = "https://files.pythonhosted.org/packages/f1/31/774212ed989c62f7f310220089f9b0a3fb8f40f5443d1727abd5d9f52bc9/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822", size = 573030, upload-time = "2026-06-30T07:15:30.553Z" }, + { url = "https://files.pythonhosted.org/packages/c9/50/22f73127a41f1ce4f87fe39aadfb9a126345801c274aa93ae88456249327/rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed", size = 202185, upload-time = "2026-06-30T07:15:32.027Z" }, + { url = "https://files.pythonhosted.org/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f", size = 220394, upload-time = "2026-06-30T07:15:33.359Z" }, + { url = "https://files.pythonhosted.org/packages/f3/83/3382fe37f809b59f02aac04dbc4e765b480b46ee0227ed516e3bdc4d3dfc/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96", size = 215753, upload-time = "2026-06-30T07:15:34.778Z" }, ] [[package]] @@ -1873,11 +1884,11 @@ wheels = [ [[package]] name = "setuptools" -version = "80.9.0" +version = "84.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/44/f5da03a8ef95d369145c5bb53050e7877c9f3d312e128605fd9504829143/setuptools-84.0.0.tar.gz", hash = "sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73", size = 1168449, upload-time = "2026-08-08T18:27:58.365Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/c510029fc6ef33a6275cd2c5d3cecd6613dfd6aa401d57c54f1c18852ccf/setuptools-84.0.0-py3-none-any.whl", hash = "sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670", size = 818216, upload-time = "2026-08-08T18:27:56.719Z" }, ] [[package]] @@ -1898,25 +1909,38 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, ] +[[package]] +name = "soupsieve" +version = "2.9.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/99/a6ca3beb3ccacb41fb3321d8a60e5566f9e6467601ef8eba6a17e1b89778/soupsieve-2.9.2.tar.gz", hash = "sha256:4a55d8cf158a9c2e587fa4922f1bbb91d68ac829e2d6f25403a85747c71daf74", size = 122445, upload-time = "2026-08-07T00:57:24.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/dc/ad025c1ee131eba60c69f4dd5779b18fcf1e6b21a343e2162a84d5d133c7/soupsieve-2.9.2-py3-none-any.whl", hash = "sha256:8089a26fd974ca7a1f30276d3d8492ab266ab15af581642dfe8aa162e0c1c823", size = 37370, upload-time = "2026-08-07T00:57:23.524Z" }, +] + [[package]] name = "sqlalchemy" -version = "2.0.44" +version = "2.0.52" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f0/f2/840d7b9496825333f532d2e3976b8eadbf52034178aac53630d09fe6e1ef/sqlalchemy-2.0.44.tar.gz", hash = "sha256:0ae7454e1ab1d780aee69fd2aae7d6b8670a581d8847f2d1e0f7ddfbf47e5a22", size = 9819830, upload-time = "2025-10-10T14:39:12.935Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/21/77b4c147963073040dc3c3a5cb7a8c3001a1893c0209432cb77f9df836aa/sqlalchemy-2.0.52.tar.gz", hash = "sha256:5e2d46356ac2ccb7d268ab6c2319ac6a2b42f1b8d5fd8bd3d46855cd82abee97", size = 9945637, upload-time = "2026-08-11T19:07:09.829Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/c4/59c7c9b068e6813c898b771204aad36683c96318ed12d4233e1b18762164/sqlalchemy-2.0.44-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72fea91746b5890f9e5e0997f16cbf3d53550580d76355ba2d998311b17b2250", size = 2139675, upload-time = "2025-10-10T16:03:31.064Z" }, - { url = "https://files.pythonhosted.org/packages/d6/ae/eeb0920537a6f9c5a3708e4a5fc55af25900216bdb4847ec29cfddf3bf3a/sqlalchemy-2.0.44-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:585c0c852a891450edbb1eaca8648408a3cc125f18cf433941fa6babcc359e29", size = 2127726, upload-time = "2025-10-10T16:03:35.934Z" }, - { url = "https://files.pythonhosted.org/packages/d8/d5/2ebbabe0379418eda8041c06b0b551f213576bfe4c2f09d77c06c07c8cc5/sqlalchemy-2.0.44-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b94843a102efa9ac68a7a30cd46df3ff1ed9c658100d30a725d10d9c60a2f44", size = 3327603, upload-time = "2025-10-10T15:35:28.322Z" }, - { url = "https://files.pythonhosted.org/packages/45/e5/5aa65852dadc24b7d8ae75b7efb8d19303ed6ac93482e60c44a585930ea5/sqlalchemy-2.0.44-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:119dc41e7a7defcefc57189cfa0e61b1bf9c228211aba432b53fb71ef367fda1", size = 3337842, upload-time = "2025-10-10T15:43:45.431Z" }, - { url = "https://files.pythonhosted.org/packages/41/92/648f1afd3f20b71e880ca797a960f638d39d243e233a7082c93093c22378/sqlalchemy-2.0.44-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0765e318ee9179b3718c4fd7ba35c434f4dd20332fbc6857a5e8df17719c24d7", size = 3264558, upload-time = "2025-10-10T15:35:29.93Z" }, - { url = "https://files.pythonhosted.org/packages/40/cf/e27d7ee61a10f74b17740918e23cbc5bc62011b48282170dc4c66da8ec0f/sqlalchemy-2.0.44-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2e7b5b079055e02d06a4308d0481658e4f06bc7ef211567edc8f7d5dce52018d", size = 3301570, upload-time = "2025-10-10T15:43:48.407Z" }, - { url = "https://files.pythonhosted.org/packages/3b/3d/3116a9a7b63e780fb402799b6da227435be878b6846b192f076d2f838654/sqlalchemy-2.0.44-cp312-cp312-win32.whl", hash = "sha256:846541e58b9a81cce7dee8329f352c318de25aa2f2bbe1e31587eb1f057448b4", size = 2103447, upload-time = "2025-10-10T15:03:21.678Z" }, - { url = "https://files.pythonhosted.org/packages/25/83/24690e9dfc241e6ab062df82cc0df7f4231c79ba98b273fa496fb3dd78ed/sqlalchemy-2.0.44-cp312-cp312-win_amd64.whl", hash = "sha256:7cbcb47fd66ab294703e1644f78971f6f2f1126424d2b300678f419aa73c7b6e", size = 2130912, upload-time = "2025-10-10T15:03:24.656Z" }, - { url = "https://files.pythonhosted.org/packages/9c/5e/6a29fa884d9fb7ddadf6b69490a9d45fded3b38541713010dad16b77d015/sqlalchemy-2.0.44-py3-none-any.whl", hash = "sha256:19de7ca1246fbef9f9d1bff8f1ab25641569df226364a0e40457dc5457c54b05", size = 1928718, upload-time = "2025-10-10T15:29:45.32Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d5/1b77a026d161f98a08f11af1a5f6c47b98ee7c7e2648af525a1004826c78/sqlalchemy-2.0.52-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:be8c49131665dfe2cc74c498aa1240ffb548d0fd901325dd11c2c7a18956f727", size = 2170940, upload-time = "2026-08-11T20:58:11.25Z" }, + { url = "https://files.pythonhosted.org/packages/54/bd/f444444adb37b5d53753fb1730ee7a421628e2e3b756c4da461af7e6394a/sqlalchemy-2.0.52-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b2d9e507a458832adcfbd8af6e2036ddf069b7710b799448542ebccae2dceee", size = 3383415, upload-time = "2026-08-11T21:02:38.534Z" }, + { url = "https://files.pythonhosted.org/packages/be/57/2eadf93a552568c57e8680b7e58bb5e9770d80942a1bdbaf4f2f63f0d7c8/sqlalchemy-2.0.52-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8738008376d22f30f411ea3efecf39b51110b6996d80bb73786f30bcfdd5fd3b", size = 3398577, upload-time = "2026-08-11T21:16:59.092Z" }, + { url = "https://files.pythonhosted.org/packages/15/c3/2887cf9dd111d1fbf05d22165b404c221ef43e029f7a2695e7302f27a7cc/sqlalchemy-2.0.52-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37a4d548327b6cab9c7d8cdb4e0e82feabee0110c4d150059068e2d1cfbd99ee", size = 3328225, upload-time = "2026-08-11T21:02:40.183Z" }, + { url = "https://files.pythonhosted.org/packages/02/0f/466bdf9e1feeeef5587f868c187d8687e21ff8c85b1775e9041130181132/sqlalchemy-2.0.52-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e49f51a5d59857a7a0dcaf9469febf7197d9394bd88f00d69c2c4e848112cdbf", size = 3357374, upload-time = "2026-08-11T21:17:01.076Z" }, + { url = "https://files.pythonhosted.org/packages/22/20/5c2b4583904af4173076dda1c9e53c9e2ffc7a702d2efde0216bbacbf7cb/sqlalchemy-2.0.52-cp312-cp312-win32.whl", hash = "sha256:afda3ec521d0517d0de783fc70030775841900896d832de5bbd066549290470e", size = 2129366, upload-time = "2026-08-11T21:14:50.991Z" }, + { url = "https://files.pythonhosted.org/packages/ed/06/543dab8ef62d4e9fb96fb31a30c2b8b14a8763bccf48d428294d6b3041c0/sqlalchemy-2.0.52-cp312-cp312-win_amd64.whl", hash = "sha256:2d5e53e36e37129fe0be8b9d08b6e4052c10a963ee6cda56c8c10dcc194b99ca", size = 2157344, upload-time = "2026-08-11T21:14:52.453Z" }, + { url = "https://files.pythonhosted.org/packages/b3/3f/3582293d1e185e71d19d7c731c3e2ee20ba21981c4a1115c0806c1f62120/sqlalchemy-2.0.52-py3-none-any.whl", hash = "sha256:3b81b8363a919ce53453591cdb93702e6bd54ade6c4fa2f468fc053baee5ed89", size = 1950700, upload-time = "2026-08-11T20:47:21.603Z" }, +] + +[package.optional-dependencies] +asyncio = [ + { name = "greenlet" }, ] [[package]] @@ -1930,59 +1954,54 @@ wheels = [ [[package]] name = "stevedore" -version = "5.6.0" +version = "5.9.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/5b/496f8abebd10c3301129abba7ddafd46c71d799a70c44ab080323987c4c9/stevedore-5.6.0.tar.gz", hash = "sha256:f22d15c6ead40c5bbfa9ca54aa7e7b4a07d59b36ae03ed12ced1a54cf0b51945", size = 516074, upload-time = "2025-11-20T10:06:07.264Z" } +sdist = { url = "https://files.pythonhosted.org/packages/db/a1/3b8ed9c1fc3aa6eebb57732d924ddaa0500ecc3b638d0454816320994383/stevedore-5.9.1.tar.gz", hash = "sha256:e97a2667923efda926e8713fde6a73616df68210a3cbc6f02b48967b676fd8bf", size = 518111, upload-time = "2026-08-20T15:25:14.754Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/40/8561ce06dc46fd17242c7724ab25b257a2ac1b35f4ebf551b40ce6105cfa/stevedore-5.6.0-py3-none-any.whl", hash = "sha256:4a36dccefd7aeea0c70135526cecb7766c4c84c473b1af68db23d541b6dc1820", size = 54428, upload-time = "2025-11-20T10:06:05.946Z" }, + { url = "https://files.pythonhosted.org/packages/c5/97/bba6e7ec2f5498b9dcb7b1b6400086b80ae5a8ebaff4b25e8c8add75f439/stevedore-5.9.1-py3-none-any.whl", hash = "sha256:5c8ff3a9f336cc1a06ac0f597bc79d11a2f950bfd32e290ca56b5a301fafafbf", size = 54931, upload-time = "2026-08-20T15:25:13.602Z" }, ] [[package]] name = "tenacity" -version = "9.1.2" +version = "9.1.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036, upload-time = "2025-04-02T08:25:09.966Z" } +sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, + { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, ] [[package]] name = "testresources" -version = "2.0.2" +version = "2.1.2" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pbr" }, +sdist = { url = "https://files.pythonhosted.org/packages/5f/f4/5e0ecad96a86bc58f7833fc26ae623a67c49148f56327b11dfddf30aa176/testresources-2.1.2.tar.gz", hash = "sha256:a2b1266fd6cb89295dbb79e3442e5ff1c27db35a0b62b84c7846002676d12ae2", size = 37654, upload-time = "2026-04-21T20:52:52.231Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/9e/3fb8523e5252cf07f6ae3c8ba685de9a03405b0ddffdd8e3e2a621e40b07/testresources-2.1.2-py3-none-any.whl", hash = "sha256:9158997b8373a80a19809858d052c3321108651c585f83c026cc801b1b34190d", size = 25364, upload-time = "2026-04-21T20:52:50.893Z" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b2/2e/905756faf6bada00adccb5dbc9e5987760675b682f08dd1312a40042a838/testresources-2.0.2.tar.gz", hash = "sha256:2cbf3d7e00ab2e9fe24b754a102644f6f334244980464c38233b18127f1deaec", size = 45057, upload-time = "2025-04-22T10:24:29.8Z" } [[package]] name = "testscenarios" -version = "0.5.0" +version = "0.6.2" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pbr" }, - { name = "testtools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f0/de/b0b5b98c0f38fd7086d082c47fcb455eedd39a044abe7c595f5f40cd6eed/testscenarios-0.5.0.tar.gz", hash = "sha256:c257cb6b90ea7e6f8fef3158121d430543412c9a87df30b5dde6ec8b9b57a2b6", size = 20951, upload-time = "2015-05-04T01:37:16.108Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/47750833ccb3bfd72f6082a644e0ea41fc0c54a6f5a0e98f73265af096eb/testscenarios-0.6.2.tar.gz", hash = "sha256:ab5ae8cd550e11ea978151981e7a8ecac329b7b22e4dec706b1e6fe213f463e7", size = 15698, upload-time = "2026-05-22T11:46:06.934Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/25/2f10da0d5427989fefa5ab51e697bc02625bbb7de2be3bc8452462efac78/testscenarios-0.5.0-py2.py3-none-any.whl", hash = "sha256:480263fa5d6e618125bdf092aab129a3aeed5996b1e668428f12cc56d6d01d28", size = 21002, upload-time = "2015-05-04T01:37:23.545Z" }, + { url = "https://files.pythonhosted.org/packages/34/cf/af3b3139963665af3bb6771c0f2762d0cd2726c5264ef56b3422853cfebb/testscenarios-0.6.2-py3-none-any.whl", hash = "sha256:54626a5aa3cd0cae2f5b689de2f1bac73376c926b1717c456d4d483f699ea4ba", size = 12187, upload-time = "2026-05-22T11:46:05.706Z" }, ] [[package]] name = "testtools" -version = "2.8.0" +version = "2.9.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/3c/e891c4b6c1824e1adf5b08ee1057925254226c42c83995780acdb67cd3ee/testtools-2.8.0.tar.gz", hash = "sha256:b4496f147a7b79440242496889a734179795df345b57d5aa406a09c90572dba1", size = 201854, upload-time = "2025-11-10T10:24:55.997Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/80/af955640b4e8348aaa8026f9f48304811ec3342d9928ddb611d38a190ea0/testtools-2.9.1.tar.gz", hash = "sha256:39ad9eb9e1b935d6838f4b3aee4d6e72db65df5602f6feda998dbb4fe8de0b19", size = 221377, upload-time = "2026-04-24T09:42:51.706Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/01/28/8e5564487da81d79191e7a73659b43a781ba12f1be81a0534c7e3939cedd/testtools-2.8.0-py3-none-any.whl", hash = "sha256:4371613e430ae5fbafcfc1396321c16c84cd9d4fec6d1dc5756ee07e097af7ac", size = 180244, upload-time = "2025-11-10T10:24:50.222Z" }, + { url = "https://files.pythonhosted.org/packages/f4/5f/80ea589bc04651ea10c0f53da06cb0ac33b616b0ec78bf7dc8d8fd642f5d/testtools-2.9.1-py3-none-any.whl", hash = "sha256:faf2d689b3614d87459f7f715fc8349a3f77a252486b104d0cfe635b44326895", size = 110306, upload-time = "2026-04-24T09:42:50.102Z" }, ] [[package]] name = "tooz" -version = "7.0.0" +version = "9.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "debtcollector" }, { name = "fasteners" }, { name = "futurist" }, { name = "msgpack" }, @@ -1992,36 +2011,36 @@ dependencies = [ { name = "tenacity" }, { name = "voluptuous" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/11/80/79441bdc556221f1491c2d4855d0170de170730a21d067d4edb2cbbd3678/tooz-7.0.0.tar.gz", hash = "sha256:af0aa21cb8b7bd561df3aea85b127e54858975314ecb69d1eac56a03e6e5b8d5", size = 102569, upload-time = "2025-06-05T13:24:30.968Z" } +sdist = { url = "https://files.pythonhosted.org/packages/80/8f/5598ffd345fc6899ad8e1dc4c8ca40310796cb3de3d33fcc6435c207ddb8/tooz-9.0.1.tar.gz", hash = "sha256:2c332f668ad910b46b54e7f1db98166a0a79ce8c4d8148206d503c4517d4ceb0", size = 105717, upload-time = "2026-07-30T17:10:09.392Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dd/2b/3117ab1da7742429ad4548e9abbb8e1d2819fde6a9c63c6d53863ec38c81/tooz-7.0.0-py3-none-any.whl", hash = "sha256:023b8692dd151984558a409151b3b97d3969fe4346e905b6ce58f408789fc62d", size = 93305, upload-time = "2025-06-05T13:24:28.434Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/dcc2bb5b2810a910bdbc5d5d0d66ccaa5878947efc4b53e03ce0fd78f80f/tooz-9.0.1-py3-none-any.whl", hash = "sha256:4eb79a63bf7ac3bd7caab3a96d03bd135d1d14f8bcc3913f358ac24d03a648fe", size = 90596, upload-time = "2026-07-30T17:10:08.27Z" }, ] [[package]] name = "typing-extensions" -version = "4.15.0" +version = "4.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ] [[package]] name = "tzdata" -version = "2025.2" +version = "2026.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674, upload-time = "2026-07-10T08:50:37.887Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, + { url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" }, ] [[package]] name = "urllib3" -version = "1.26.20" +version = "2.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/e8/6ff5e6bc22095cfc59b6ea711b687e2b7ed4bdb373f7eeec370a97d7392f/urllib3-1.26.20.tar.gz", hash = "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32", size = 307380, upload-time = "2024-08-29T15:43:11.37Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/cf/8435d5a7159e2a9c83a95896ed596f68cf798005fe107cc655b5c5c14704/urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e", size = 144225, upload-time = "2024-08-29T15:43:08.921Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] [[package]] @@ -2035,65 +2054,96 @@ wheels = [ [[package]] name = "voluptuous" -version = "0.15.2" +version = "0.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/91/af/a54ce0fb6f1d867e0b9f0efe5f082a691f51ccf705188fca67a3ecefd7f4/voluptuous-0.15.2.tar.gz", hash = "sha256:6ffcab32c4d3230b4d2af3a577c87e1908a714a11f6f95570456b1849b0279aa", size = 51651, upload-time = "2024-07-02T19:10:00.528Z" } +sdist = { url = "https://files.pythonhosted.org/packages/92/f4/0738e6849858deae22218be3bbb8207ba83a96e9d0ec7e8e8cd67b30e5ca/voluptuous-0.16.0.tar.gz", hash = "sha256:006535e22fed944aec17bef6e8725472476194743c87bd233e912eb463f8ff05", size = 54238, upload-time = "2025-12-18T23:18:46.08Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/db/a8/8f9cc6749331186e6a513bfe3745454f81d25f6e34c6024f88f80c71ed28/voluptuous-0.15.2-py3-none-any.whl", hash = "sha256:016348bc7788a9af9520b1764ebd4de0df41fe2138ebe9e06fa036bf86a65566", size = 31349, upload-time = "2024-07-02T19:09:58.125Z" }, + { url = "https://files.pythonhosted.org/packages/d1/00/0e0da784245c93cf346150ab67634177bf277f93b7a162bb56c928c39c04/voluptuous-0.16.0-py3-none-any.whl", hash = "sha256:ee342095263e1b5afbd4d418cb5adc92810eebfd07696bb033a261210df33db4", size = 31931, upload-time = "2025-12-18T23:18:44.694Z" }, +] + +[[package]] +name = "waitress" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/cb/04ddb054f45faa306a230769e868c28b8065ea196891f09004ebace5b184/waitress-3.0.2.tar.gz", hash = "sha256:682aaaf2af0c44ada4abfb70ded36393f0e307f4ab9456a215ce0020baefc31f", size = 179901, upload-time = "2024-11-16T20:02:35.195Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/57/a27182528c90ef38d82b636a11f606b0cbb0e17588ed205435f8affe3368/waitress-3.0.2-py3-none-any.whl", hash = "sha256:c56d67fd6e87c2ee598b76abdd4e96cfad1f24cacdea5078d382b1f9d7b5ed2e", size = 56232, upload-time = "2024-11-16T20:02:33.858Z" }, ] [[package]] name = "wcwidth" -version = "0.2.14" +version = "0.8.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" } +sdist = { url = "https://files.pythonhosted.org/packages/34/74/c6428f875774288bec1396f5bfcbc2d925700a4dad61727fd5f2b12f249d/wcwidth-0.8.2.tar.gz", hash = "sha256:91fbef97204b96a3d4d421609b80340b760cf33e26da123ff243d76b1fda8dda", size = 1466253, upload-time = "2026-06-29T18:11:11.601Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" }, + { url = "https://files.pythonhosted.org/packages/96/42/3e5985a0a7e57de470b320c6d6a1a67c844f6737a587f3d44dd13d1819e7/wcwidth-0.8.2-py3-none-any.whl", hash = "sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85", size = 323166, upload-time = "2026-06-29T18:11:09.888Z" }, ] [[package]] name = "webob" -version = "1.8.9" +version = "1.8.11" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/85/0b/1732085540b01f65e4e7999e15864fe14cd18b12a95731a43fd6fd11b26a/webob-1.8.9.tar.gz", hash = "sha256:ad6078e2edb6766d1334ec3dee072ac6a7f95b1e32ce10def8ff7f0f02d56589", size = 279775, upload-time = "2024-10-24T03:19:20.651Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/92/9329872e36be90e25ba616ac6e9ee8c1be4326a1ea3fdffcdabd0eb48efb/webob-1.8.11.tar.gz", hash = "sha256:aa8c27231070b135c025e567a9cd7eda03f4df71352ffaac740cb6a75f0f81a5", size = 286047, upload-time = "2026-08-02T06:26:26.063Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/bd/c336448be43d40be28e71f2e0f3caf7ccb28e2755c58f4c02c065bfe3e8e/WebOb-1.8.9-py2.py3-none-any.whl", hash = "sha256:45e34c58ed0c7e2ecd238ffd34432487ff13d9ad459ddfd77895e67abba7c1f9", size = 115364, upload-time = "2024-10-24T03:19:18.642Z" }, + { url = "https://files.pythonhosted.org/packages/c1/71/370a65bc7726a1cbf478445c898c7affe907aa42e2e9e5bb38319db74115/webob-1.8.11-py2.py3-none-any.whl", hash = "sha256:4addd1d38d6a7fbe0eda22d45f25a40d74c8b290f3a99c0ac3d4023cf21f2da2", size = 118319, upload-time = "2026-08-02T06:26:24.765Z" }, +] + +[[package]] +name = "webtest" +version = "3.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "waitress" }, + { name = "webob" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/85/73877efb10ba4ef02364d5671724b1326d482dc928eace5fe114215443b7/webtest-3.0.7.tar.gz", hash = "sha256:7aeab50f970d46c068e7a36dd162cb242591edf72a1d04efd21374772b931741", size = 80260, upload-time = "2025-10-06T20:38:59.293Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/74/cb337bb1758254589b6fdc0aeeb91fa987ac1e6877a79b75edbd214fc0a9/webtest-3.0.7-py3-none-any.whl", hash = "sha256:2f51a0844f3a8beaef89bc23d225fe05ad816f7e429ffcc655a13013a799ac6c", size = 32393, upload-time = "2025-10-06T20:38:57.728Z" }, +] + +[[package]] +name = "win-inet-pton" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/da/0b1487b5835497dea00b00d87c2aca168bb9ca2e2096981690239e23760a/win_inet_pton-1.1.0.tar.gz", hash = "sha256:dd03d942c0d3e2b1cf8bab511844546dfa5f74cb61b241699fa379ad707dea4f", size = 2949, upload-time = "2019-02-19T17:46:23.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/31/ff772a44aa56319df8afbb0b34f1a856f66f05b9d5f1fed917849e47fdae/win_inet_pton-1.1.0-py2.py3-none-any.whl", hash = "sha256:eaf0193cbe7152ac313598a0da7313fb479f769343c0c16c5308f64887dc885b", size = 4848, upload-time = "2019-02-19T17:46:22.182Z" }, ] [[package]] name = "wrapt" -version = "2.0.1" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/49/2a/6de8a50cb435b7f42c46126cf1a54b2aab81784e74c8595c8e025e8f36d3/wrapt-2.0.1.tar.gz", hash = "sha256:9c9c635e78497cacb81e84f8b11b23e0aacac7a136e73b8e5b2109a1d9fc468f", size = 82040, upload-time = "2025-11-07T00:45:33.312Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/b0/c1f5a970721f06b85c0cd5142e0ff8fe067708abd779b0c4f4be7d61d09f/wrapt-2.3.0.tar.gz", hash = "sha256:681a2d0eefd721998f90642762b8e75c2159ec531b20ad5e437245ea7b06a107", size = 131509, upload-time = "2026-07-28T06:06:14.895Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/73/8cb252858dc8254baa0ce58ce382858e3a1cf616acebc497cb13374c95c6/wrapt-2.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1fdbb34da15450f2b1d735a0e969c24bdb8d8924892380126e2a293d9902078c", size = 78129, upload-time = "2025-11-07T00:43:48.852Z" }, - { url = "https://files.pythonhosted.org/packages/19/42/44a0db2108526ee6e17a5ab72478061158f34b08b793df251d9fbb9a7eb4/wrapt-2.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3d32794fe940b7000f0519904e247f902f0149edbe6316c710a8562fb6738841", size = 61205, upload-time = "2025-11-07T00:43:50.402Z" }, - { url = "https://files.pythonhosted.org/packages/4d/8a/5b4b1e44b791c22046e90d9b175f9a7581a8cc7a0debbb930f81e6ae8e25/wrapt-2.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:386fb54d9cd903ee0012c09291336469eb7b244f7183d40dc3e86a16a4bace62", size = 61692, upload-time = "2025-11-07T00:43:51.678Z" }, - { url = "https://files.pythonhosted.org/packages/11/53/3e794346c39f462bcf1f58ac0487ff9bdad02f9b6d5ee2dc84c72e0243b2/wrapt-2.0.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7b219cb2182f230676308cdcacd428fa837987b89e4b7c5c9025088b8a6c9faf", size = 121492, upload-time = "2025-11-07T00:43:55.017Z" }, - { url = "https://files.pythonhosted.org/packages/c6/7e/10b7b0e8841e684c8ca76b462a9091c45d62e8f2de9c4b1390b690eadf16/wrapt-2.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:641e94e789b5f6b4822bb8d8ebbdfc10f4e4eae7756d648b717d980f657a9eb9", size = 123064, upload-time = "2025-11-07T00:43:56.323Z" }, - { url = "https://files.pythonhosted.org/packages/0e/d1/3c1e4321fc2f5ee7fd866b2d822aa89b84495f28676fd976c47327c5b6aa/wrapt-2.0.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe21b118b9f58859b5ebaa4b130dee18669df4bd111daad082b7beb8799ad16b", size = 117403, upload-time = "2025-11-07T00:43:53.258Z" }, - { url = "https://files.pythonhosted.org/packages/a4/b0/d2f0a413cf201c8c2466de08414a15420a25aa83f53e647b7255cc2fab5d/wrapt-2.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:17fb85fa4abc26a5184d93b3efd2dcc14deb4b09edcdb3535a536ad34f0b4dba", size = 121500, upload-time = "2025-11-07T00:43:57.468Z" }, - { url = "https://files.pythonhosted.org/packages/bd/45/bddb11d28ca39970a41ed48a26d210505120f925918592283369219f83cc/wrapt-2.0.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:b89ef9223d665ab255ae42cc282d27d69704d94be0deffc8b9d919179a609684", size = 116299, upload-time = "2025-11-07T00:43:58.877Z" }, - { url = "https://files.pythonhosted.org/packages/81/af/34ba6dd570ef7a534e7eec0c25e2615c355602c52aba59413411c025a0cb/wrapt-2.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a453257f19c31b31ba593c30d997d6e5be39e3b5ad9148c2af5a7314061c63eb", size = 120622, upload-time = "2025-11-07T00:43:59.962Z" }, - { url = "https://files.pythonhosted.org/packages/e2/3e/693a13b4146646fb03254636f8bafd20c621955d27d65b15de07ab886187/wrapt-2.0.1-cp312-cp312-win32.whl", hash = "sha256:3e271346f01e9c8b1130a6a3b0e11908049fe5be2d365a5f402778049147e7e9", size = 58246, upload-time = "2025-11-07T00:44:03.169Z" }, - { url = "https://files.pythonhosted.org/packages/a7/36/715ec5076f925a6be95f37917b66ebbeaa1372d1862c2ccd7a751574b068/wrapt-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:2da620b31a90cdefa9cd0c2b661882329e2e19d1d7b9b920189956b76c564d75", size = 60492, upload-time = "2025-11-07T00:44:01.027Z" }, - { url = "https://files.pythonhosted.org/packages/ef/3e/62451cd7d80f65cc125f2b426b25fbb6c514bf6f7011a0c3904fc8c8df90/wrapt-2.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:aea9c7224c302bc8bfc892b908537f56c430802560e827b75ecbde81b604598b", size = 58987, upload-time = "2025-11-07T00:44:02.095Z" }, - { url = "https://files.pythonhosted.org/packages/15/d1/b51471c11592ff9c012bd3e2f7334a6ff2f42a7aed2caffcf0bdddc9cb89/wrapt-2.0.1-py3-none-any.whl", hash = "sha256:4d2ce1bf1a48c5277d7969259232b57645aae5686dba1eaeade39442277afbca", size = 44046, upload-time = "2025-11-07T00:45:32.116Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4a/d17a0fad1bf1c5f2c887ff71fef75654141b0880bff71d157d955b5bec3a/wrapt-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0a45ffae742ce91a16e11cb6c7cd71e7f9994f3cbd283b962ab093f5c6dcf525", size = 82139, upload-time = "2026-07-28T06:04:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/6e/55/51b92daaf6defb57f4dc56bdcce985400f75c6984a03ca5e78ccac717028/wrapt-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:69e477046f2237ef0bc6547544ee73008dc764ca26eff44f09e976d221b34d5d", size = 82723, upload-time = "2026-07-28T06:04:36.502Z" }, + { url = "https://files.pythonhosted.org/packages/28/7f/cfd9bc4b1f5e424eeea83d0493e43f3b1b02707ce8e50c47945873982bd5/wrapt-2.3.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d221a6e6ddd302b8397433184e96b59f259f50024b854db1c411a881586b6b8", size = 172381, upload-time = "2026-07-28T06:04:37.674Z" }, + { url = "https://files.pythonhosted.org/packages/cb/89/ff7814f6eb6856b479946117d1138a2fbb46cdb6b1f379db359056c69743/wrapt-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:392158c9a7f2ab1b8699418bfc0fe6f83548788c418b27d7bf2019ad3405cebb", size = 174120, upload-time = "2026-07-28T06:04:38.987Z" }, + { url = "https://files.pythonhosted.org/packages/12/1e/8eded8615d39e3ce81f626937a3a87b280a2a86239a2bf14a4b4bb345034/wrapt-2.3.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e5301c35cf75655eb33498f2bd6ae8703ca19940e3167dc9cdf740c712a39c60", size = 163035, upload-time = "2026-07-28T06:04:40.361Z" }, + { url = "https://files.pythonhosted.org/packages/35/ea/a0af2d9da62897af2a055484920de05dade30d2ba2c0d65cbdea875d3d8b/wrapt-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:418f54bb09d1762db02c7009b4051149893af3153a87f92d70356703c11eea02", size = 171887, upload-time = "2026-07-28T06:04:41.614Z" }, + { url = "https://files.pythonhosted.org/packages/7e/dd/63cd4c864c65ef4906df64bd2d378f4a62b54f28063f282dfb3bf93caead/wrapt-2.3.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1598becd30f8f2777d18564064eb4f4dbe1ab0e05a8f09786d0ef505ac782bf3", size = 161113, upload-time = "2026-07-28T06:04:42.864Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ee/82f1fc9e431b5c2c5a6d201aa865dbeae3984c311c6d11a185f0c8367cf6/wrapt-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3da470536bf9645143323dd41b32db55c6f4304ad382094c1a1da8a92061e10d", size = 170530, upload-time = "2026-07-28T06:04:44.212Z" }, + { url = "https://files.pythonhosted.org/packages/37/a5/5dc590e863a419930d988f8b7ca3e75a6befcfb10b6003b3a152f3d5f732/wrapt-2.3.0-cp312-cp312-win32.whl", hash = "sha256:fb8e2e6704a1e0b1b989546c69e2688371ef4a07fa5f61bde3eb6211186f5ac1", size = 78323, upload-time = "2026-07-28T06:04:45.484Z" }, + { url = "https://files.pythonhosted.org/packages/51/f9/4a6925a07951df56394f7e6ebe14f69f1c5ef9d87aa63e0839acf15aa63a/wrapt-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:cdc021cb0b62471d6aac7f2bd92f3b4658073775f9ee7fcd325c511129e7bcc8", size = 81180, upload-time = "2026-07-28T06:04:47.021Z" }, + { url = "https://files.pythonhosted.org/packages/a8/4f/8b5de0395b2a72216751d41c9861df6facaeb611b619d8810ed2b3b23eb2/wrapt-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:67bfe2485f50368c3fcd2275fc1fd100e350d601e0058921a7c82678a465aeab", size = 80155, upload-time = "2026-07-28T06:04:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/00/39/3daf9f47be208606586de4568ba6713db53ebc8fd7a575aea1fe57983b69/wrapt-2.3.0-py3-none-any.whl", hash = "sha256:d8c7ed08477429752b8c44991f40ad7838b18332a160698740a6bfbc10d998a2", size = 61866, upload-time = "2026-07-28T06:06:12.9Z" }, ] [[package]] name = "yappi" -version = "1.7.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/00/76/264b149c6a11bb9094336c87d904953d4ba05eb0a4172e7e2d6523285a48/yappi-1.7.3.tar.gz", hash = "sha256:bef71ad0595b600261668dcb1e18b935a7117a724c04d7be60d9d246e32d0928", size = 59594, upload-time = "2025-10-24T11:17:20.156Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/59/ea/a4bdae99d0463dff28c53b07f9a98333064e28335a0513ad26c9f8b7464a/yappi-1.7.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:835ba8b6078aa480c185c43e70489d6f7730d3c3570a51451d1f9e29f0fdecf5", size = 32965, upload-time = "2025-10-24T11:16:36.217Z" }, - { url = "https://files.pythonhosted.org/packages/de/9e/ac369cf4b42676b24684cd7bb16a439b76a46d8d1fe5abe13f88c5dabe48/yappi-1.7.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a23419753961c523a4d98799e199b32e7102bc1b4bd2279199e63e9bc37c0874", size = 32901, upload-time = "2025-10-24T11:16:37.042Z" }, - { url = "https://files.pythonhosted.org/packages/e3/59/f85c08afbc3e0c344d27a00978438a15f384bf1f0bfc1761df66e5141edd/yappi-1.7.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b26c062c072ee1831d77cf06f8a812dc0cbcac63ef2f5b4cc6b4e67718603b6", size = 81700, upload-time = "2025-10-24T11:16:37.831Z" }, - { url = "https://files.pythonhosted.org/packages/76/ff/eb0a78fbea9aff0e5aa49932cc471381dd3bf9be500893d3fd72d17bf132/yappi-1.7.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54d17fb356f2cfbb4862901d05cf686bddd46d5e5d3b4bbcb3a0ead3d4fac77d", size = 81291, upload-time = "2025-10-24T11:16:39.083Z" }, - { url = "https://files.pythonhosted.org/packages/64/02/6df059b9c0d9932561e4a6e1e7495cc97a909895d0278b034eddbad1da62/yappi-1.7.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6c62600783a0cf659c46324264996489fc6348dfc113faaf31b7c9af8e747133", size = 79029, upload-time = "2025-10-24T11:16:39.953Z" }, - { url = "https://files.pythonhosted.org/packages/e8/ab/e2787c5d1261e08eec6682b76ba39d4a0ace5fa4e778f8255e82c6a7d4e0/yappi-1.7.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0cb41e4243907368d24081a4acd45d9dfa0867c8cb372491bb00d774f03fb21f", size = 79163, upload-time = "2025-10-24T11:16:41.203Z" }, - { url = "https://files.pythonhosted.org/packages/51/c0/ffb170e9e648c9d623648427e738a8d42ad708c7f2ad4413d84533bdfa7e/yappi-1.7.3-cp312-cp312-win32.whl", hash = "sha256:b93bbf4e6951c1ace9687ac6b027c4b14a581359f233b2dfb21108fe08650397", size = 32608, upload-time = "2025-10-24T11:16:42.11Z" }, - { url = "https://files.pythonhosted.org/packages/04/2c/5cf8208f904644568c1fb74ddd6ed3ed2eccfccb239f0d72386d1cb69c6f/yappi-1.7.3-cp312-cp312-win_amd64.whl", hash = "sha256:396c183dd0ab93532f2fe5c1742b96e136846d56c249b85e72b653af56d11352", size = 34962, upload-time = "2025-10-24T11:16:43.565Z" }, - { url = "https://files.pythonhosted.org/packages/bd/de/f4d21b20c1e6d825977888e1dcf77070709ece92c3e479deb10b55c61e31/yappi-1.7.3-cp312-cp312-win_arm64.whl", hash = "sha256:0317c9bf8908437578cb7661d2d5786043d2817b0377bb1e493669a1f1dbad76", size = 32591, upload-time = "2025-10-24T11:16:44.442Z" }, +version = "1.7.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/47/f7ec7744dff1104560d6276f951a8182f5b805e8d86ece591aebd0512845/yappi-1.7.6.tar.gz", hash = "sha256:c94281936af77c00c6ac2306a0e7f85a67e354d717120df85fcc5dfb9243d4dd", size = 62639, upload-time = "2026-03-17T22:31:40.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/0e/948fdb5980494c50fcf4d24cfd1d5c2ea9c48d4dd151e6b9c032d7ac5fed/yappi-1.7.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:56fae31c4e09448a9919c1e6a4f976b2a49aa914f42e6e95355f6329c83003da", size = 33256, upload-time = "2026-03-17T22:30:57.871Z" }, + { url = "https://files.pythonhosted.org/packages/3f/be/af330ad8ede549ecaa8b29d3a2185e2209b3a87056c05dbaaa7841497c6d/yappi-1.7.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:15ed0d845e30b35952d09dd4f70df81089db05b735d57c75e0924ebacda14a34", size = 33189, upload-time = "2026-03-17T22:30:58.733Z" }, + { url = "https://files.pythonhosted.org/packages/7a/80/220c5a34e3a4949cea0e0ff1049afab3a67f1bfb348db89a16bb74c77621/yappi-1.7.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e878f63781761db7b62265468d78147b95df9ca2af9bc5140f94ad0faffe11c", size = 82799, upload-time = "2026-03-17T22:30:59.649Z" }, + { url = "https://files.pythonhosted.org/packages/94/e0/e49b8e12140dba82767ee9fd8de1577be90a09a083aa6e0f02fdfc3e8ee9/yappi-1.7.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a88615e2b9817887f6d1addfd12466a8529f25acc58b656205ae3f641cd725b", size = 82322, upload-time = "2026-03-17T22:31:00.524Z" }, + { url = "https://files.pythonhosted.org/packages/1f/95/fdcabc38a3e70e7c6394d07868ff25c9984a1ea97020cc65d8858c71aa62/yappi-1.7.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:178b23a56a59ddd58a528848e9242040ecad6d2fe0bcfb439455eef02cd43b07", size = 79978, upload-time = "2026-03-17T22:31:01.456Z" }, + { url = "https://files.pythonhosted.org/packages/83/e6/6e11c44a90725a6f1afa4bc308618b2efc07a90d4c06e1d5a3b0125f8e6c/yappi-1.7.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62cbb47dffca45b906d52a3c8f02e508f67d657275fb9d897e1e736fe5afe25f", size = 80053, upload-time = "2026-03-17T22:31:02.366Z" }, + { url = "https://files.pythonhosted.org/packages/a2/79/f056c72ba190d186fe52eb6a9181aab15dbd68a868ab5c7375e3b5796565/yappi-1.7.6-cp312-cp312-win32.whl", hash = "sha256:dbcf79ee2f1a96ec52e8291c07e27c0e38eead61a5c24d57eb467b5d9e6f2f9f", size = 32906, upload-time = "2026-03-17T22:31:03.29Z" }, + { url = "https://files.pythonhosted.org/packages/0c/89/a9ee11f80482263b7268c8968a4e675393454167b20574831a357610afd4/yappi-1.7.6-cp312-cp312-win_amd64.whl", hash = "sha256:59bd23fb39a7b9027c5eecc94585042849cb36be9af2d35c31812be1408af356", size = 35204, upload-time = "2026-03-17T22:31:04.131Z" }, + { url = "https://files.pythonhosted.org/packages/3b/72/711a33a5bf45e5af484a48001b75908dade76d4b8ed5d180f89dc62c5a5a/yappi-1.7.6-cp312-cp312-win_arm64.whl", hash = "sha256:0adc831b099a554831335819d7eb1643e189e4f3aa2db873ca5d584bd42dba01", size = 32836, upload-time = "2026-03-17T22:31:05.116Z" }, ] From de4a7ee3d26a8caab4f204913f4bb62e795f9818 Mon Sep 17 00:00:00 2001 From: Doug Goldstein <doug.goldstein@rackspace.com> Date: Mon, 24 Aug 2026 16:11:45 -0500 Subject: [PATCH 02/21] test(neutron-understack): baremetal vif-attach binding scenarios Exercise the Ironic vif-attach lifecycle through the ML2 manager: hierarchical VXLAN->dynamic-VLAN binding across the understack and undersync drivers, refusal without physical_network, refusal of unsupported vnic_types, vif-detach reconcile, and port-delete segment release. Catalogued as BM-BIND-01..05 in SCENARIOS.md. BM-BIND-04 asserts vif-detach releases the dynamic VLAN segment, which the driver does not do today (it leaks, releasing only on delete). It is marked xfail(strict=True) and tracked by rackerlabs/understack#2239, so it flips to a failure once fixed, prompting removal of the marker. --- .../tests/scenarios/SCENARIOS.md | 88 +++++++-- .../tests/scenarios/test_baremetal_binding.py | 174 ++++++++++++++++++ 2 files changed, 250 insertions(+), 12 deletions(-) create mode 100644 python/neutron-understack/neutron_understack/tests/scenarios/test_baremetal_binding.py diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md b/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md index 33b46903d..daf513fd1 100644 --- a/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md +++ b/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md @@ -1,7 +1,7 @@ # ML2 mechanism scenario catalog This is the human-readable **test plan** for the `neutron-understack` ML2 mechanism -drivers. Each scenario is given a stable ID and describes the operation it models +drivers. Each scenario below has a stable ID and describes the operation it models and the **mechanism calls + data** we expect to observe. Every scenario here must be implemented by at least one test tagged @@ -10,16 +10,80 @@ that exists here. `test_scenario_coverage.py` enforces this equivalence in both directions, and `conftest.py` fails the run if any scenario test is missing a marker. So this file and the tests cannot silently drift apart. -## Conventions +Scenario IDs are declared as `### <ID> — <title>` headings. Ideas under +"Planned / future" are intentionally *not* IDs (no `###` heading) so they are not +treated as catalogued-but-untested. -- Scenario IDs are declared as `### <ID> — <title>` headings, e.g. - `### BM-BIND-01 — baremetal vif-attach binds hierarchically`. IDs use the form - `<AREA>-<TOPIC>-<NN>` (uppercase, hyphenated), which the coverage check parses. -- Ideas not yet implemented go under a "Planned / future" section as plain bullets - (no `###` heading) so they are not treated as catalogued-but-untested. -- A scenario whose desired behavior is a known bug is still catalogued; its test - asserts the desired behavior and is marked `@pytest.mark.xfail(strict=True)` with - a reason. Record such bugs under a "Known bugs" section. +## Baremetal port binding (Ironic vif-attach) -Scenarios are added per driver/area (baremetal port binding, routers, trunks, ...) -as those test modules land. +Runtime chain: `mechanism_drivers = ovn,understack,baremetal,undersync`. These +scenarios load `logger,understack,undersync` (OVN is only needed for router +scenarios). Tenant networks are VXLAN; the physnet named in the binding profile +(`physnet1` in tests) is a VLAN group. + +### BM-BIND-01 — baremetal vif-attach binds hierarchically +- given: a VXLAN tenant network and an unbound baremetal port +- when: the port is updated with `binding:host_id` + a binding profile carrying + `physical_network=<physnet>` and `local_link_information` (the Ironic vif-attach) +- then: + - `understack` binds the VXLAN segment and hands down a dynamically allocated + VLAN segment on `<physnet>` (`continue_binding`) + - `undersync` binds that VLAN segment (`set_binding`, `vif_type=OTHER`, + `status=ACTIVE`) + - two binding levels result: level 0 driver `understack` (VXLAN), + level 1 driver `undersync` (dynamic VLAN) + - `undersync.sync(<physnet>)` reconciles the switch + +### BM-BIND-02 — vif-attach without physical_network refuses binding +- given: a VXLAN tenant network and an unbound baremetal port +- when: vif-attach supplies a binding profile with **no** `physical_network` +- then: `understack` refuses to bind (no `continue_binding`), the port ends + `binding:vif_type=binding_failed` with no binding levels, and + `undersync.sync` is not called + +### BM-BIND-03 — unsupported vnic_type is not bound by us +- given: a VXLAN tenant network and a port with an unsupported vnic_type + (e.g. `direct`) +- when: vif-attach is attempted +- then: neither `understack` nor `undersync` binds it (`binding_failed`), and + `undersync.sync` is not called + +### BM-BIND-04 — vif-detach unbinds, reconciles, and releases the VLAN segment +- given: a bound baremetal port (per BM-BIND-01) +- when: the binding is cleared (`binding:host_id=""`, empty profile) +- then: the port returns to `binding:vif_type=unbound`, + `undersync.sync(<physnet>)` reconciles the switch, and the dynamic VLAN segment + is **released** so its VLAN id returns to the pool +- status: **KNOWN BUG (xfail)** — the driver does not release the segment on + detach today (see "Known bugs"). The test asserts the desired behavior and is + marked `xfail(strict=True)`, so it will start failing (prompting removal of the + xfail) once the bug is fixed. + +### BM-BIND-05 — port delete releases the dynamic VLAN segment +- given: a bound baremetal port (per BM-BIND-01) +- when: the port is deleted +- then: the dynamic VLAN segment is released (no ports remain bound to it) and + `undersync.sync(<physnet>)` reconciles the switch + +## Known bugs (surfaced by these tests) + +- **Dynamic VLAN segment leaks on vif-detach** (BM-BIND-04, `xfail`, + rackerlabs/understack#2239). On the + bound→unbound update, `_tenant_network_port_cleanup` releases + `original_top_bound_segment` — the VXLAN segment, which is not dynamic — instead + of the dynamic VLAN segment at the bottom binding level. So + `release_segment_if_unused` no-ops and the VLAN segment (and its VLAN id) is + never freed on detach. It is only released on port **delete** + (`_delete_port_baremetal`, BM-BIND-05), which contradicts the comment there that + says detach "normally" releases it. Result: VLAN ids leak whenever a port is + unbound without being deleted. Fix should release the dynamic (bottom) segment + on the detach transition; the xfail flips to a pass once fixed. + +## Planned / future (not yet catalogued IDs) + +- Router create + `add_router_interface`: assert the `network:router_interface` + port is created and `UnderstackDriver.create_port_postcommit` → + `routers.create_port_postcommit` runs with the expected data. Requires faking + the OVN IDL (`neutron_understack.routers.ovn_client()`). +- Trunk parent/subport binding: assert per-subport dynamic VLAN allocation and + `undersync.sync` calls. diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/test_baremetal_binding.py b/python/neutron-understack/neutron_understack/tests/scenarios/test_baremetal_binding.py new file mode 100644 index 000000000..5813ec048 --- /dev/null +++ b/python/neutron-understack/neutron_understack/tests/scenarios/test_baremetal_binding.py @@ -0,0 +1,174 @@ +"""Scenario tests for baremetal (Ironic) port binding through the ML2 chain. + +See neutron_understack/tests/scenarios/SCENARIOS.md for the catalog these tests +implement. Each test is tagged with its scenario ID via @pytest.mark.scenario. +""" + +import pytest +from neutron.db import segments_db +from neutron.plugins.ml2 import db as ml2_db +from neutron_lib import constants as p_const +from neutron_lib.api.definitions import portbindings + +from neutron_understack.tests.scenarios.base import DEFAULT_PHYSNET +from neutron_understack.tests.scenarios.base import UnderstackMl2ScenarioBase + +HOST = "host-1" + + +class TestBaremetalBinding(UnderstackMl2ScenarioBase): + def _make_vxlan_network(self): + return self._make_network(self.fmt, "vxlan-net", True)["network"] + + def _create_unbound_baremetal_port(self, net_id): + res = self._create_port( + self.fmt, + net_id, + arg_list=(portbindings.VNIC_TYPE,), + is_admin=True, + **{portbindings.VNIC_TYPE: portbindings.VNIC_BAREMETAL}, + ) + assert res.status_int == 201, res.body + return self.deserialize(self.fmt, res)["port"] + + def _vif_attach(self, port_id, physnet=DEFAULT_PHYSNET, host=HOST): + """Simulate the Ironic vif-attach: update the port with host + profile.""" + data = { + "port": { + portbindings.HOST_ID: host, + portbindings.PROFILE: self.baremetal_binding_profile(physnet=physnet), + } + } + # binding:host_id / binding:profile writes require the service role + # under secure RBAC (this is the identity Ironic/Nova use). + req = self.new_update_request("ports", data, port_id, as_service=True) + res = req.get_response(self.api) + assert res.status_int == 200, res.body + return self.deserialize(self.fmt, res)["port"] + + @pytest.mark.scenario("BM-BIND-01") + def test_baremetal_vif_attach_binds_hierarchically(self): + net = self._make_vxlan_network() + port = self._create_unbound_baremetal_port(net["id"]) + port_id = port["id"] + # A freshly created baremetal port with no host is unbound. + assert port[portbindings.VIF_TYPE] == portbindings.VIF_TYPE_UNBOUND + + updated = self._vif_attach(port_id) + + # undersync finalized the binding to VIF_TYPE_OTHER. + assert updated[portbindings.VIF_TYPE] == portbindings.VIF_TYPE_OTHER + + # Two hierarchical binding levels: understack bound the VXLAN segment + # (level 0, via continue_binding) and handed a dynamic VLAN segment to + # undersync, which bound it (level 1, via set_binding). + levels = ml2_db.get_binding_level_objs(self.context, port_id, HOST) + assert len(levels) == 2, levels + assert levels[0].driver == "understack" + assert levels[1].driver == "undersync" + + # Top segment is VXLAN (the tenant network segment). + top = segments_db.get_segment_by_id(self.context, levels[0].segment_id) + assert top[segments_db.NETWORK_TYPE] == p_const.TYPE_VXLAN + + # Bottom segment is a *dynamically allocated* VLAN segment on our physnet. + bottom = segments_db.get_segment_by_id(self.context, levels[1].segment_id) + assert bottom[segments_db.NETWORK_TYPE] == p_const.TYPE_VLAN + assert bottom[segments_db.PHYSICAL_NETWORK] == DEFAULT_PHYSNET + + # The switch reconcile fired for the port's VLAN group. + self.undersync_mock.sync.assert_any_call(DEFAULT_PHYSNET) + + @pytest.mark.scenario("BM-BIND-02") + def test_vif_attach_without_physical_network_refuses_binding(self): + """A binding profile missing physical_network cannot be bound.""" + net = self._make_vxlan_network() + port = self._create_unbound_baremetal_port(net["id"]) + + # physnet=None omits physical_network from the profile. + updated = self._vif_attach(port["id"], physnet=None) + + assert updated[portbindings.VIF_TYPE] == portbindings.VIF_TYPE_BINDING_FAILED + assert ml2_db.get_binding_level_objs(self.context, port["id"], HOST) == [] + self.undersync_mock.sync.assert_not_called() + + @pytest.mark.scenario("BM-BIND-03") + def test_unsupported_vnic_type_is_not_bound_by_us(self): + """A non-baremetal/normal vnic_type is refused by our drivers.""" + net = self._make_vxlan_network() + res = self._create_port( + self.fmt, + net["id"], + arg_list=(portbindings.VNIC_TYPE,), + is_admin=True, + **{portbindings.VNIC_TYPE: portbindings.VNIC_DIRECT}, + ) + assert res.status_int == 201, res.body + port = self.deserialize(self.fmt, res)["port"] + + updated = self._vif_attach(port["id"]) + + assert updated[portbindings.VIF_TYPE] == portbindings.VIF_TYPE_BINDING_FAILED + self.undersync_mock.sync.assert_not_called() + + # BUG rackerlabs/understack#2239: the dynamic VLAN segment leaks on + # vif-detach -- see SCENARIOS.md "Known bugs". This test asserts the *desired* + # behavior and currently fails. strict=True makes an unexpected pass fail the + # run, prompting removal of the xfail once the driver is fixed. + @pytest.mark.xfail( + strict=True, + reason=( + "understack#2239: dynamic VLAN segment leaks on vif-detach; " + "_tenant_network_port_cleanup releases original_top_bound_segment " + "(the non-dynamic VXLAN segment) instead of the dynamic VLAN segment" + ), + ) + @pytest.mark.scenario("BM-BIND-04") + def test_vif_detach_releases_segment_and_reconciles(self): + """Vif detach should unbind, reconcile the switch, AND release the VLAN. + + The bound->unbound transition must free the dynamic VLAN segment so its + VLAN id is not leaked back to the pool. Today the driver reconciles + (undersync.sync) but retains the segment (see the xfail reason). + """ + net = self._make_vxlan_network() + port = self._create_unbound_baremetal_port(net["id"]) + port_id = port["id"] + self._vif_attach(port_id) + + levels = ml2_db.get_binding_level_objs(self.context, port_id, HOST) + vlan_segment_id = levels[1].segment_id + self.undersync_mock.reset_mock() + + # vif detach: clear host + profile. + data = {"port": {portbindings.HOST_ID: "", portbindings.PROFILE: {}}} + req = self.new_update_request("ports", data, port_id, as_service=True) + res = req.get_response(self.api) + assert res.status_int == 200, res.body + updated = self.deserialize(self.fmt, res)["port"] + + assert updated[portbindings.VIF_TYPE] == portbindings.VIF_TYPE_UNBOUND + # The switch is reconciled for the port's VLAN group ... + self.undersync_mock.sync.assert_any_call(DEFAULT_PHYSNET) + # ... and the dynamic VLAN segment is released (no port uses it anymore). + assert segments_db.get_segment_by_id(self.context, vlan_segment_id) is None + + @pytest.mark.scenario("BM-BIND-05") + def test_port_delete_releases_dynamic_vlan_segment(self): + """Deleting a bound baremetal port releases its dynamic VLAN segment.""" + net = self._make_vxlan_network() + port = self._create_unbound_baremetal_port(net["id"]) + port_id = port["id"] + self._vif_attach(port_id) + + levels = ml2_db.get_binding_level_objs(self.context, port_id, HOST) + vlan_segment_id = levels[1].segment_id + assert segments_db.get_segment_by_id(self.context, vlan_segment_id) is not None + self.undersync_mock.reset_mock() + + self._delete("ports", port_id, as_admin=True) + + # The dynamic VLAN segment is released now that no port uses it ... + assert segments_db.get_segment_by_id(self.context, vlan_segment_id) is None + # ... and the switch is reconciled for that VLAN group. + self.undersync_mock.sync.assert_any_call(DEFAULT_PHYSNET) From eb6ac1996121bf5e7c4884d1c55a4d4d48034211 Mon Sep 17 00:00:00 2001 From: Doug Goldstein <doug.goldstein@rackspace.com> Date: Mon, 24 Aug 2026 16:11:45 -0500 Subject: [PATCH 03/21] ci(neutron-understack): scenario traceability report from JUnit Tag each scenario test's JUnit case with a `scenario` property, and for neutron-understack publish a scenario ID -> status traceability matrix to the CI job summary plus upload the JUnit as an artifact. scripts/scenario-report.py builds the matrix from the catalog + JUnit and labels xfailed scenarios as known bugs. SCENARIOS.md stays in-repo next to the tests as the source of truth; it is developer-facing and not published to the operator docs site. --- .github/workflows/code-test.yaml | 21 +++- .../tests/scenarios/conftest.py | 17 ++- scripts/scenario-report.py | 111 ++++++++++++++++++ 3 files changed, 143 insertions(+), 6 deletions(-) create mode 100644 scripts/scenario-report.py diff --git a/.github/workflows/code-test.yaml b/.github/workflows/code-test.yaml index c1b35e18e..7c410120c 100644 --- a/.github/workflows/code-test.yaml +++ b/.github/workflows/code-test.yaml @@ -58,13 +58,32 @@ jobs: if [ -e .stestr.conf ]; then uv run stestr run else - uv run pytest --cov --cov-report xml:coverage.xml + uv run pytest --cov --cov-report xml:coverage.xml --junitxml=junit.xml fi - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: coverage-${{ matrix.project }} path: python/${{ matrix.project }}/coverage.xml retention-days: 1 + # Publish the ML2 scenario traceability matrix (scenario ID -> status) to + # the job summary and keep the JUnit as an artifact. neutron-understack + # only: its scenario tests tag each JUnit case with a `scenario` property. + - name: ML2 scenario traceability report + if: ${{ always() && matrix.project == 'neutron-understack' }} + run: | + if [ -f junit.xml ]; then + uv run python ../../scripts/scenario-report.py \ + --catalog neutron_understack/tests/scenarios/SCENARIOS.md \ + --junit junit.xml >> "$GITHUB_STEP_SUMMARY" + else + echo "No junit.xml produced (tests did not run)." >> "$GITHUB_STEP_SUMMARY" + fi + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + if: ${{ always() && matrix.project == 'neutron-understack' }} + with: + name: scenario-junit-${{ matrix.project }} + path: python/${{ matrix.project }}/junit.xml + retention-days: 1 coverage-upload: diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/conftest.py b/python/neutron-understack/neutron_understack/tests/scenarios/conftest.py index 6632c1ee8..ef9490a1f 100644 --- a/python/neutron-understack/neutron_understack/tests/scenarios/conftest.py +++ b/python/neutron-understack/neutron_understack/tests/scenarios/conftest.py @@ -1,8 +1,11 @@ -"""Scenario-test enforcement hooks. +"""Scenario-test enforcement + JUnit tagging hooks. -Fails the collection if any test under the scenarios package is missing a -``@pytest.mark.scenario("<ID>")`` marker, so a scenario test cannot silently -escape the catalog↔marker coverage check in test_scenario_coverage.py. +- Fails collection if any test under the scenarios package is missing a + ``@pytest.mark.scenario("<ID>")`` marker, so a scenario test cannot silently + escape the catalog↔marker coverage check in test_scenario_coverage.py. +- Stamps each scenario test's ID onto ``user_properties`` so it surfaces in the + JUnit XML as ``<property name="scenario" value="<ID>"/>``. scripts/ + scenario-report.py turns that into a scenario→status traceability matrix. """ import pytest @@ -19,8 +22,12 @@ def pytest_collection_modifyitems(items): continue if path.rsplit("/", 1)[-1] in _EXEMPT_FILES: continue - if item.get_closest_marker("scenario") is None: + marker = item.get_closest_marker("scenario") + if marker is None: missing.append(item.nodeid) + continue + if marker.args: + item.user_properties.append(("scenario", marker.args[0])) if missing: raise pytest.UsageError( "scenario tests missing an @pytest.mark.scenario('<ID>') marker:\n " diff --git a/scripts/scenario-report.py b/scripts/scenario-report.py new file mode 100644 index 000000000..45da3b8f2 --- /dev/null +++ b/scripts/scenario-report.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +"""Render a scenario traceability matrix from a scenario catalog + JUnit XML. + +Reads a SCENARIOS.md catalog (``### <ID> — <title>`` headings) and, optionally, +a JUnit XML file produced by ``pytest --junitxml`` where each scenario test was +tagged with a ``scenario`` property (see the scenarios conftest.py). Emits a +markdown table mapping each catalogued scenario to its implementing test(s) and +last-run status, suitable for ``$GITHUB_STEP_SUMMARY`` or a docs page. + +Reporting only: always exits 0. The catalog<->marker equivalence is enforced +separately by test_scenario_coverage.py. +""" + +import argparse +import re +import sys +import xml.etree.ElementTree as ET +from pathlib import Path + +# Catalog IDs are declared as headings: "### BM-BIND-01 — title". +_CATALOG_RE = re.compile(r"^#{2,3}\s+([A-Z][A-Z0-9]*(?:-[A-Z0-9]+)+)\s*[—-]\s*(.*)$") + +_STATUS = { + "pass": "✅ pass", + "fail": "❌ fail", + "xfail": "⚠️ known bug (xfail)", + "skipped": "⏭️ skipped", + "missing": "❓ no test", + "notrun": "· not run", +} + + +def parse_catalog(path): + """Return ordered list of (id, title) from the catalog headings.""" + entries = [] + for line in Path(path).read_text().splitlines(): + match = _CATALOG_RE.match(line) + if match: + entries.append((match.group(1), match.group(2).strip())) + return entries + + +def _testcase_status(testcase): + if testcase.find("failure") is not None or testcase.find("error") is not None: + return "fail" + skipped = testcase.find("skipped") + if skipped is not None: + return "xfail" if (skipped.get("type") or "").endswith("xfail") else "skipped" + return "pass" + + +def parse_junit(path): + """Return {scenario_id: [(test_name, status), ...]} from JUnit XML.""" + results = {} + # Input is our own pytest JUnit output (trusted, CI-generated), not + # untrusted external XML, so stdlib ElementTree is fine here. + root = ET.parse(path).getroot() # noqa: S314 + for testcase in root.iter("testcase"): + sid = None + for prop in testcase.iter("property"): + if prop.get("name") == "scenario": + sid = prop.get("value") + break + if sid is None: + continue + results.setdefault(sid, []).append( + (testcase.get("name", "?"), _testcase_status(testcase)) + ) + return results + + +def render(catalog, results, junit_provided): + lines = [ + "## ML2 scenario traceability", + "", + "| Scenario | Title | Test | Status |", + "| --- | --- | --- | --- |", + ] + for sid, title in catalog: + runs = results.get(sid) + if not runs: + status_key = "missing" if junit_provided else "notrun" + lines.append(f"| `{sid}` | {title} | — | {_STATUS[status_key]} |") + continue + for test_name, status in runs: + lines.append(f"| `{sid}` | {title} | `{test_name}` | {_STATUS[status]} |") + # Surface any scenarios seen in JUnit that are not in the catalog. + catalogued = {sid for sid, _ in catalog} + for sid in sorted(set(results) - catalogued): + for test_name, status in results[sid]: + lines.append( + f"| `{sid}` (uncataloged!) | — | `{test_name}` | {_STATUS[status]} |" + ) + lines.append("") + return "\n".join(lines) + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--catalog", required=True, help="path to SCENARIOS.md") + parser.add_argument("--junit", help="path to pytest JUnit XML (optional)") + args = parser.parse_args(argv) + + catalog = parse_catalog(args.catalog) + results = parse_junit(args.junit) if args.junit else {} + sys.stdout.write(render(catalog, results, junit_provided=bool(args.junit))) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 58ce1887bedec27e7ff51d75f9ea224dddac3318 Mon Sep 17 00:00:00 2001 From: Doug Goldstein <doug.goldstein@rackspace.com> Date: Mon, 24 Aug 2026 16:21:23 -0500 Subject: [PATCH 04/21] test(neutron-understack): VRF router interface sync scenario (xfail) Add UnderstackMl2RouterScenarioBase (real L3 router + flavors plugin via ML2TestFramework) and a VRF-RTR-01 scenario: a VXLAN network with baremetal ports bound to two physnets, then a VRF router attached on the internal side. The test asserts undersync syncs both physnets so the switches carrying those ports are reconciled for the new router. It does not today, so the test is xfail(strict=True), tracked by rackerlabs/understack#2240. The VRF flavor is simulated by patching routers._router_has_flavor, as the existing router unit tests do. --- .../tests/scenarios/SCENARIOS.md | 18 +++ .../tests/scenarios/base.py | 103 +++++++++++------- .../scenarios/test_vrf_router_interface.py | 84 ++++++++++++++ 3 files changed, 164 insertions(+), 41 deletions(-) create mode 100644 python/neutron-understack/neutron_understack/tests/scenarios/test_vrf_router_interface.py diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md b/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md index daf513fd1..a161b9c8e 100644 --- a/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md +++ b/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md @@ -65,6 +65,24 @@ scenarios). Tenant networks are VXLAN; the physnet named in the binding profile - then: the dynamic VLAN segment is released (no ports remain bound to it) and `undersync.sync(<physnet>)` reconciles the switch +## Router interface (VRF flavor) + +These scenarios load a real L3 router + flavors plugin (`ML2TestFramework`). The +VRF (flavored) router path skips the OVN uplink work, so no OVN IDL fake is +needed. The flavored path is simulated by patching `routers._router_has_flavor`, +mirroring the existing unit tests. + +### VRF-RTR-01 — VRF router attach syncs bound baremetal port physnets +- given: a VXLAN network + subnet with baremetal ports bound to two different + physnets (`physnet1`, `physnet2`) +- when: a VRF router is created and the subnet is attached on the internal side +- then: undersync syncs each physnet carrying the network's baremetal ports so + the switches are reconciled for the new router +- status: **KNOWN BUG (xfail, rackerlabs/understack#2240)** — attaching the VRF + router interface does not sync those physnets today (`undersync.sync` is never + called for them). The test asserts the desired behavior and is + `xfail(strict=True)`. + ## Known bugs (surfaced by these tests) - **Dynamic VLAN segment leaks on vif-detach** (BM-BIND-04, `xfail`, diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/base.py b/python/neutron-understack/neutron_understack/tests/scenarios/base.py index bcc739f95..404a38f5c 100644 --- a/python/neutron-understack/neutron_understack/tests/scenarios/base.py +++ b/python/neutron-understack/neutron_understack/tests/scenarios/base.py @@ -1,21 +1,25 @@ """In-process ML2 scenario-test harness for the understack mechanism drivers. -These tests reuse neutron's own functional test base (``Ml2PluginV2TestCase``), -which stands up a real ``Ml2Plugin`` backed by in-memory SQLite. We load our two -mechanism drivers -- ``understack`` and ``undersync`` -- into the binding chain and -drive real ``create_network`` / ``create_port`` / bind operations through the ML2 -manager, so a scenario can assert *which* calls reach our drivers and *with what -data*. This is the same style upstream neutron uses to test its own ML2 drivers -(OVN, OVS). +These tests reuse neutron's own functional test bases, which stand up a real +``Ml2Plugin`` backed by in-memory SQLite. We load our two mechanism drivers -- +``understack`` and ``undersync`` -- into the binding chain and drive real +``create_network`` / ``create_port`` / router operations through the ML2 manager, +so a scenario can assert *which* calls reach our drivers and *with what data*. +This is the same style upstream neutron uses to test its own ML2 drivers. + +``UnderstackMl2ScenarioBase`` is the plain base (port binding scenarios). +``UnderstackMl2RouterScenarioBase`` adds a real L3 router + flavors plugin +(``ML2TestFramework``) for router scenarios. Only the genuine external edge on the binding path -- the Undersync HTTP client -- -is mocked. Router scenarios (a follow-up) additionally fake the OVN IDL that -``neutron_understack.routers`` reaches into. +is mocked. Router scenarios that reach the OVN uplink path would additionally +need the OVN IDL faked; the flavored (VRF) router path skips it. """ from unittest import mock from neutron.conf.plugins.ml2.drivers import driver_type +from neutron.tests.unit.plugins.ml2.base import ML2TestFramework from neutron.tests.unit.plugins.ml2.test_plugin import Ml2PluginV2TestCase from neutron_lib.plugins import directory from oslo_config import cfg @@ -23,49 +27,53 @@ from neutron_understack import config as understack_config from neutron_understack.undersync import Undersync -#: A physnet the type manager knows about (the parent setUp configures physnet1 -#: with VLAN range 1:100), so ``allocate_dynamic_segment`` succeeds for the VLAN -#: group named in a baremetal binding profile. +#: Physnets the type manager knows about (the parent setUp configures physnet1/2 +#: with VLAN ranges), so ``allocate_dynamic_segment`` succeeds for the VLAN group +#: named in a baremetal binding profile. DEFAULT_PHYSNET = "physnet1" +SECOND_PHYSNET = "physnet2" #: Stand-in switch identity for the binding profile's local_link_information. DEFAULT_SWITCH_ID = "11:22:33:44:55:66" DEFAULT_SWITCH_INFO = "a1-1-1.iad3.rackspace.net" -class UnderstackMl2ScenarioBase(Ml2PluginV2TestCase): - """Base class for understack ML2 scenario tests.""" - - # Order matters: understack performs hierarchical VXLAN->VLAN binding and +def _apply_understack_ml2_overrides(): + """Config overrides the plugin needs before it loads in setup_parent(). + + Tenant networks are VXLAN (matching production); dynamic VLAN segments come + from the physnet ranges the parent setUp configures. + """ + cfg.CONF.set_override("project_network_types", ["vxlan"], group="ml2") + # ml2_type_vxlan is only registered when the vxlan type driver loads, so + # register it here before overriding its (empty) vni_ranges. + driver_type.register_ml2_drivers_vxlan_opts(cfg.CONF) + cfg.CONF.set_override("vni_ranges", ["1000:2000"], group="ml2_type_vxlan") + + # UnderstackDriver.initialize() registers these itself, but we register up + # front so we can set values the driver reads while loading. + understack_config.register_ml2_understack_opts(cfg.CONF) + cfg.CONF.set_override( + "undersync_url", "http://undersync.test", group="ml2_understack" + ) + cfg.CONF.set_override("undersync_dry_run", False, group="ml2_understack") + cfg.CONF.set_override( + "provisioning_network", + "00000000-0000-0000-0000-000000000000", + group="ml2_understack", + ) + + +class _UnderstackMl2ScenarioMixin: + """Shared setUp + helpers for understack ML2 scenario bases.""" + + # Order matters: understack does hierarchical VXLAN->VLAN binding and # undersync finalizes the VLAN segment. 'logger' mirrors the production list. _mechanism_drivers = ["logger", "understack", "undersync"] def setUp(self): - # ml2 opts are registered at plugin import time, so these overrides are - # in place before the plugin loads inside setup_parent(). Tenant networks - # are VXLAN (matching production); dynamic VLAN segments come from the - # physnet ranges the parent setUp configures. - cfg.CONF.set_override("project_network_types", ["vxlan"], group="ml2") - # The ml2_type_vxlan group is only registered when the vxlan type driver - # loads, so register it here before overriding its (empty) vni_ranges. - driver_type.register_ml2_drivers_vxlan_opts(cfg.CONF) - cfg.CONF.set_override("vni_ranges", ["1000:2000"], group="ml2_type_vxlan") - - # UnderstackDriver.initialize() registers these itself, but we register - # up front so we can set values the driver reads while loading. - understack_config.register_ml2_understack_opts(cfg.CONF) - cfg.CONF.set_override( - "undersync_url", "http://undersync.test", group="ml2_understack" - ) - cfg.CONF.set_override("undersync_dry_run", False, group="ml2_understack") - cfg.CONF.set_override( - "provisioning_network", - "00000000-0000-0000-0000-000000000000", - group="ml2_understack", - ) - + _apply_understack_ml2_overrides() super().setUp() - # Replace the real HTTP Undersync client with a mock -- the single # genuine external edge on the binding path. The trunk driver captured # its own reference at construction (trunk.py), so patch that too. @@ -74,7 +82,8 @@ def setUp(self): self.understack_driver.undersync = self.undersync_mock self.understack_driver.trunk_driver.undersync = self.undersync_mock - def _mech_driver(self, name): + @staticmethod + def _mech_driver(name): """Return the loaded mechanism driver instance registered under ``name``.""" manager = directory.get_plugin().mechanism_manager for ext in manager.ordered_mech_drivers: @@ -102,3 +111,15 @@ def baremetal_binding_profile(physnet=DEFAULT_PHYSNET, port_id="Ethernet1/1"): if physnet is not None: profile["physical_network"] = physnet return profile + + +class UnderstackMl2ScenarioBase(_UnderstackMl2ScenarioMixin, Ml2PluginV2TestCase): + """Base for port-binding scenarios (no L3/router machinery).""" + + +class UnderstackMl2RouterScenarioBase(_UnderstackMl2ScenarioMixin, ML2TestFramework): + """Base for router scenarios: real L3RouterPlugin + flavors plugin. + + ``self.l3_plugin`` is the loaded L3 plugin and ``self._create_router()`` + creates a router directly (bypassing HTTP), both provided by ML2TestFramework. + """ diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/test_vrf_router_interface.py b/python/neutron-understack/neutron_understack/tests/scenarios/test_vrf_router_interface.py new file mode 100644 index 000000000..5ba970905 --- /dev/null +++ b/python/neutron-understack/neutron_understack/tests/scenarios/test_vrf_router_interface.py @@ -0,0 +1,84 @@ +"""Scenario tests for VRF router interface attach vs. bound baremetal ports. + +See neutron_understack/tests/scenarios/SCENARIOS.md (VRF-RTR-*) for the catalog. +""" + +from unittest import mock + +import pytest +from neutron_lib.api.definitions import portbindings + +from neutron_understack.tests.scenarios.base import DEFAULT_PHYSNET +from neutron_understack.tests.scenarios.base import SECOND_PHYSNET +from neutron_understack.tests.scenarios.base import UnderstackMl2RouterScenarioBase + + +class TestVrfRouterInterface(UnderstackMl2RouterScenarioBase): + def _bind_baremetal_port(self, net_id, physnet, host): + """Create a baremetal port on the network and vif-attach it to physnet.""" + res = self._create_port( + self.fmt, + net_id, + arg_list=(portbindings.VNIC_TYPE,), + is_admin=True, + **{portbindings.VNIC_TYPE: portbindings.VNIC_BAREMETAL}, + ) + assert res.status_int == 201, res.body + port_id = self.deserialize(self.fmt, res)["port"]["id"] + data = { + "port": { + portbindings.HOST_ID: host, + portbindings.PROFILE: self.baremetal_binding_profile(physnet=physnet), + } + } + req = self.new_update_request("ports", data, port_id, as_service=True) + assert req.get_response(self.api).status_int == 200 + return port_id + + # BUG: attaching a VRF router interface to a network does not reconcile the + # switches carrying that network's already-bound baremetal ports -- undersync + # is never asked to sync those physnets. This test asserts the desired + # behavior and currently fails; strict=True flips it to a failure (prompting + # removal of the xfail) once the driver syncs on router attach. + @pytest.mark.xfail( + strict=True, + reason=( + "understack#2240: VRF router interface attach does not sync the " + "physnets of baremetal ports already bound on the network" + ), + ) + @pytest.mark.scenario("VRF-RTR-01") + def test_vrf_router_interface_syncs_bound_port_physnets(self): + net = self._make_network(self.fmt, "vxlan-net", True) + net_id = net["network"]["id"] + subnet = self._make_subnet( + self.fmt, net, gateway="10.0.0.1", cidr="10.0.0.0/24" + )["subnet"] + + # Two baremetal ports on this network, bound to different physnets. + self._bind_baremetal_port(net_id, DEFAULT_PHYSNET, "host-a") + self._bind_baremetal_port(net_id, SECOND_PHYSNET, "host-b") + + # Isolate the router attach from the sync calls made during the binds. + self.undersync_mock.reset_mock() + + # Create a VRF (flavored) router and attach the subnet on the internal + # side. The flavored path is simulated by patching _router_has_flavor, + # mirroring test_routers.py; this is the branch a real VRF router takes. + with mock.patch( + "neutron_understack.routers._router_has_flavor", return_value=True + ): + router = self._create_router() + self.l3_plugin.add_router_interface( + self.context, router["id"], {"subnet_id": subnet["id"]} + ) + + # Desired: the switches carrying the network's baremetal ports get + # reconciled, so undersync syncs each of their physnets. + synced = { + call.args[0] + for call in self.undersync_mock.sync.call_args_list + if call.args + } + assert DEFAULT_PHYSNET in synced + assert SECOND_PHYSNET in synced From 2970cc9a4db266141c1f8923588a6dd24aa740f3 Mon Sep 17 00:00:00 2001 From: Doug Goldstein <doug.goldstein@rackspace.com> Date: Mon, 24 Aug 2026 17:37:41 -0500 Subject: [PATCH 05/21] test(neutron-understack): SVI router interface sync scenario (xfail) Extend the router scenarios to the SVI flavor. SVI-RTR-01 mirrors VRF-RTR-01 but attaches an SVI router to an address-scoped IPv4 subnet (SVI routers require a scope), and asserts undersync syncs the physnets of the network's bound baremetal ports. It does not today -- the same gap as VRF, tracked by rackerlabs/understack#2240 -- so the test is xfail(strict=True). The shared _bind_baremetal_port helper moves onto UnderstackMl2RouterScenarioBase. The SVI flavor is simulated by patching _router_has_flavor and svi._is_svi_router, mirroring the unit tests. --- .../tests/scenarios/SCENARIOS.md | 12 ++ .../tests/scenarios/base.py | 22 ++++ .../scenarios/test_svi_router_interface.py | 104 ++++++++++++++++++ .../scenarios/test_vrf_router_interface.py | 22 ---- 4 files changed, 138 insertions(+), 22 deletions(-) create mode 100644 python/neutron-understack/neutron_understack/tests/scenarios/test_svi_router_interface.py diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md b/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md index a161b9c8e..7753a812d 100644 --- a/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md +++ b/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md @@ -83,6 +83,18 @@ mirroring the existing unit tests. called for them). The test asserts the desired behavior and is `xfail(strict=True)`. +### SVI-RTR-01 — SVI router attach syncs bound baremetal port physnets +- given: a VXLAN network + an address-scoped IPv4 subnet, with baremetal ports + bound to two different physnets (`physnet1`, `physnet2`) +- when: an SVI router is created and the subnet is attached on the internal side +- then: undersync syncs each physnet carrying the network's baremetal ports so + the switches are reconciled for the new router +- status: **KNOWN BUG (xfail, rackerlabs/understack#2240)** — the same gap as + VRF-RTR-01 for the SVI flavor: attaching the SVI router interface does not + sync those physnets today. The SVI flavor is simulated by patching + `_router_has_flavor` and `svi._is_svi_router`; the subnet is address-scoped so + the SVI precommit scope validation passes. + ## Known bugs (surfaced by these tests) - **Dynamic VLAN segment leaks on vif-detach** (BM-BIND-04, `xfail`, diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/base.py b/python/neutron-understack/neutron_understack/tests/scenarios/base.py index 404a38f5c..ef4895ba7 100644 --- a/python/neutron-understack/neutron_understack/tests/scenarios/base.py +++ b/python/neutron-understack/neutron_understack/tests/scenarios/base.py @@ -21,6 +21,7 @@ from neutron.conf.plugins.ml2.drivers import driver_type from neutron.tests.unit.plugins.ml2.base import ML2TestFramework from neutron.tests.unit.plugins.ml2.test_plugin import Ml2PluginV2TestCase +from neutron_lib.api.definitions import portbindings from neutron_lib.plugins import directory from oslo_config import cfg @@ -123,3 +124,24 @@ class UnderstackMl2RouterScenarioBase(_UnderstackMl2ScenarioMixin, ML2TestFramew ``self.l3_plugin`` is the loaded L3 plugin and ``self._create_router()`` creates a router directly (bypassing HTTP), both provided by ML2TestFramework. """ + + def _bind_baremetal_port(self, net_id, physnet, host): + """Create a baremetal port on the network and vif-attach it to physnet.""" + res = self._create_port( + self.fmt, + net_id, + arg_list=(portbindings.VNIC_TYPE,), + is_admin=True, + **{portbindings.VNIC_TYPE: portbindings.VNIC_BAREMETAL}, + ) + assert res.status_int == 201, res.body + port_id = self.deserialize(self.fmt, res)["port"]["id"] + data = { + "port": { + portbindings.HOST_ID: host, + portbindings.PROFILE: self.baremetal_binding_profile(physnet=physnet), + } + } + req = self.new_update_request("ports", data, port_id, as_service=True) + assert req.get_response(self.api).status_int == 200 + return port_id diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/test_svi_router_interface.py b/python/neutron-understack/neutron_understack/tests/scenarios/test_svi_router_interface.py new file mode 100644 index 000000000..bf7116bf9 --- /dev/null +++ b/python/neutron-understack/neutron_understack/tests/scenarios/test_svi_router_interface.py @@ -0,0 +1,104 @@ +"""Scenario tests for SVI router interface attach vs. bound baremetal ports. + +Extends the VRF scenario (VRF-RTR-01) to the SVI router flavor. See +neutron_understack/tests/scenarios/SCENARIOS.md (SVI-RTR-*) for the catalog. +""" + +from unittest import mock + +import pytest + +from neutron_understack.tests.scenarios.base import DEFAULT_PHYSNET +from neutron_understack.tests.scenarios.base import SECOND_PHYSNET +from neutron_understack.tests.scenarios.base import UnderstackMl2RouterScenarioBase + + +class TestSviRouterInterface(UnderstackMl2RouterScenarioBase): + def _scoped_ipv4_subnet(self, network): + """Create an IPv4 subnet in an address scope (SVI routers require one).""" + scope = self.core_plugin.create_address_scope( + self.context, + { + "address_scope": { + "name": "svi-scope", + "ip_version": 4, + "shared": False, + "tenant_id": self._project_id, + } + }, + ) + pool = self.core_plugin.create_subnetpool( + self.context, + { + "subnetpool": { + "name": "svi-pool", + "prefixes": ["10.0.0.0/16"], + "address_scope_id": scope["id"], + "default_prefixlen": 24, + "min_prefixlen": 8, + "max_prefixlen": 32, + "shared": False, + "is_default": False, + "tenant_id": self._project_id, + } + }, + ) + return self._make_subnet( + self.fmt, + network, + gateway="10.0.0.1", + cidr="10.0.0.0/24", + subnetpool_id=pool["id"], + )["subnet"] + + # BUG: like the VRF case (#2240), attaching an SVI router interface does not + # reconcile the switches carrying the network's already-bound baremetal + # ports -- undersync is never asked to sync those physnets. Asserts the + # desired behavior; strict=True flips to a failure once fixed. + @pytest.mark.xfail( + strict=True, + reason=( + "understack#2240: SVI router interface attach does not sync the " + "physnets of baremetal ports already bound on the network" + ), + ) + @pytest.mark.scenario("SVI-RTR-01") + def test_svi_router_interface_syncs_bound_port_physnets(self): + net = self._make_network(self.fmt, "vxlan-net", True) + net_id = net["network"]["id"] + subnet = self._scoped_ipv4_subnet(net) + + # Two baremetal ports on this network, bound to different physnets. + self._bind_baremetal_port(net_id, DEFAULT_PHYSNET, "host-a") + self._bind_baremetal_port(net_id, SECOND_PHYSNET, "host-b") + + # Isolate the router attach from the sync calls made during the binds. + self.undersync_mock.reset_mock() + + # Create an SVI (flavored) router and attach the scoped subnet on the + # internal side. The SVI flavor is simulated by patching the flavor + # detection seams, mirroring the unit tests: _router_has_flavor gates the + # (skipped) uplink path, and svi._is_svi_router drives the precommit SVI + # scope validation, which passes because the subnet is address-scoped. + with ( + mock.patch( + "neutron_understack.routers._router_has_flavor", return_value=True + ), + mock.patch( + "neutron_understack.l3_router.svi._is_svi_router", return_value=True + ), + ): + router = self._create_router() + self.l3_plugin.add_router_interface( + self.context, router["id"], {"subnet_id": subnet["id"]} + ) + + # Desired: the switches carrying the network's baremetal ports get + # reconciled, so undersync syncs each of their physnets. + synced = { + call.args[0] + for call in self.undersync_mock.sync.call_args_list + if call.args + } + assert DEFAULT_PHYSNET in synced + assert SECOND_PHYSNET in synced diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/test_vrf_router_interface.py b/python/neutron-understack/neutron_understack/tests/scenarios/test_vrf_router_interface.py index 5ba970905..0fff16d03 100644 --- a/python/neutron-understack/neutron_understack/tests/scenarios/test_vrf_router_interface.py +++ b/python/neutron-understack/neutron_understack/tests/scenarios/test_vrf_router_interface.py @@ -6,7 +6,6 @@ from unittest import mock import pytest -from neutron_lib.api.definitions import portbindings from neutron_understack.tests.scenarios.base import DEFAULT_PHYSNET from neutron_understack.tests.scenarios.base import SECOND_PHYSNET @@ -14,27 +13,6 @@ class TestVrfRouterInterface(UnderstackMl2RouterScenarioBase): - def _bind_baremetal_port(self, net_id, physnet, host): - """Create a baremetal port on the network and vif-attach it to physnet.""" - res = self._create_port( - self.fmt, - net_id, - arg_list=(portbindings.VNIC_TYPE,), - is_admin=True, - **{portbindings.VNIC_TYPE: portbindings.VNIC_BAREMETAL}, - ) - assert res.status_int == 201, res.body - port_id = self.deserialize(self.fmt, res)["port"]["id"] - data = { - "port": { - portbindings.HOST_ID: host, - portbindings.PROFILE: self.baremetal_binding_profile(physnet=physnet), - } - } - req = self.new_update_request("ports", data, port_id, as_service=True) - assert req.get_response(self.api).status_int == 200 - return port_id - # BUG: attaching a VRF router interface to a network does not reconcile the # switches carrying that network's already-bound baremetal ports -- undersync # is never asked to sync those physnets. This test asserts the desired From 5bae0d3d1487b3866c4362e4633df253d46aea6f Mon Sep 17 00:00:00 2001 From: Doug Goldstein <doug.goldstein@rackspace.com> Date: Mon, 24 Aug 2026 17:40:18 -0500 Subject: [PATCH 06/21] ci: publish consolidated test results on PRs Upload each project's JUnit and add a publish-test-results job that runs EnricoMi/publish-unit-test-result-action once to post a pass/fail/skip summary as a PR comment and check. Pinned by SHA; needs checks and pull-requests write. Fork PRs would need a workflow_run-triggered variant, noted inline. --- .github/workflows/code-test.yaml | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/.github/workflows/code-test.yaml b/.github/workflows/code-test.yaml index 7c410120c..034f70a36 100644 --- a/.github/workflows/code-test.yaml +++ b/.github/workflows/code-test.yaml @@ -78,11 +78,14 @@ jobs: else echo "No junit.xml produced (tests did not run)." >> "$GITHUB_STEP_SUMMARY" fi + # Keep each project's JUnit for the consolidated publish-test-results job. + # if-no-files-found=ignore: stestr projects don't emit junit.xml. - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - if: ${{ always() && matrix.project == 'neutron-understack' }} + if: ${{ always() }} with: - name: scenario-junit-${{ matrix.project }} + name: junit-${{ matrix.project }} path: python/${{ matrix.project }}/junit.xml + if-no-files-found: ignore retention-days: 1 @@ -101,3 +104,25 @@ jobs: name: coverage.xml path: coverage.xml retention-days: 1 + + # Consolidated pass/fail/skip report from every project's JUnit, posted as a + # PR comment + check. Same-repo PRs only get write permissions; fork PRs would + # need a separate workflow_run-triggered workflow (not wired up here). + publish-test-results: + needs: [uv] + if: ${{ always() }} + runs-on: ubuntu-latest + permissions: + checks: write + pull-requests: write + contents: read + steps: + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + pattern: junit-* + merge-multiple: true + path: junit + - uses: EnricoMi/publish-unit-test-result-action@d0a4676d0e0b938bc201470d88276b7c74c712b3 # v2.24.0 + with: + check_name: Python test results + junit_files: junit/**/*.xml From 3201122358a7afabe9ce776d6a49dc6cb957b2cb Mon Sep 17 00:00:00 2001 From: Doug Goldstein <doug.goldstein@rackspace.com> Date: Mon, 24 Aug 2026 17:48:27 -0500 Subject: [PATCH 07/21] test(neutron-understack): trunk subport + no-IP sync scenarios Add trunk scenarios on a real neutron trunk plugin (UnderstackMl2TrunkScenarioBase). TRUNK-SUB-ADD and TRUNK-SUB-DEL assert undersync syncs the parent baremetal port's physnet when a subport is added or removed; TRUNK-PARENT-NOIP asserts the sync fires even when the parent has no IP. BM-BIND-06 asserts a bound baremetal port with no IP still emits the physnet sync. Move _bind_baremetal_port to the shared mixin with a fixed_ips option. utils.fetch_network_node_trunk_id (live OVN/Ironic lookup) is stubbed. --- .../tests/scenarios/SCENARIOS.md | 32 +++++ .../tests/scenarios/base.py | 54 +++++--- .../tests/scenarios/test_baremetal_binding.py | 24 ++++ .../tests/scenarios/test_trunk_operations.py | 116 ++++++++++++++++++ 4 files changed, 212 insertions(+), 14 deletions(-) create mode 100644 python/neutron-understack/neutron_understack/tests/scenarios/test_trunk_operations.py diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md b/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md index 7753a812d..caa8fecf4 100644 --- a/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md +++ b/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md @@ -65,6 +65,38 @@ scenarios). Tenant networks are VXLAN; the physnet named in the binding profile - then: the dynamic VLAN segment is released (no ports remain bound to it) and `undersync.sync(<physnet>)` reconciles the switch +### BM-BIND-06 — vif-attach with no IP still emits the physnet sync +- given: a VXLAN network with a subnet, and a baremetal port created with no + fixed IP (`fixed_ips: []`) +- when: the port is vif-attached (host + profile with `physical_network`) +- then: the port binds and `undersync.sync(<physnet>)` still fires — the sync + does not depend on the port having an IP + +## Trunk subport operations + +These scenarios load the real neutron trunk service plugin +(`UnderstackMl2TrunkScenarioBase`). The parent is a bound baremetal port; subport +adds/removes must reconcile the parent's switch (VLAN group). +`utils.fetch_network_node_trunk_id` (live OVN + Ironic discovery) is stubbed. + +### TRUNK-SUB-ADD — subport attach syncs the parent's physnet +- given: a bound baremetal parent port on `physnet1` with a trunk, and a subport + port on another network +- when: the subport is added to the trunk (VLAN segmentation) +- then: `undersync.sync(physnet1)` reconciles the parent port's switch + +### TRUNK-SUB-DEL — subport removal syncs the parent's physnet +- given: the TRUNK-SUB-ADD setup with the subport attached +- when: the subport is removed from the trunk +- then: `undersync.sync(physnet1)` reconciles the parent port's switch + +### TRUNK-PARENT-NOIP — subport add syncs when the parent has no IP +- given: a bound baremetal parent on a subnetted network but with no fixed IP, + plus a trunk and a subport on another network +- when: the subport is added +- then: `undersync.sync(physnet1)` still fires — a parent with no IP does not + suppress the reconcile + ## Router interface (VRF flavor) These scenarios load a real L3 router + flavors plugin (`ML2TestFramework`). The diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/base.py b/python/neutron-understack/neutron_understack/tests/scenarios/base.py index ef4895ba7..3379ab551 100644 --- a/python/neutron-understack/neutron_understack/tests/scenarios/base.py +++ b/python/neutron-understack/neutron_understack/tests/scenarios/base.py @@ -113,26 +113,22 @@ def baremetal_binding_profile(physnet=DEFAULT_PHYSNET, port_id="Ethernet1/1"): profile["physical_network"] = physnet return profile + def _bind_baremetal_port(self, net_id, physnet, host, fixed_ips=None): + """Create a baremetal port on the network and vif-attach it to physnet. -class UnderstackMl2ScenarioBase(_UnderstackMl2ScenarioMixin, Ml2PluginV2TestCase): - """Base for port-binding scenarios (no L3/router machinery).""" - - -class UnderstackMl2RouterScenarioBase(_UnderstackMl2ScenarioMixin, ML2TestFramework): - """Base for router scenarios: real L3RouterPlugin + flavors plugin. - - ``self.l3_plugin`` is the loaded L3 plugin and ``self._create_router()`` - creates a router directly (bypassing HTTP), both provided by ML2TestFramework. - """ - - def _bind_baremetal_port(self, net_id, physnet, host): - """Create a baremetal port on the network and vif-attach it to physnet.""" + Returns the bound port's id. binding:host_id / binding:profile writes + require the service role under secure RBAC. Pass ``fixed_ips=[]`` to force + a port with no IP even on a subnetted network. + """ + create_kwargs = {portbindings.VNIC_TYPE: portbindings.VNIC_BAREMETAL} + if fixed_ips is not None: + create_kwargs["fixed_ips"] = fixed_ips res = self._create_port( self.fmt, net_id, arg_list=(portbindings.VNIC_TYPE,), is_admin=True, - **{portbindings.VNIC_TYPE: portbindings.VNIC_BAREMETAL}, + **create_kwargs, ) assert res.status_int == 201, res.body port_id = self.deserialize(self.fmt, res)["port"]["id"] @@ -145,3 +141,33 @@ def _bind_baremetal_port(self, net_id, physnet, host): req = self.new_update_request("ports", data, port_id, as_service=True) assert req.get_response(self.api).status_int == 200 return port_id + + +class UnderstackMl2ScenarioBase(_UnderstackMl2ScenarioMixin, Ml2PluginV2TestCase): + """Base for port-binding scenarios (no L3/router machinery).""" + + +class UnderstackMl2RouterScenarioBase(_UnderstackMl2ScenarioMixin, ML2TestFramework): + """Base for router scenarios: real L3RouterPlugin + flavors plugin. + + ``self.l3_plugin`` is the loaded L3 plugin and ``self._create_router()`` + creates a router directly (bypassing HTTP), both provided by ML2TestFramework. + """ + + +class UnderstackMl2TrunkScenarioBase(UnderstackMl2ScenarioBase): + """Base for trunk scenarios: loads the real neutron trunk service plugin. + + The understack trunk driver registers against the trunk plugin's AFTER_INIT + event, so loading the plugin here wires the SUBPORTS/TRUNK handlers. Exposes + ``self.trunk_plugin``. + """ + + def get_additional_service_plugins(self): + plugins = super().get_additional_service_plugins() + plugins["trunk_plugin_name"] = "neutron.services.trunk.plugin.TrunkPlugin" + return plugins + + def setUp(self): + super().setUp() + self.trunk_plugin = directory.get_plugin("trunk") diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/test_baremetal_binding.py b/python/neutron-understack/neutron_understack/tests/scenarios/test_baremetal_binding.py index 5813ec048..4997cb3aa 100644 --- a/python/neutron-understack/neutron_understack/tests/scenarios/test_baremetal_binding.py +++ b/python/neutron-understack/neutron_understack/tests/scenarios/test_baremetal_binding.py @@ -79,6 +79,30 @@ def test_baremetal_vif_attach_binds_hierarchically(self): # The switch reconcile fired for the port's VLAN group. self.undersync_mock.sync.assert_any_call(DEFAULT_PHYSNET) + @pytest.mark.scenario("BM-BIND-06") + def test_vif_attach_without_ip_still_syncs(self): + """A bound baremetal port with no IP still emits the physnet sync.""" + net = self._make_vxlan_network() + # Subnet exists on the network, but the port is created with no IP. + self._make_subnet( + self.fmt, {"network": net}, gateway="10.8.0.1", cidr="10.8.0.0/24" + ) + res = self._create_port( + self.fmt, + net["id"], + arg_list=(portbindings.VNIC_TYPE,), + is_admin=True, + **{portbindings.VNIC_TYPE: portbindings.VNIC_BAREMETAL, "fixed_ips": []}, + ) + assert res.status_int == 201, res.body + port = self.deserialize(self.fmt, res)["port"] + assert port["fixed_ips"] == [] + + updated = self._vif_attach(port["id"]) + + assert updated[portbindings.VIF_TYPE] == portbindings.VIF_TYPE_OTHER + self.undersync_mock.sync.assert_any_call(DEFAULT_PHYSNET) + @pytest.mark.scenario("BM-BIND-02") def test_vif_attach_without_physical_network_refuses_binding(self): """A binding profile missing physical_network cannot be bound.""" diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/test_trunk_operations.py b/python/neutron-understack/neutron_understack/tests/scenarios/test_trunk_operations.py new file mode 100644 index 000000000..ea8b6c6ca --- /dev/null +++ b/python/neutron-understack/neutron_understack/tests/scenarios/test_trunk_operations.py @@ -0,0 +1,116 @@ +"""Scenario tests for trunk subport operations driving undersync sync. + +See neutron_understack/tests/scenarios/SCENARIOS.md (TRUNK-*) for the catalog. +""" + +import uuid +from unittest import mock + +import pytest + +from neutron_understack.tests.scenarios.base import DEFAULT_PHYSNET +from neutron_understack.tests.scenarios.base import UnderstackMl2TrunkScenarioBase + +# Any VLAN in the default tenant range [1, 3799]; no NetworkSegmentRange rows +# exist in the harness, so the whole range is allowed for subports. +SUBPORT_VLAN = 1500 + +# fetch_network_node_trunk_id() does live OVN gateway + Ironic discovery; stub it +# to a non-matching id so the tenant-trunk segmentation check runs normally. +_FAKE_NN_TRUNK = str(uuid.uuid4()) + + +class TestTrunkOperations(UnderstackMl2TrunkScenarioBase): + def _plain_port(self, net_id): + res = self._create_port(self.fmt, net_id, is_admin=True) + assert res.status_int == 201, res.body + return self.deserialize(self.fmt, res)["port"]["id"] + + def _make_trunk(self, parent_id): + trunk = self.trunk_plugin.create_trunk( + self.context, + { + "trunk": { + "port_id": parent_id, + "project_id": self._project_id, + "admin_state_up": True, + "sub_ports": [], + } + }, + ) + return trunk["id"] + + def _add_subport(self, trunk_id, subport_id, seg_id=SUBPORT_VLAN): + with mock.patch( + "neutron_understack.utils.fetch_network_node_trunk_id", + return_value=_FAKE_NN_TRUNK, + ): + self.trunk_plugin.add_subports( + self.context, + trunk_id, + { + "sub_ports": [ + { + "port_id": subport_id, + "segmentation_type": "vlan", + "segmentation_id": seg_id, + } + ] + }, + ) + + def _remove_subport(self, trunk_id, subport_id): + with mock.patch( + "neutron_understack.utils.fetch_network_node_trunk_id", + return_value=_FAKE_NN_TRUNK, + ): + self.trunk_plugin.remove_subports( + self.context, trunk_id, {"sub_ports": [{"port_id": subport_id}]} + ) + + @pytest.mark.scenario("TRUNK-SUB-ADD") + def test_subport_add_syncs_parent_physnet(self): + parent_net = self._make_network(self.fmt, "parent-net", True)["network"]["id"] + parent_id = self._bind_baremetal_port(parent_net, DEFAULT_PHYSNET, "host-a") + subport_net = self._make_network(self.fmt, "subport-net", True)["network"]["id"] + subport_id = self._plain_port(subport_net) + trunk_id = self._make_trunk(parent_id) + + self.undersync_mock.reset_mock() + self._add_subport(trunk_id, subport_id) + + # Attaching the subport reconciles the parent port's switch (VLAN group). + self.undersync_mock.sync.assert_any_call(DEFAULT_PHYSNET) + + @pytest.mark.scenario("TRUNK-SUB-DEL") + def test_subport_remove_syncs_parent_physnet(self): + parent_net = self._make_network(self.fmt, "parent-net", True)["network"]["id"] + parent_id = self._bind_baremetal_port(parent_net, DEFAULT_PHYSNET, "host-a") + subport_net = self._make_network(self.fmt, "subport-net", True)["network"]["id"] + subport_id = self._plain_port(subport_net) + trunk_id = self._make_trunk(parent_id) + self._add_subport(trunk_id, subport_id) + + self.undersync_mock.reset_mock() + self._remove_subport(trunk_id, subport_id) + + # Removing the subport reconciles the parent port's switch (VLAN group). + self.undersync_mock.sync.assert_any_call(DEFAULT_PHYSNET) + + @pytest.mark.scenario("TRUNK-PARENT-NOIP") + def test_subport_add_syncs_when_parent_has_no_ip(self): + # Parent network has a subnet, but the parent port is bound with no IP. + parent_net = self._make_network(self.fmt, "parent-net", True) + self._make_subnet(self.fmt, parent_net, gateway="10.9.0.1", cidr="10.9.0.0/24") + parent_id = self._bind_baremetal_port( + parent_net["network"]["id"], DEFAULT_PHYSNET, "host-a", fixed_ips=[] + ) + subport_net = self._make_network(self.fmt, "subport-net", True)["network"]["id"] + subport_id = self._plain_port(subport_net) + trunk_id = self._make_trunk(parent_id) + + self.undersync_mock.reset_mock() + self._add_subport(trunk_id, subport_id) + + # The parent having no IP does not suppress the switch reconcile. + self.undersync_mock.sync.assert_any_call(DEFAULT_PHYSNET) From 51184d28ee8159548a3963cc20d16308180920f0 Mon Sep 17 00:00:00 2001 From: Doug Goldstein <doug.goldstein@rackspace.com> Date: Mon, 24 Aug 2026 18:14:56 -0500 Subject: [PATCH 08/21] docs(neutron-understack): backlog of uncovered ML2 scenarios Replace the vague "planned/future" note with a structured backlog of not-yet-covered scenarios grouped by area (port bind/create/delete, trunk, SVI validation, router, VNI/providers, cross-cutting), each with a proposed ID to promote to a catalogued heading when its test lands. Kept as plain bullets so the coverage check ignores them. Cisco ASA is explicitly out of scope. --- .../tests/scenarios/SCENARIOS.md | 66 ++++++++++++++++--- 1 file changed, 58 insertions(+), 8 deletions(-) diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md b/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md index caa8fecf4..7b98c386b 100644 --- a/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md +++ b/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md @@ -141,11 +141,61 @@ mirroring the existing unit tests. unbound without being deleted. Fix should release the dynamic (bottom) segment on the detach transition; the xfail flips to a pass once fixed. -## Planned / future (not yet catalogued IDs) - -- Router create + `add_router_interface`: assert the `network:router_interface` - port is created and `UnderstackDriver.create_port_postcommit` → - `routers.create_port_postcommit` runs with the expected data. Requires faking - the OVN IDL (`neutron_understack.routers.ovn_client()`). -- Trunk parent/subport binding: assert per-subport dynamic VLAN allocation and - `undersync.sync` calls. +## Backlog (not yet covered) + +Proposed scenarios, grouped by area. These are intentionally plain bullets (not +`###` IDs) so the coverage check does not treat them as catalogued-but-untested; +promote a bullet to a `### <ID>` heading when its test lands. + +Port create / bind / delete (no new test doubles): +- PROV-BIND-01 — bind a port on the provisioning network (`is_provisioning_network`). +- PROV-DEL-01 — delete a port on the provisioning network: sync only, segment not + released (clean/provision cycle). +- BM-BIND-REUSE-01 — a second baremetal port on the same network+physnet reuses + the existing dynamic VLAN segment (`vlan_segment_for_physnet`), no new alloc. +- BM-REBIND-01 — rebind on `binding:host_id` change. +- BM-DEL-NOPHYSNET-01 — delete a bound port whose profile lost `physical_network` + (early return, no sync). + +Trunk (no new test doubles): +- TRUNK-CREATE-WITH-SUBPORTS-01 — trunk created with subports present + (`trunk_created` path) syncs the parent physnet. +- TRUNK-DEL-01 — deleting a trunk cleans the parent switchport and syncs. +- TRUNK-PARENT-UNBIND-01 — unbinding a trunked baremetal parent runs `clean_trunk` + (subport teardown) alongside BM-BIND-04's segment handling. +- TRUNK-PARENT-UNBOUND-01 — subport add with an unbound parent is a no-op (no sync). +- TRUNK-SEGID-RANGE-01 — subport seg_id outside the allowed range raises + `SubportSegmentationIDError` (negative). +- TRUNK-NOPHYSNET-01 — subport add/remove on a parent with no `physical_network` + raises `BadRequest` (negative). +- TRUNK-MULTI-01 — multiple subports across physnets in one operation. + +SVI validation (no new test doubles): +- SVI-VAL-IPV6-01 — attaching an IPv6 subnet to an SVI router is rejected. +- SVI-VAL-NOSCOPE-01 — attaching a subnet with no address scope is rejected. +- SVI-VAL-CONFLICT-01 — attaching subnets with conflicting scopes is rejected. +- SVI-EXTGW-01 — an SVI router cannot get an external gateway (`BadRequest`). + +Router (needs an OVN IDL fake for `routers.ovn_client()`): +- RTR-ATTACH-01 — non-flavored router attach builds the uplink: dynamic segment, + shared neutron port, network-node trunk subport, OVN localnet port. +- RTR-DETACH-01 — `remove_router_interface` tears the uplink down + (`handle_router_interface_after_delete`). +- RTR-DELETE-01 — `delete_router` tears the uplink down + (`handle_router_interface_removal`). +- RTR-SECOND-01 — a second router on the same network is a no-op. +- RTR-HCG-VXLAN-01 — `link_vxlan_network_ha_chassis_group` populates the unified + HCG for a vxlan external gateway. + +VNI / providers: +- VNI-ALLOC-01 — VRF router create/delete allocates/releases an `evpn_vni` + (`UnderstackVniPlugin`). +- PALO-ADOPT-01 — a Palo Alto flavored router adopts a netdev Ironic node + (needs an Ironic client fake). + +Cross-cutting: +- DRYRUN-01 — `undersync_dry_run=True` routes to dry-run instead of sync. +- RTR-DETACH-SYNC-01 — router detach should sync the network's bound baremetal + physnets (teardown counterpart of #2240; likely xfail). + +Explicitly out of scope: Cisco ASA floating-IP NAT. From 2f59b71d080d9b8ec7da48fd1c18a8de4e821fef Mon Sep 17 00:00:00 2001 From: Doug Goldstein <doug.goldstein@rackspace.com> Date: Mon, 24 Aug 2026 18:17:27 -0500 Subject: [PATCH 09/21] test(neutron-understack): provisioning-network + segment-reuse scenarios BM-BIND-REUSE-01 asserts a second baremetal port on the same network+physnet reuses the existing dynamic VLAN segment. PROV-BIND-01 binds a port on the provisioning network. PROV-DEL-01 asserts deleting a provisioning-network port syncs but retains the VLAN segment (the clean/provision cycle reuses it), unlike BM-BIND-05's tenant delete. --- .../tests/scenarios/SCENARIOS.md | 24 +++++++-- .../tests/scenarios/test_baremetal_binding.py | 50 +++++++++++++++++++ 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md b/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md index 7b98c386b..3b2041b2d 100644 --- a/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md +++ b/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md @@ -72,6 +72,25 @@ scenarios). Tenant networks are VXLAN; the physnet named in the binding profile - then: the port binds and `undersync.sync(<physnet>)` still fires — the sync does not depend on the port having an IP +### BM-BIND-REUSE-01 — second port on same network+physnet reuses the VLAN seg +- given: a VXLAN network with one bound baremetal port on `physnet1` +- when: a second baremetal port on the same network is vif-attached to `physnet1` +- then: it reuses the existing dynamic VLAN segment (same segment id), no new + allocation + +### PROV-BIND-01 — provisioning-network port binds +- given: a network configured as `ml2_understack.provisioning_network` +- when: a baremetal port on it is vif-attached +- then: it binds hierarchically and `undersync.sync(<physnet>)` fires (no special + casing on the bind path) + +### PROV-DEL-01 — provisioning-network delete retains the VLAN segment +- given: a bound baremetal port on the provisioning network +- when: the port is deleted +- then: `undersync.sync(<physnet>)` fires but the dynamic VLAN segment is + retained (the clean/provision cycle reuses it), unlike a tenant port + (BM-BIND-05) + ## Trunk subport operations These scenarios load the real neutron trunk service plugin @@ -148,11 +167,6 @@ Proposed scenarios, grouped by area. These are intentionally plain bullets (not promote a bullet to a `### <ID>` heading when its test lands. Port create / bind / delete (no new test doubles): -- PROV-BIND-01 — bind a port on the provisioning network (`is_provisioning_network`). -- PROV-DEL-01 — delete a port on the provisioning network: sync only, segment not - released (clean/provision cycle). -- BM-BIND-REUSE-01 — a second baremetal port on the same network+physnet reuses - the existing dynamic VLAN segment (`vlan_segment_for_physnet`), no new alloc. - BM-REBIND-01 — rebind on `binding:host_id` change. - BM-DEL-NOPHYSNET-01 — delete a bound port whose profile lost `physical_network` (early return, no sync). diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/test_baremetal_binding.py b/python/neutron-understack/neutron_understack/tests/scenarios/test_baremetal_binding.py index 4997cb3aa..a577851ba 100644 --- a/python/neutron-understack/neutron_understack/tests/scenarios/test_baremetal_binding.py +++ b/python/neutron-understack/neutron_understack/tests/scenarios/test_baremetal_binding.py @@ -9,6 +9,7 @@ from neutron.plugins.ml2 import db as ml2_db from neutron_lib import constants as p_const from neutron_lib.api.definitions import portbindings +from oslo_config import cfg from neutron_understack.tests.scenarios.base import DEFAULT_PHYSNET from neutron_understack.tests.scenarios.base import UnderstackMl2ScenarioBase @@ -79,6 +80,55 @@ def test_baremetal_vif_attach_binds_hierarchically(self): # The switch reconcile fired for the port's VLAN group. self.undersync_mock.sync.assert_any_call(DEFAULT_PHYSNET) + def _bottom_segment_id(self, port_id, host=HOST): + levels = ml2_db.get_binding_level_objs(self.context, port_id, host) + return levels[-1].segment_id + + @pytest.mark.scenario("BM-BIND-REUSE-01") + def test_second_port_same_physnet_reuses_dynamic_segment(self): + """A 2nd baremetal port on the same network+physnet reuses the VLAN seg.""" + net = self._make_vxlan_network() + port_a = self._create_unbound_baremetal_port(net["id"]) + self._vif_attach(port_a["id"], host="host-a") + port_b = self._create_unbound_baremetal_port(net["id"]) + self._vif_attach(port_b["id"], host="host-b") + + seg_a = self._bottom_segment_id(port_a["id"], "host-a") + seg_b = self._bottom_segment_id(port_b["id"], "host-b") + assert seg_a == seg_b, (seg_a, seg_b) + + @pytest.mark.scenario("PROV-BIND-01") + def test_provisioning_network_port_binds(self): + """A baremetal port on the provisioning network binds hierarchically.""" + net = self._make_vxlan_network() + cfg.CONF.set_override("provisioning_network", net["id"], group="ml2_understack") + port = self._create_unbound_baremetal_port(net["id"]) + + updated = self._vif_attach(port["id"]) + + assert updated[portbindings.VIF_TYPE] == portbindings.VIF_TYPE_OTHER + self.undersync_mock.sync.assert_any_call(DEFAULT_PHYSNET) + + @pytest.mark.scenario("PROV-DEL-01") + def test_provisioning_network_delete_retains_segment(self): + """Deleting a provisioning-network port syncs but keeps the VLAN segment. + + The clean/provision cycle re-uses the segment, so unlike a tenant port + (BM-BIND-05) the dynamic VLAN segment is not released on delete. + """ + net = self._make_vxlan_network() + cfg.CONF.set_override("provisioning_network", net["id"], group="ml2_understack") + port = self._create_unbound_baremetal_port(net["id"]) + self._vif_attach(port["id"]) + vlan_segment_id = self._bottom_segment_id(port["id"]) + self.undersync_mock.reset_mock() + + self._delete("ports", port["id"], as_admin=True) + + # Sync fired, but the segment is retained (provisioning cycle). + self.undersync_mock.sync.assert_any_call(DEFAULT_PHYSNET) + assert segments_db.get_segment_by_id(self.context, vlan_segment_id) is not None + @pytest.mark.scenario("BM-BIND-06") def test_vif_attach_without_ip_still_syncs(self): """A bound baremetal port with no IP still emits the physnet sync.""" From 4fecd2ed6f1bb286449ec9c4d2f7daba80ee7de3 Mon Sep 17 00:00:00 2001 From: Doug Goldstein <doug.goldstein@rackspace.com> Date: Mon, 24 Aug 2026 18:22:34 -0500 Subject: [PATCH 10/21] test(neutron-understack): SVI address-scope validation rejections Add SVI negative scenarios exercising the create_port_precommit scope validator: SVI-VAL-NOSCOPE-01 (subnet with no address scope), SVI-VAL-IPV6-01 (IPv6 subnet), and SVI-VAL-CONFLICT-01 (conflicting per-IP-version scopes). Each attach is rejected; the validator's BadRequest is surfaced by the ML2 manager as MechanismDriverError. Factor the SVI-flavor patching into an _as_svi_router() helper and make _scoped_ipv4_subnet parameterizable for multiple scopes. --- .../tests/scenarios/SCENARIOS.md | 25 +++- .../scenarios/test_svi_router_interface.py | 110 ++++++++++++++---- 2 files changed, 110 insertions(+), 25 deletions(-) diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md b/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md index 3b2041b2d..d7c0f974e 100644 --- a/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md +++ b/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md @@ -146,6 +146,22 @@ mirroring the existing unit tests. `_router_has_flavor` and `svi._is_svi_router`; the subnet is address-scoped so the SVI precommit scope validation passes. +### SVI-VAL-NOSCOPE-01 — SVI rejects a subnet with no address scope +- given: an SVI router and a subnet not in any address scope +- when: the subnet is attached on the internal side +- then: the attach is rejected (the SVI precommit validator raises BadRequest, + which the ML2 manager surfaces as MechanismDriverError) + +### SVI-VAL-IPV6-01 — SVI rejects an IPv6 subnet +- given: an SVI router and an IPv6 subnet +- when: the subnet is attached +- then: the attach is rejected (SVI routers are IPv4-only) + +### SVI-VAL-CONFLICT-01 — SVI rejects conflicting address scopes +- given: an SVI router with an interface in address scope A +- when: a subnet in a different scope B is attached +- then: the attach is rejected (per-IP-version scope conflict) + ## Known bugs (surfaced by these tests) - **Dynamic VLAN segment leaks on vif-detach** (BM-BIND-04, `xfail`, @@ -184,11 +200,10 @@ Trunk (no new test doubles): raises `BadRequest` (negative). - TRUNK-MULTI-01 — multiple subports across physnets in one operation. -SVI validation (no new test doubles): -- SVI-VAL-IPV6-01 — attaching an IPv6 subnet to an SVI router is rejected. -- SVI-VAL-NOSCOPE-01 — attaching a subnet with no address scope is rejected. -- SVI-VAL-CONFLICT-01 — attaching subnets with conflicting scopes is rejected. -- SVI-EXTGW-01 — an SVI router cannot get an external gateway (`BadRequest`). +SVI validation: +- SVI-EXTGW-01 — an SVI router cannot get an external gateway. Needs the real + Svi provider + flavor wiring (the `_reject_svi_external_gateway` callback), + which the patched-flavor scenarios do not load. Router (needs an OVN IDL fake for `routers.ovn_client()`): - RTR-ATTACH-01 — non-flavored router attach builds the uplink: dynamic segment, diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/test_svi_router_interface.py b/python/neutron-understack/neutron_understack/tests/scenarios/test_svi_router_interface.py index bf7116bf9..9604d47bd 100644 --- a/python/neutron-understack/neutron_understack/tests/scenarios/test_svi_router_interface.py +++ b/python/neutron-understack/neutron_understack/tests/scenarios/test_svi_router_interface.py @@ -4,9 +4,11 @@ neutron_understack/tests/scenarios/SCENARIOS.md (SVI-RTR-*) for the catalog. """ +import contextlib from unittest import mock import pytest +from neutron.plugins.ml2.common import exceptions as ml2_exc from neutron_understack.tests.scenarios.base import DEFAULT_PHYSNET from neutron_understack.tests.scenarios.base import SECOND_PHYSNET @@ -14,13 +16,33 @@ class TestSviRouterInterface(UnderstackMl2RouterScenarioBase): - def _scoped_ipv4_subnet(self, network): - """Create an IPv4 subnet in an address scope (SVI routers require one).""" + @contextlib.contextmanager + def _as_svi_router(self): + """Treat the router as SVI-flavored (skip OVN uplink, run SVI checks).""" + with ( + mock.patch( + "neutron_understack.routers._router_has_flavor", return_value=True + ), + mock.patch( + "neutron_understack.l3_router.svi._is_svi_router", return_value=True + ), + ): + yield + + def _scoped_ipv4_subnet( + self, + network, + name="svi", + pool_prefix="10.0.0.0/16", + cidr="10.0.0.0/24", + gateway="10.0.0.1", + ): + """Create an IPv4 subnet in its own address scope (SVI routers need one).""" scope = self.core_plugin.create_address_scope( self.context, { "address_scope": { - "name": "svi-scope", + "name": f"{name}-scope", "ip_version": 4, "shared": False, "tenant_id": self._project_id, @@ -31,8 +53,8 @@ def _scoped_ipv4_subnet(self, network): self.context, { "subnetpool": { - "name": "svi-pool", - "prefixes": ["10.0.0.0/16"], + "name": f"{name}-pool", + "prefixes": [pool_prefix], "address_scope_id": scope["id"], "default_prefixlen": 24, "min_prefixlen": 8, @@ -46,8 +68,8 @@ def _scoped_ipv4_subnet(self, network): return self._make_subnet( self.fmt, network, - gateway="10.0.0.1", - cidr="10.0.0.0/24", + gateway=gateway, + cidr=cidr, subnetpool_id=pool["id"], )["subnet"] @@ -75,19 +97,10 @@ def test_svi_router_interface_syncs_bound_port_physnets(self): # Isolate the router attach from the sync calls made during the binds. self.undersync_mock.reset_mock() - # Create an SVI (flavored) router and attach the scoped subnet on the - # internal side. The SVI flavor is simulated by patching the flavor - # detection seams, mirroring the unit tests: _router_has_flavor gates the - # (skipped) uplink path, and svi._is_svi_router drives the precommit SVI - # scope validation, which passes because the subnet is address-scoped. - with ( - mock.patch( - "neutron_understack.routers._router_has_flavor", return_value=True - ), - mock.patch( - "neutron_understack.l3_router.svi._is_svi_router", return_value=True - ), - ): + # Create an SVI router and attach the scoped subnet on the internal side. + # The SVI flavor is simulated via _as_svi_router(); the scope validation + # passes because the subnet is address-scoped. + with self._as_svi_router(): router = self._create_router() self.l3_plugin.add_router_interface( self.context, router["id"], {"subnet_id": subnet["id"]} @@ -102,3 +115,60 @@ def test_svi_router_interface_syncs_bound_port_physnets(self): } assert DEFAULT_PHYSNET in synced assert SECOND_PHYSNET in synced + + def _attach_expecting_reject(self, subnet_id, router_id): + # The SVI scope validator raises BadRequest in create_port_precommit, + # which the ML2 manager wraps in MechanismDriverError. + with self._as_svi_router(), pytest.raises(ml2_exc.MechanismDriverError): + self.l3_plugin.add_router_interface( + self.context, router_id, {"subnet_id": subnet_id} + ) + + @pytest.mark.scenario("SVI-VAL-NOSCOPE-01") + def test_svi_rejects_subnet_without_address_scope(self): + net = self._make_network(self.fmt, "n", True) + subnet = self._make_subnet( + self.fmt, net, gateway="10.5.0.1", cidr="10.5.0.0/24" + )["subnet"] + router = self._create_router() + self._attach_expecting_reject(subnet["id"], router["id"]) + + @pytest.mark.scenario("SVI-VAL-IPV6-01") + def test_svi_rejects_ipv6_subnet(self): + net = self._make_network(self.fmt, "n", True) + subnet = self._make_subnet( + self.fmt, + net, + gateway="fe80::1", + cidr="fe80::/64", + ip_version=6, + )["subnet"] + router = self._create_router() + self._attach_expecting_reject(subnet["id"], router["id"]) + + @pytest.mark.scenario("SVI-VAL-CONFLICT-01") + def test_svi_rejects_conflicting_address_scopes(self): + net_a = self._make_network(self.fmt, "net-a", True) + subnet_a = self._scoped_ipv4_subnet( + net_a, + name="a", + pool_prefix="10.1.0.0/16", + cidr="10.1.0.0/24", + gateway="10.1.0.1", + ) + net_b = self._make_network(self.fmt, "net-b", True) + subnet_b = self._scoped_ipv4_subnet( + net_b, + name="b", + pool_prefix="10.2.0.0/16", + cidr="10.2.0.0/24", + gateway="10.2.0.1", + ) + router = self._create_router() + # First subnet (scope A) attaches cleanly. + with self._as_svi_router(): + self.l3_plugin.add_router_interface( + self.context, router["id"], {"subnet_id": subnet_a["id"]} + ) + # Second subnet has a different scope -> rejected. + self._attach_expecting_reject(subnet_b["id"], router["id"]) From 0dfcc95f1039b55c05d1d71d8664ebf3ebf39052 Mon Sep 17 00:00:00 2001 From: Doug Goldstein <doug.goldstein@rackspace.com> Date: Mon, 24 Aug 2026 18:26:04 -0500 Subject: [PATCH 11/21] test(neutron-understack): trunk delete, multi, unbound, seg-id scenarios TRUNK-DEL-01 asserts deleting a trunk cleans the parent switchport and syncs. TRUNK-MULTI-01 adds two subports in one operation. TRUNK-PARENT- UNBOUND-01 asserts an unbound parent yields no switchport config and no sync. TRUNK-SEGID-RANGE-01 asserts a subport seg_id outside [1, 3799] is rejected (SubportSegmentationIDError surfaced as CallbackFailure). --- .../tests/scenarios/SCENARIOS.md | 33 ++++++-- .../tests/scenarios/test_trunk_operations.py | 84 +++++++++++++++++++ 2 files changed, 109 insertions(+), 8 deletions(-) diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md b/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md index d7c0f974e..61a2e905e 100644 --- a/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md +++ b/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md @@ -116,6 +116,27 @@ adds/removes must reconcile the parent's switch (VLAN group). - then: `undersync.sync(physnet1)` still fires — a parent with no IP does not suppress the reconcile +### TRUNK-DEL-01 — trunk delete syncs the parent's physnet +- given: a bound baremetal parent with a trunk and an attached subport +- when: the trunk is deleted +- then: the parent switchport is cleaned and `undersync.sync(physnet1)` fires + +### TRUNK-MULTI-01 — adding multiple subports syncs the parent's physnet +- given: a bound baremetal parent with a trunk +- when: two subports on different networks are added in one operation +- then: `undersync.sync(physnet1)` fires + +### TRUNK-PARENT-UNBOUND-01 — subport add with an unbound parent is a no-op +- given: an unbound (plain) parent port with a trunk +- when: a subport is added +- then: no switchport config and no `undersync.sync` (nothing to reconcile) + +### TRUNK-SEGID-RANGE-01 — subport seg_id outside the allowed range is rejected +- given: a bound baremetal parent with a trunk +- when: a subport is added with a segmentation_id outside `[1, 3799]` +- then: `SubportSegmentationIDError` (raised in the SUBPORTS PRECOMMIT_CREATE + callback, surfaced as `CallbackFailure`) + ## Router interface (VRF flavor) These scenarios load a real L3 router + flavors plugin (`ML2TestFramework`). The @@ -189,16 +210,12 @@ Port create / bind / delete (no new test doubles): Trunk (no new test doubles): - TRUNK-CREATE-WITH-SUBPORTS-01 — trunk created with subports present - (`trunk_created` path) syncs the parent physnet. -- TRUNK-DEL-01 — deleting a trunk cleans the parent switchport and syncs. + (`trunk_created` path); confirm whether it syncs (it may not — possible gap). - TRUNK-PARENT-UNBIND-01 — unbinding a trunked baremetal parent runs `clean_trunk` (subport teardown) alongside BM-BIND-04's segment handling. -- TRUNK-PARENT-UNBOUND-01 — subport add with an unbound parent is a no-op (no sync). -- TRUNK-SEGID-RANGE-01 — subport seg_id outside the allowed range raises - `SubportSegmentationIDError` (negative). -- TRUNK-NOPHYSNET-01 — subport add/remove on a parent with no `physical_network` - raises `BadRequest` (negative). -- TRUNK-MULTI-01 — multiple subports across physnets in one operation. +- TRUNK-NOPHYSNET-01 — subport add/remove on a parent with no `physical_network`. + Not reachable via normal binding (a baremetal port cannot bind without a + physnet), so needs an artificially mutated binding profile. SVI validation: - SVI-EXTGW-01 — an SVI router cannot get an external gateway. Needs the real diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/test_trunk_operations.py b/python/neutron-understack/neutron_understack/tests/scenarios/test_trunk_operations.py index ea8b6c6ca..dd58c69e6 100644 --- a/python/neutron-understack/neutron_understack/tests/scenarios/test_trunk_operations.py +++ b/python/neutron-understack/neutron_understack/tests/scenarios/test_trunk_operations.py @@ -7,6 +7,7 @@ from unittest import mock import pytest +from neutron_lib.callbacks import exceptions as cb_exc from neutron_understack.tests.scenarios.base import DEFAULT_PHYSNET from neutron_understack.tests.scenarios.base import UnderstackMl2TrunkScenarioBase @@ -114,3 +115,86 @@ def test_subport_add_syncs_when_parent_has_no_ip(self): # The parent having no IP does not suppress the switch reconcile. self.undersync_mock.sync.assert_any_call(DEFAULT_PHYSNET) + + @pytest.mark.scenario("TRUNK-DEL-01") + def test_trunk_delete_syncs_parent_physnet(self): + parent_net = self._make_network(self.fmt, "parent-net", True)["network"]["id"] + parent_id = self._bind_baremetal_port(parent_net, DEFAULT_PHYSNET, "host-a") + subport_net = self._make_network(self.fmt, "subport-net", True)["network"]["id"] + subport_id = self._plain_port(subport_net) + trunk_id = self._make_trunk(parent_id) + self._add_subport(trunk_id, subport_id) + + self.undersync_mock.reset_mock() + with mock.patch( + "neutron_understack.utils.fetch_network_node_trunk_id", + return_value=_FAKE_NN_TRUNK, + ): + self.trunk_plugin.delete_trunk(self.context, trunk_id) + + self.undersync_mock.sync.assert_any_call(DEFAULT_PHYSNET) + + @pytest.mark.scenario("TRUNK-PARENT-UNBOUND-01") + def test_subport_add_unbound_parent_no_sync(self): + # An unbound (plain) parent port: no switchport config, no sync. + parent_net = self._make_network(self.fmt, "parent-net", True)["network"]["id"] + parent_id = self._plain_port(parent_net) + subport_net = self._make_network(self.fmt, "subport-net", True)["network"]["id"] + subport_id = self._plain_port(subport_net) + trunk_id = self._make_trunk(parent_id) + + self.undersync_mock.reset_mock() + self._add_subport(trunk_id, subport_id) + + self.undersync_mock.sync.assert_not_called() + + @pytest.mark.scenario("TRUNK-MULTI-01") + def test_multiple_subports_add_syncs(self): + parent_net = self._make_network(self.fmt, "parent-net", True)["network"]["id"] + parent_id = self._bind_baremetal_port(parent_net, DEFAULT_PHYSNET, "host-a") + sub_net_a = self._make_network(self.fmt, "sub-a", True)["network"]["id"] + sub_net_b = self._make_network(self.fmt, "sub-b", True)["network"]["id"] + subport_a = self._plain_port(sub_net_a) + subport_b = self._plain_port(sub_net_b) + trunk_id = self._make_trunk(parent_id) + + self.undersync_mock.reset_mock() + with mock.patch( + "neutron_understack.utils.fetch_network_node_trunk_id", + return_value=_FAKE_NN_TRUNK, + ): + self.trunk_plugin.add_subports( + self.context, + trunk_id, + { + "sub_ports": [ + { + "port_id": subport_a, + "segmentation_type": "vlan", + "segmentation_id": 1500, + }, + { + "port_id": subport_b, + "segmentation_type": "vlan", + "segmentation_id": 1600, + }, + ] + }, + ) + + self.undersync_mock.sync.assert_any_call(DEFAULT_PHYSNET) + + @pytest.mark.scenario("TRUNK-SEGID-RANGE-01") + def test_subport_segid_out_of_range_rejected(self): + parent_net = self._make_network(self.fmt, "parent-net", True)["network"]["id"] + parent_id = self._bind_baremetal_port(parent_net, DEFAULT_PHYSNET, "host-a") + subport_net = self._make_network(self.fmt, "subport-net", True)["network"]["id"] + subport_id = self._plain_port(subport_net) + trunk_id = self._make_trunk(parent_id) + + # 4000 is a valid VLAN but outside the default tenant range [1, 3799]. + # subports_added raises SubportSegmentationIDError, which the SUBPORTS + # PRECOMMIT_CREATE callback machinery re-raises as CallbackFailure. + with pytest.raises(cb_exc.CallbackFailure) as exc_info: + self._add_subport(trunk_id, subport_id, seg_id=4000) + assert "Segmentation ID" in str(exc_info.value) From 9383466de775c6fddc985ad4ab19c1c53c909bc1 Mon Sep 17 00:00:00 2001 From: Doug Goldstein <doug.goldstein@rackspace.com> Date: Mon, 24 Aug 2026 18:46:11 -0500 Subject: [PATCH 12/21] test(neutron-understack): non-flavored router uplink scenarios Add a FakeOvnClient (records localnet LSP create/delete, short-circuits the vxlan HCG workaround) and UnderstackMl2RouterOvnScenarioBase (L3 + flavors + trunk plugin, network-node physnet + OVS opts). RTR-ATTACH-01 asserts a non-flavored router attach builds the uplink (dynamic segment, shared port, network-node trunk subport, OVN localnet). RTR-SECOND-01 asserts a second router on the network is a no-op. RTR-DETACH-01 asserts remove_router_interface tears the uplink down. --- .../tests/scenarios/SCENARIOS.md | 38 ++++-- .../tests/scenarios/base.py | 31 +++++ .../tests/scenarios/fakes.py | 53 ++++++++ .../tests/scenarios/test_router_uplink.py | 128 ++++++++++++++++++ 4 files changed, 242 insertions(+), 8 deletions(-) create mode 100644 python/neutron-understack/neutron_understack/tests/scenarios/fakes.py create mode 100644 python/neutron-understack/neutron_understack/tests/scenarios/test_router_uplink.py diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md b/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md index 61a2e905e..73e70e1b4 100644 --- a/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md +++ b/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md @@ -183,6 +183,31 @@ mirroring the existing unit tests. - when: a subnet in a different scope B is attached - then: the attach is rejected (per-IP-version scope conflict) +## Router uplink (non-flavored, OVN) + +These load an L3 router + flavors + trunk plugin +(`UnderstackMl2RouterOvnScenarioBase`) and patch `routers.ovn_client` with a +`FakeOvnClient` (records localnet LSP create/delete; short-circuits the vxlan +HCG workaround). `utils.fetch_network_node_trunk_id` is mocked to a trunk the +test creates. + +### RTR-ATTACH-01 — non-flavored router attach builds the uplink +- given: a network+subnet, a network-node trunk, and a non-flavored router +- when: the subnet is attached on the internal side +- then: a dynamic uplink segment is allocated, a shared `uplink-` neutron port + is created, added to the network-node trunk, and an OVN localnet LSP is + created on the network's logical switch + +### RTR-SECOND-01 — second router on the same network is a no-op +- given: a network with two subnets, the first already attached to a router +- when: a second router attaches the second subnet +- then: no new uplink is built (`is_only_router_port_on_network` is false) + +### RTR-DETACH-01 — remove_router_interface tears down the uplink +- given: a network with a router interface and its uplink +- when: the interface is removed +- then: the OVN uplink LSPs are deleted and the shared `uplink-` port removed + ## Known bugs (surfaced by these tests) - **Dynamic VLAN segment leaks on vif-detach** (BM-BIND-04, `xfail`, @@ -223,15 +248,12 @@ SVI validation: which the patched-flavor scenarios do not load. Router (needs an OVN IDL fake for `routers.ovn_client()`): -- RTR-ATTACH-01 — non-flavored router attach builds the uplink: dynamic segment, - shared neutron port, network-node trunk subport, OVN localnet port. -- RTR-DETACH-01 — `remove_router_interface` tears the uplink down - (`handle_router_interface_after_delete`). -- RTR-DELETE-01 — `delete_router` tears the uplink down - (`handle_router_interface_removal`). -- RTR-SECOND-01 — a second router on the same network is a no-op. +- RTR-DELETE-01 — `delete_router` cleanup via `handle_router_interface_removal` + (PORT PRECOMMIT_DELETE). Not reachable with the standard L3 plugin, which + raises RouterInUse unless interfaces are removed first; needs a trigger that + deletes a router-interface port directly. - RTR-HCG-VXLAN-01 — `link_vxlan_network_ha_chassis_group` populates the unified - HCG for a vxlan external gateway. + HCG for a vxlan external gateway (needs a deeper OVN NB/SB fake). VNI / providers: - VNI-ALLOC-01 — VRF router create/delete allocates/releases an `evpn_vni` diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/base.py b/python/neutron-understack/neutron_understack/tests/scenarios/base.py index 3379ab551..08b34f33e 100644 --- a/python/neutron-understack/neutron_understack/tests/scenarios/base.py +++ b/python/neutron-understack/neutron_understack/tests/scenarios/base.py @@ -18,6 +18,7 @@ from unittest import mock +from neutron.conf.agent import ovs_conf from neutron.conf.plugins.ml2.drivers import driver_type from neutron.tests.unit.plugins.ml2.base import ML2TestFramework from neutron.tests.unit.plugins.ml2.test_plugin import Ml2PluginV2TestCase @@ -155,6 +156,36 @@ class UnderstackMl2RouterScenarioBase(_UnderstackMl2ScenarioMixin, ML2TestFramew """ +class UnderstackMl2RouterOvnScenarioBase(UnderstackMl2RouterScenarioBase): + """Router base for the non-flavored uplink path (OVN + network-node trunk). + + Loads the trunk plugin (the uplink adds the shared port to the network-node + trunk) and configures ``network_node_switchport_physnet``. Scenarios patch + ``routers.ovn_client`` with a FakeOvnClient and mock + ``utils.fetch_network_node_trunk_id`` to a trunk they create. + """ + + #: A VLAN physnet the parent setUp configures (physnet3: 400-500), used for + #: the router<->network-node uplink segment. + NETWORK_NODE_PHYSNET = "physnet3" + + def get_additional_service_plugins(self): + plugins = super().get_additional_service_plugins() + plugins["trunk_plugin_name"] = "neutron.services.trunk.plugin.TrunkPlugin" + return plugins + + def setUp(self): + super().setUp() + cfg.CONF.set_override( + "network_node_switchport_physnet", + self.NETWORK_NODE_PHYSNET, + group="ml2_understack", + ) + # create_uplink_port reads cfg.CONF.OVS igmp options. + ovs_conf.register_ovs_agent_opts(cfg.CONF) + self.trunk_plugin = directory.get_plugin("trunk") + + class UnderstackMl2TrunkScenarioBase(UnderstackMl2ScenarioBase): """Base for trunk scenarios: loads the real neutron trunk service plugin. diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/fakes.py b/python/neutron-understack/neutron_understack/tests/scenarios/fakes.py new file mode 100644 index 000000000..3440ecfca --- /dev/null +++ b/python/neutron-understack/neutron_understack/tests/scenarios/fakes.py @@ -0,0 +1,53 @@ +"""In-harness fakes for external systems the router path reaches into. + +The router uplink path (``neutron_understack.routers``) talks to the OVN +Northbound IDL via ``routers.ovn_client()``. These fakes record the localnet +port operations and make the vxlan-HCG workaround short-circuit, so router +scenarios can run without a real OVN database. +""" + +import contextlib +from unittest import mock + + +class FakeNbIdl: + """Minimal OVN Northbound IDL: records localnet LSP create/delete.""" + + def __init__(self): + self.created_ports = [] + self.deleted_ports = [] + + def create_lswitch_port(self, **kwargs): + self.created_ports.append(kwargs) + return mock.MagicMock(name="create_lswitch_port_cmd") + + def delete_lswitch_port(self, **kwargs): + self.deleted_ports.append(kwargs) + return mock.MagicMock(name="delete_lswitch_port_cmd") + + def lookup(self, table, name, default=None): + # Report the per-network HCG as already populated so + # link_vxlan_network_ha_chassis_group returns early (that workaround has + # its own dedicated scenario; the uplink scenarios don't exercise it). + if table == "HA_Chassis_Group": + return mock.MagicMock(ha_chassis=[mock.MagicMock()]) + return default + + def db_list_rows(self, table): + cmd = mock.MagicMock() + cmd.execute.return_value = [] + return cmd + + def transaction(self, **kwargs): + return contextlib.nullcontext(mock.MagicMock()) + + +class FakeOvnClient: + """Stand-in for the OVN ML2 driver's ``_ovn_client``.""" + + def __init__(self): + self._nb_idl = FakeNbIdl() + self._sb_idl = mock.MagicMock() + + def _transaction(self, commands, txn=None): + return None diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/test_router_uplink.py b/python/neutron-understack/neutron_understack/tests/scenarios/test_router_uplink.py new file mode 100644 index 000000000..896ab1aeb --- /dev/null +++ b/python/neutron-understack/neutron_understack/tests/scenarios/test_router_uplink.py @@ -0,0 +1,128 @@ +"""Scenario tests for the non-flavored router uplink path (OVN + trunk). + +See neutron_understack/tests/scenarios/SCENARIOS.md (RTR-*) for the catalog. +""" + +import contextlib +from unittest import mock + +import pytest +from neutron.common.ovn import utils as ovn_utils + +from neutron_understack.tests.scenarios.base import UnderstackMl2RouterOvnScenarioBase +from neutron_understack.tests.scenarios.fakes import FakeOvnClient + + +class TestRouterUplink(UnderstackMl2RouterOvnScenarioBase): + def _plain_port(self, net_id): + res = self._create_port(self.fmt, net_id, is_admin=True) + assert res.status_int == 201, res.body + return self.deserialize(self.fmt, res)["port"]["id"] + + def _make_network_node_trunk(self): + """Create a stand-in network-node trunk and return its id.""" + nn_net = self._make_network(self.fmt, "nn-net", True)["network"]["id"] + nn_parent = self._plain_port(nn_net) + trunk = self.trunk_plugin.create_trunk( + self.context, + { + "trunk": { + "port_id": nn_parent, + "project_id": self._project_id, + "admin_state_up": True, + "sub_ports": [], + } + }, + ) + return trunk["id"] + + def _router_net_with_subnet(self, name="router-net"): + net = self._make_network(self.fmt, name, True) + subnet = self._make_subnet( + self.fmt, net, gateway="10.20.0.1", cidr="10.20.0.0/24" + )["subnet"] + return net["network"]["id"], subnet["id"] + + @contextlib.contextmanager + def _ovn(self, fake, nn_trunk_id): + with ( + mock.patch("neutron_understack.routers.ovn_client", return_value=fake), + mock.patch( + "neutron_understack.utils.fetch_network_node_trunk_id", + return_value=nn_trunk_id, + ), + ): + yield + + def _shared_uplink_ports(self, net_id): + ports = self.core_plugin.get_ports( + self.context, filters={"network_id": [net_id]} + ) + return [p for p in ports if (p.get("name") or "").startswith("uplink-")] + + @pytest.mark.scenario("RTR-ATTACH-01") + def test_nonflavored_router_attach_builds_uplink(self): + nn_trunk_id = self._make_network_node_trunk() + net_id, subnet_id = self._router_net_with_subnet() + fake = FakeOvnClient() + router = self._create_router() + + with self._ovn(fake, nn_trunk_id): + self.l3_plugin.add_router_interface( + self.context, router["id"], {"subnet_id": subnet_id} + ) + + # An OVN localnet uplink port was created on this network's switch. + assert len(fake._nb_idl.created_ports) == 1, fake._nb_idl.created_ports + created = fake._nb_idl.created_ports[0] + assert created["lswitch_name"] == ovn_utils.ovn_name(net_id) + assert created["lport_name"].startswith("uplink-") + # A shared "uplink-" neutron port was created on the network. + assert self._shared_uplink_ports(net_id) + + @pytest.mark.scenario("RTR-SECOND-01") + def test_second_router_on_network_is_noop(self): + nn_trunk_id = self._make_network_node_trunk() + # One network, two subnets (routers can't share a subnet's gateway IP). + net = self._make_network(self.fmt, "router-net", True) + net_id = net["network"]["id"] + subnet_a = self._make_subnet( + self.fmt, net, gateway="10.20.0.1", cidr="10.20.0.0/24" + )["subnet"] + subnet_b = self._make_subnet( + self.fmt, net, gateway="10.21.0.1", cidr="10.21.0.0/24" + )["subnet"] + fake = FakeOvnClient() + router_a = self._create_router() + router_b = self._create_router() + + with self._ovn(fake, nn_trunk_id): + self.l3_plugin.add_router_interface( + self.context, router_a["id"], {"subnet_id": subnet_a["id"]} + ) + self.l3_plugin.add_router_interface( + self.context, router_b["id"], {"subnet_id": subnet_b["id"]} + ) + + # Only the first router built the uplink; the second is a no-op. + assert len(fake._nb_idl.created_ports) == 1, fake._nb_idl.created_ports + assert len(self._shared_uplink_ports(net_id)) == 1 + + @pytest.mark.scenario("RTR-DETACH-01") + def test_remove_router_interface_tears_down_uplink(self): + nn_trunk_id = self._make_network_node_trunk() + net_id, subnet_id = self._router_net_with_subnet() + fake = FakeOvnClient() + router = self._create_router() + + with self._ovn(fake, nn_trunk_id): + self.l3_plugin.add_router_interface( + self.context, router["id"], {"subnet_id": subnet_id} + ) + self.l3_plugin.remove_router_interface( + self.context, router["id"], {"subnet_id": subnet_id} + ) + + # The OVN uplink LSPs were deleted and the shared port removed. + assert fake._nb_idl.deleted_ports, fake._nb_idl.deleted_ports + assert self._shared_uplink_ports(net_id) == [] From 3f37ff7931928ccd38ab2446c065a89d6dd05aaf Mon Sep 17 00:00:00 2001 From: Doug Goldstein <doug.goldstein@rackspace.com> Date: Mon, 24 Aug 2026 18:51:16 -0500 Subject: [PATCH 13/21] test(neutron-understack): Palo Alto router flavor scenarios Wire a real L3 service provider + flavor + service profile for the Palo Alto router flavor, with IronicClient faked (single-node netdev pool). PALO-ADOPT-01 asserts creating a router of that flavor adopts the available node; PALO-RELEASE-01 asserts deleting it returns the node. Adds FakeIronicClient to the scenario fakes. --- .../tests/scenarios/SCENARIOS.md | 27 ++++- .../tests/scenarios/fakes.py | 34 ++++++ .../tests/scenarios/test_palo_alto_router.py | 109 ++++++++++++++++++ 3 files changed, 166 insertions(+), 4 deletions(-) create mode 100644 python/neutron-understack/neutron_understack/tests/scenarios/test_palo_alto_router.py diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md b/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md index 73e70e1b4..b5de0e67d 100644 --- a/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md +++ b/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md @@ -208,6 +208,23 @@ test creates. - when: the interface is removed - then: the OVN uplink LSPs are deleted and the shared `uplink-` port removed +## Router flavor providers (Palo Alto) + +These register the Palo Alto provider as an L3 service provider and create a real +flavor + service profile (`driver` = the PaloAlto class, `metainfo.resource_class` += the netdev pool). `IronicClient` is faked (single-node pool). + +### PALO-ADOPT-01 — Palo Alto router adopts an Ironic netdev node +- given: the Palo Alto provider registered and a matching flavor +- when: a router with that flavor is created +- then: the ROUTER BEFORE_CREATE callback adopts the available netdev node for + the router (via the faked Ironic client) + +### PALO-RELEASE-01 — deleting a Palo Alto router releases its node +- given: a Palo Alto router that adopted a node +- when: the router is deleted +- then: the ROUTER AFTER_DELETE callback returns the node to the pool + ## Known bugs (surfaced by these tests) - **Dynamic VLAN segment leaks on vif-detach** (BM-BIND-04, `xfail`, @@ -255,11 +272,13 @@ Router (needs an OVN IDL fake for `routers.ovn_client()`): - RTR-HCG-VXLAN-01 — `link_vxlan_network_ha_chassis_group` populates the unified HCG for a vxlan external gateway (needs a deeper OVN NB/SB fake). -VNI / providers: +VNI: - VNI-ALLOC-01 — VRF router create/delete allocates/releases an `evpn_vni` - (`UnderstackVniPlugin`). -- PALO-ADOPT-01 — a Palo Alto flavored router adopts a netdev Ironic node - (needs an Ironic client fake). + (`UnderstackVniPlugin`). Needs the understack_vni service plugin loaded, its + `understack_router_vni_allocations` table created (import the model so the + SQLite fixture builds it), and a flavor whose service-profile metainfo sets + `vni_alloc`. The Vrf/UserDefined provider it pairs with may pull in OVN L3 on + router create, so it likely also needs the OVN fake. Cross-cutting: - DRYRUN-01 — `undersync_dry_run=True` routes to dry-run instead of sync. diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/fakes.py b/python/neutron-understack/neutron_understack/tests/scenarios/fakes.py index 3440ecfca..7db5a568e 100644 --- a/python/neutron-understack/neutron_understack/tests/scenarios/fakes.py +++ b/python/neutron-understack/neutron_understack/tests/scenarios/fakes.py @@ -51,3 +51,37 @@ def __init__(self): def _transaction(self, commands, txn=None): return None + + +class FakeIronicClient: + """Stand-in for neutron_understack.ironic.IronicClient (netdev pool). + + Models a single-node pool: one node is available until adopted, then the + router->node mapping is remembered so release can return it. Records the + adopt/release calls for assertions. + """ + + def __init__(self, node_id="netdev-node-1"): + self._node = mock.MagicMock(id=node_id) + self._available = True + self._by_router = {} + self.adopted = [] + self.released = [] + + def available_node_for_resource_class(self, resource_class): + return self._node if self._available else None + + def adopt_node_for_router(self, node, project_id, router_id, router_name): + self._available = False + self._by_router[router_id] = node + self.adopted.append( + {"node": node, "project_id": project_id, "router_id": router_id} + ) + return node + + def release_node_for_router(self, router_id): + node = self._by_router.pop(router_id, None) + if node is not None: + self._available = True + self.released.append(router_id) + return node diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/test_palo_alto_router.py b/python/neutron-understack/neutron_understack/tests/scenarios/test_palo_alto_router.py new file mode 100644 index 000000000..78acfc20a --- /dev/null +++ b/python/neutron-understack/neutron_understack/tests/scenarios/test_palo_alto_router.py @@ -0,0 +1,109 @@ +"""Scenario tests for the Palo Alto router flavor (Ironic netdev adoption). + +See neutron_understack/tests/scenarios/SCENARIOS.md (PALO-*) for the catalog. +""" + +import json +from unittest import mock + +import pytest +from neutron_lib.plugins import constants as plugin_constants +from neutron_lib.plugins import directory +from oslo_config import cfg + +from neutron_understack.tests.scenarios.base import UnderstackMl2RouterScenarioBase +from neutron_understack.tests.scenarios.fakes import FakeIronicClient + +PALO_DRIVER = "neutron_understack.l3_router.palo_alto.PaloAlto" + + +class TestPaloAltoRouter(UnderstackMl2RouterScenarioBase): + def setUp(self): + # Register the Palo Alto provider so its ROUTER callbacks are wired. + cfg.CONF.set_override( + "service_provider", + [f"L3_ROUTER_NAT:palo_alto:{PALO_DRIVER}:default"], + group="service_providers", + ) + super().setUp() + self.flavor_plugin = directory.get_plugin(plugin_constants.FLAVORS) + + def _palo_flavor(self, resource_class="netdev-fw"): + profile = self.flavor_plugin.create_service_profile( + self.context, + { + "service_profile": { + "driver": PALO_DRIVER, + "metainfo": json.dumps({"resource_class": resource_class}), + "enabled": True, + "description": "palo alto", + } + }, + ) + flavor = self.flavor_plugin.create_flavor( + self.context, + { + "flavor": { + "name": "palo-alto", + "service_type": plugin_constants.L3, + "enabled": True, + "description": "palo alto flavor", + } + }, + ) + self.flavor_plugin.create_flavor_service_profile( + self.context, + {"service_profile": {"id": profile["id"]}}, + flavor["id"], + ) + return flavor["id"] + + @pytest.mark.scenario("PALO-ADOPT-01") + def test_palo_alto_router_adopts_ironic_node(self): + fake_ironic = FakeIronicClient() + flavor_id = self._palo_flavor() + + with mock.patch( + "neutron_understack.l3_router.palo_alto.IronicClient", + return_value=fake_ironic, + ): + router = self.l3_plugin.create_router( + self.context, + { + "router": { + "name": "pa-router", + "admin_state_up": True, + "project_id": self._project_id, + "flavor_id": flavor_id, + } + }, + ) + + # The router adopted the single available netdev node. + assert len(fake_ironic.adopted) == 1, fake_ironic.adopted + assert fake_ironic.adopted[0]["router_id"] == router["id"] + + @pytest.mark.scenario("PALO-RELEASE-01") + def test_palo_alto_router_delete_releases_ironic_node(self): + fake_ironic = FakeIronicClient() + flavor_id = self._palo_flavor() + + with mock.patch( + "neutron_understack.l3_router.palo_alto.IronicClient", + return_value=fake_ironic, + ): + router = self.l3_plugin.create_router( + self.context, + { + "router": { + "name": "pa-router", + "admin_state_up": True, + "project_id": self._project_id, + "flavor_id": flavor_id, + } + }, + ) + self.l3_plugin.delete_router(self.context, router["id"]) + + # The adopted node was returned to the pool on delete. + assert fake_ironic.released == [router["id"]], fake_ironic.released From 4c7f312c9f97e037787fa6a22e265319153090f2 Mon Sep 17 00:00:00 2001 From: Doug Goldstein <doug.goldstein@rackspace.com> Date: Mon, 24 Aug 2026 18:53:09 -0500 Subject: [PATCH 14/21] test(neutron-understack): VRF router detach sync scenario (xfail) VRF-DETACH-01 is the teardown counterpart of VRF-RTR-01: removing a VRF router interface should sync the physnets of baremetal ports still bound on the network, but does not today (understack#2240). Marked xfail(strict=True). Note DRYRUN-01 in the backlog: undersync_dry_run lives inside the (mocked) Undersync client, so it has no scenario-observable effect. --- .../tests/scenarios/SCENARIOS.md | 14 +++++-- .../scenarios/test_vrf_router_interface.py | 42 +++++++++++++++++++ 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md b/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md index b5de0e67d..119450026 100644 --- a/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md +++ b/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md @@ -155,6 +155,14 @@ mirroring the existing unit tests. called for them). The test asserts the desired behavior and is `xfail(strict=True)`. +### VRF-DETACH-01 — VRF router detach syncs bound baremetal port physnets +- given: a VXLAN network with baremetal ports on two physnets and an attached + VRF router interface +- when: the router interface is removed +- then: undersync should sync each physnet still carrying baremetal ports +- status: **KNOWN BUG (xfail, rackerlabs/understack#2240)** — the teardown + counterpart of VRF-RTR-01; detach does not sync those physnets today. + ### SVI-RTR-01 — SVI router attach syncs bound baremetal port physnets - given: a VXLAN network + an address-scoped IPv4 subnet, with baremetal ports bound to two different physnets (`physnet1`, `physnet2`) @@ -281,8 +289,8 @@ VNI: router create, so it likely also needs the OVN fake. Cross-cutting: -- DRYRUN-01 — `undersync_dry_run=True` routes to dry-run instead of sync. -- RTR-DETACH-SYNC-01 — router detach should sync the network's bound baremetal - physnets (teardown counterpart of #2240; likely xfail). +- DRYRUN-01 — `undersync_dry_run=True` routes to dry-run instead of sync. This + branch lives inside `Undersync.sync()` (an HTTP-client detail) which the + scenarios mock, so it has no scenario-observable effect; better unit-tested. Explicitly out of scope: Cisco ASA floating-IP NAT. diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/test_vrf_router_interface.py b/python/neutron-understack/neutron_understack/tests/scenarios/test_vrf_router_interface.py index 0fff16d03..bc14d2471 100644 --- a/python/neutron-understack/neutron_understack/tests/scenarios/test_vrf_router_interface.py +++ b/python/neutron-understack/neutron_understack/tests/scenarios/test_vrf_router_interface.py @@ -60,3 +60,45 @@ def test_vrf_router_interface_syncs_bound_port_physnets(self): } assert DEFAULT_PHYSNET in synced assert SECOND_PHYSNET in synced + + # BUG: the teardown counterpart of the attach gap -- detaching a VRF router + # interface also fails to sync the network's bound baremetal physnets. + @pytest.mark.xfail( + strict=True, + reason=( + "understack#2240: VRF router interface detach does not sync the " + "physnets of baremetal ports still bound on the network" + ), + ) + @pytest.mark.scenario("VRF-DETACH-01") + def test_vrf_router_interface_detach_syncs_bound_port_physnets(self): + net = self._make_network(self.fmt, "vxlan-net", True) + net_id = net["network"]["id"] + subnet = self._make_subnet( + self.fmt, net, gateway="10.0.0.1", cidr="10.0.0.0/24" + )["subnet"] + self._bind_baremetal_port(net_id, DEFAULT_PHYSNET, "host-a") + self._bind_baremetal_port(net_id, SECOND_PHYSNET, "host-b") + + with mock.patch( + "neutron_understack.routers._router_has_flavor", return_value=True + ): + router = self._create_router() + self.l3_plugin.add_router_interface( + self.context, router["id"], {"subnet_id": subnet["id"]} + ) + # Isolate the detach from the attach/bind sync calls. + self.undersync_mock.reset_mock() + self.l3_plugin.remove_router_interface( + self.context, router["id"], {"subnet_id": subnet["id"]} + ) + + # Desired: detaching the router reconciles the switches still carrying + # the network's baremetal ports. + synced = { + call.args[0] + for call in self.undersync_mock.sync.call_args_list + if call.args + } + assert DEFAULT_PHYSNET in synced + assert SECOND_PHYSNET in synced From 46c73e744e04dce9d1d04a15ed777610914d585c Mon Sep 17 00:00:00 2001 From: Doug Goldstein <doug.goldstein@rackspace.com> Date: Mon, 24 Aug 2026 20:48:13 -0500 Subject: [PATCH 15/21] test(neutron-understack): address code-review findings on scenario suite Act on the PR code review (test/CI hygiene; no product code changed): - Gate the EnricoMi publish job to same-repo PRs (fork tokens are read-only and would fail the check). - Add a test asserting the neutron/neutron-lib revs in [tool.uv.sources] match the container Dockerfile ARGs, so they cannot silently drift. - Share catalog-heading parsing (catalog.py) between the coverage test and the report script, removing the divergent regexes. - Render a strict-xfail XPASS distinctly, not a plain fail. - Scope the SVI IPv6 subnet so SVI-VAL-IPV6-01 pins the IPv6 guard. --- .github/workflows/code-test.yaml | 4 +- .../tests/scenarios/catalog.py | 28 ++++++++++ .../tests/scenarios/test_scenario_coverage.py | 11 ++-- .../scenarios/test_svi_router_interface.py | 52 ++++++++++++++++--- .../tests/test_container_rev_lockstep.py | 45 ++++++++++++++++ scripts/scenario-report.py | 21 ++++---- 6 files changed, 135 insertions(+), 26 deletions(-) create mode 100644 python/neutron-understack/neutron_understack/tests/scenarios/catalog.py create mode 100644 python/neutron-understack/neutron_understack/tests/test_container_rev_lockstep.py diff --git a/.github/workflows/code-test.yaml b/.github/workflows/code-test.yaml index 034f70a36..bc68b5814 100644 --- a/.github/workflows/code-test.yaml +++ b/.github/workflows/code-test.yaml @@ -110,7 +110,9 @@ jobs: # need a separate workflow_run-triggered workflow (not wired up here). publish-test-results: needs: [uv] - if: ${{ always() }} + # Skip on fork PRs: the token is read-only there, so the action cannot post + # the check/comment. Forks would need a separate workflow_run workflow. + if: ${{ always() && github.event.pull_request.head.repo.fork != true }} runs-on: ubuntu-latest permissions: checks: write diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/catalog.py b/python/neutron-understack/neutron_understack/tests/scenarios/catalog.py new file mode 100644 index 000000000..5f971e306 --- /dev/null +++ b/python/neutron-understack/neutron_understack/tests/scenarios/catalog.py @@ -0,0 +1,28 @@ +"""Single source of truth for parsing the SCENARIOS.md catalog. + +Both the coverage meta-test (test_scenario_coverage.py) and the CI report +(scripts/scenario-report.py) parse scenario IDs from the catalog headings; they +import from here so the heading format is defined once and cannot diverge. +""" + +import re + +#: Catalog IDs are declared as headings: "### <ID> — <title>" (title optional). +_CATALOG_RE = re.compile( + r"^#{2,3}\s+([A-Z][A-Z0-9]*(?:-[A-Z0-9]+)+)(?:\s*[—-]\s*(.*))?$" +) + + +def parse_catalog(text): + """Return an ordered list of ``(scenario_id, title)`` from the catalog.""" + entries = [] + for line in text.splitlines(): + match = _CATALOG_RE.match(line) + if match: + entries.append((match.group(1), (match.group(2) or "").strip())) + return entries + + +def catalog_ids(text): + """Return an ordered list of catalogued scenario IDs.""" + return [scenario_id for scenario_id, _ in parse_catalog(text)] diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/test_scenario_coverage.py b/python/neutron-understack/neutron_understack/tests/scenarios/test_scenario_coverage.py index 237b50576..9b37ee916 100644 --- a/python/neutron-understack/neutron_understack/tests/scenarios/test_scenario_coverage.py +++ b/python/neutron-understack/neutron_understack/tests/scenarios/test_scenario_coverage.py @@ -11,22 +11,17 @@ import re from pathlib import Path +from neutron_understack.tests.scenarios.catalog import catalog_ids + SCENARIOS_DIR = Path(__file__).parent CATALOG = SCENARIOS_DIR / "SCENARIOS.md" -# Catalog IDs are declared as headings: "### BM-BIND-01 — title". -_CATALOG_ID_RE = re.compile(r"^#{2,3}\s+([A-Z][A-Z0-9]*(?:-[A-Z0-9]+)+)\b") # Markers in test sources: @pytest.mark.scenario("BM-BIND-01"). _MARKER_ID_RE = re.compile(r"""\.scenario\(\s*["']([^"']+)["']""") def _catalog_ids(): - ids = [] - for line in CATALOG.read_text().splitlines(): - match = _CATALOG_ID_RE.match(line) - if match: - ids.append(match.group(1)) - return ids + return catalog_ids(CATALOG.read_text()) def _marker_ids(): diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/test_svi_router_interface.py b/python/neutron-understack/neutron_understack/tests/scenarios/test_svi_router_interface.py index 9604d47bd..d677bd86a 100644 --- a/python/neutron-understack/neutron_understack/tests/scenarios/test_svi_router_interface.py +++ b/python/neutron-understack/neutron_understack/tests/scenarios/test_svi_router_interface.py @@ -73,6 +73,48 @@ def _scoped_ipv4_subnet( subnetpool_id=pool["id"], )["subnet"] + def _scoped_ipv6_subnet(self, network): + """Create an address-scoped IPv6 subnet. + + Scoped so the SVI no-scope guard cannot be what rejects it -- only the + IPv6 guard can -- which is what SVI-VAL-IPV6-01 needs to pin. + """ + scope = self.core_plugin.create_address_scope( + self.context, + { + "address_scope": { + "name": "v6-scope", + "ip_version": 6, + "shared": False, + "tenant_id": self._project_id, + } + }, + ) + pool = self.core_plugin.create_subnetpool( + self.context, + { + "subnetpool": { + "name": "v6-pool", + "prefixes": ["2001:db8::/48"], + "address_scope_id": scope["id"], + "default_prefixlen": 64, + "min_prefixlen": 48, + "max_prefixlen": 128, + "shared": False, + "is_default": False, + "tenant_id": self._project_id, + } + }, + ) + return self._make_subnet( + self.fmt, + network, + gateway="2001:db8::1", + cidr="2001:db8::/64", + ip_version=6, + subnetpool_id=pool["id"], + )["subnet"] + # BUG: like the VRF case (#2240), attaching an SVI router interface does not # reconcile the switches carrying the network's already-bound baremetal # ports -- undersync is never asked to sync those physnets. Asserts the @@ -135,14 +177,10 @@ def test_svi_rejects_subnet_without_address_scope(self): @pytest.mark.scenario("SVI-VAL-IPV6-01") def test_svi_rejects_ipv6_subnet(self): + # Address-scoped so only the IPv6 guard (not the no-scope guard) can + # reject it -- otherwise this would not actually pin IPv6 rejection. net = self._make_network(self.fmt, "n", True) - subnet = self._make_subnet( - self.fmt, - net, - gateway="fe80::1", - cidr="fe80::/64", - ip_version=6, - )["subnet"] + subnet = self._scoped_ipv6_subnet(net) router = self._create_router() self._attach_expecting_reject(subnet["id"], router["id"]) diff --git a/python/neutron-understack/neutron_understack/tests/test_container_rev_lockstep.py b/python/neutron-understack/neutron_understack/tests/test_container_rev_lockstep.py new file mode 100644 index 000000000..47192549a --- /dev/null +++ b/python/neutron-understack/neutron_understack/tests/test_container_rev_lockstep.py @@ -0,0 +1,45 @@ +"""Enforce that the test env and the container build use the same neutron. + +The scenario tests' guarantee -- "validate ML2 behavior against the same neutron +the container ships" -- only holds if the git revs pinned in this package's +[tool.uv.sources] match containers/neutron/Dockerfile's ARGs. This test fails if +they drift (e.g. a partial Renovate bump), rather than relying on a comment. +""" + +import re +import tomllib +from pathlib import Path + +import pytest + +_PKG_ROOT = Path(__file__).resolve().parents[2] +_REPO_ROOT = Path(__file__).resolve().parents[4] +_DOCKERFILE = _REPO_ROOT / "containers" / "neutron" / "Dockerfile" + +# (uv.sources key, Dockerfile ARG name) +_PINS = [ + ("neutron", "NEUTRON_GIT_REF"), + ("neutron-lib", "NEUTRON_LIB_GIT_REF"), +] + + +def _uv_source_rev(name): + data = tomllib.loads((_PKG_ROOT / "pyproject.toml").read_text()) + return data["tool"]["uv"]["sources"][name]["rev"] + + +def _dockerfile_arg(text, arg): + match = re.search(rf"^ARG {arg}=(\S+)", text, re.MULTILINE) + return match.group(1) if match else None + + +@pytest.mark.parametrize(("source_key", "arg_name"), _PINS) +def test_neutron_rev_matches_container(source_key, arg_name): + if not _DOCKERFILE.exists(): + pytest.skip(f"Dockerfile not found at {_DOCKERFILE} (not a repo checkout)") + uv_rev = _uv_source_rev(source_key) + docker_rev = _dockerfile_arg(_DOCKERFILE.read_text(), arg_name) + assert uv_rev == docker_rev, ( + f"{source_key} rev drift: pyproject [tool.uv.sources]={uv_rev} but " + f"Dockerfile {arg_name}={docker_rev}. Keep them in lockstep." + ) diff --git a/scripts/scenario-report.py b/scripts/scenario-report.py index 45da3b8f2..d5da84c5e 100644 --- a/scripts/scenario-report.py +++ b/scripts/scenario-report.py @@ -12,18 +12,19 @@ """ import argparse -import re import sys import xml.etree.ElementTree as ET from pathlib import Path -# Catalog IDs are declared as headings: "### BM-BIND-01 — title". -_CATALOG_RE = re.compile(r"^#{2,3}\s+([A-Z][A-Z0-9]*(?:-[A-Z0-9]+)+)\s*[—-]\s*(.*)$") +# Catalog heading parsing is shared with the coverage meta-test so the format is +# defined once. Available because the script runs in the neutron-understack env. +from neutron_understack.tests.scenarios.catalog import parse_catalog as _parse_text _STATUS = { "pass": "✅ pass", "fail": "❌ fail", "xfail": "⚠️ known bug (xfail)", + "xpass": "🎉 fixed — remove xfail", "skipped": "⏭️ skipped", "missing": "❓ no test", "notrun": "· not run", @@ -32,16 +33,16 @@ def parse_catalog(path): """Return ordered list of (id, title) from the catalog headings.""" - entries = [] - for line in Path(path).read_text().splitlines(): - match = _CATALOG_RE.match(line) - if match: - entries.append((match.group(1), match.group(2).strip())) - return entries + return _parse_text(Path(path).read_text()) def _testcase_status(testcase): - if testcase.find("failure") is not None or testcase.find("error") is not None: + failure = testcase.find("failure") + if failure is not None or testcase.find("error") is not None: + # A strict xfail that unexpectedly passes is a JUnit <failure> whose + # message is "[XPASS(strict)] ...": the bug is fixed, remove the marker. + if failure is not None and (failure.get("message") or "").startswith("[XPASS"): + return "xpass" return "fail" skipped = testcase.find("skipped") if skipped is not None: From 472b468dc221c3acbf86affbb1183989fc349044 Mon Sep 17 00:00:00 2001 From: Doug Goldstein <doug.goldstein@rackspace.com> Date: Tue, 25 Aug 2026 08:46:37 -0500 Subject: [PATCH 16/21] test(neutron-understack): collection-based enforcement + richer asserts Round of review follow-ups on the scenario suite: - Enforce catalog<->test parity from pytest's collected items (in the conftest hook) instead of regex-scanning sources, which could count a marker in a comment or dead code. Validate marker shape and share the scenario-ID pattern via catalog.py; unit-test the validator. - Strengthen the trunk and router scenarios to assert the resulting DB state -- subport binding levels, dynamic VLAN segments (allocated, released, distinct), trunk membership, and the OVN localnet port -- not just that undersync.sync was called. - Update SCENARIOS.md descriptions to match. --- .../tests/scenarios/SCENARIOS.md | 42 ++++++---- .../tests/scenarios/catalog.py | 7 +- .../tests/scenarios/conftest.py | 83 +++++++++++++++---- .../tests/scenarios/test_router_uplink.py | 65 +++++++++++++-- .../tests/scenarios/test_scenario_coverage.py | 65 +++++++-------- .../tests/scenarios/test_trunk_operations.py | 72 ++++++++++++++-- 6 files changed, 253 insertions(+), 81 deletions(-) diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md b/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md index 119450026..329fddff5 100644 --- a/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md +++ b/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md @@ -6,9 +6,9 @@ and the **mechanism calls + data** we expect to observe. Every scenario here must be implemented by at least one test tagged `@pytest.mark.scenario("<ID>")`, and every such marker must reference a scenario -that exists here. `test_scenario_coverage.py` enforces this equivalence in both -directions, and `conftest.py` fails the run if any scenario test is missing a -marker. So this file and the tests cannot silently drift apart. +that exists here. `conftest.py` validates collected markers and enforces this +equivalence whenever the complete scenario package is collected. So this file +and the tests cannot silently drift apart in the scenario CI run. Scenario IDs are declared as `### <ID> — <title>` headings. Ideas under "Planned / future" are intentionally *not* IDs (no `###` heading) so they are not @@ -102,40 +102,50 @@ adds/removes must reconcile the parent's switch (VLAN group). - given: a bound baremetal parent port on `physnet1` with a trunk, and a subport port on another network - when: the subport is added to the trunk (VLAN segmentation) -- then: `undersync.sync(physnet1)` reconciles the parent port's switch +- then: the trunk records the subport, a level-0 `understack` binding points it + at a dynamic VLAN segment on `physnet1`, and `undersync.sync(physnet1)` + reconciles the parent port's switch ### TRUNK-SUB-DEL — subport removal syncs the parent's physnet - given: the TRUNK-SUB-ADD setup with the subport attached - when: the subport is removed from the trunk -- then: `undersync.sync(physnet1)` reconciles the parent port's switch +- then: the trunk result no longer contains the subport, its synthetic binding + level is deleted, its now-unused dynamic VLAN segment is released, and + `undersync.sync(physnet1)` reconciles the parent port's switch ### TRUNK-PARENT-NOIP — subport add syncs when the parent has no IP - given: a bound baremetal parent on a subnetted network but with no fixed IP, plus a trunk and a subport on another network - when: the subport is added -- then: `undersync.sync(physnet1)` still fires — a parent with no IP does not - suppress the reconcile +- then: the subport binding and dynamic segment are created and + `undersync.sync(physnet1)` still fires — a parent with no IP does not suppress + the reconcile ### TRUNK-DEL-01 — trunk delete syncs the parent's physnet - given: a bound baremetal parent with a trunk and an attached subport - when: the trunk is deleted -- then: the parent switchport is cleaned and `undersync.sync(physnet1)` fires +- then: the trunk is gone, the subport binding and now-unused dynamic segment + are deleted, the parent switchport is cleaned, and `undersync.sync(physnet1)` + fires ### TRUNK-MULTI-01 — adding multiple subports syncs the parent's physnet - given: a bound baremetal parent with a trunk - when: two subports on different networks are added in one operation -- then: `undersync.sync(physnet1)` fires +- then: each subport has its own level-0 binding to a dynamic VLAN segment on + `physnet1`, the segments are distinct, and `undersync.sync(physnet1)` fires ### TRUNK-PARENT-UNBOUND-01 — subport add with an unbound parent is a no-op - given: an unbound (plain) parent port with a trunk - when: a subport is added -- then: no switchport config and no `undersync.sync` (nothing to reconcile) +- then: trunk membership is recorded, but no subport binding level or dynamic + VLAN segment is created and no `undersync.sync` occurs ### TRUNK-SEGID-RANGE-01 — subport seg_id outside the allowed range is rejected - given: a bound baremetal parent with a trunk - when: a subport is added with a segmentation_id outside `[1, 3799]` - then: `SubportSegmentationIDError` (raised in the SUBPORTS PRECOMMIT_CREATE - callback, surfaced as `CallbackFailure`) + callback, surfaced as `CallbackFailure`), with no binding level or dynamic + segment allocated ## Router interface (VRF flavor) @@ -202,9 +212,9 @@ test creates. ### RTR-ATTACH-01 — non-flavored router attach builds the uplink - given: a network+subnet, a network-node trunk, and a non-flavored router - when: the subnet is attached on the internal side -- then: a dynamic uplink segment is allocated, a shared `uplink-` neutron port - is created, added to the network-node trunk, and an OVN localnet LSP is - created on the network's logical switch +- then: a dynamic VLAN uplink segment is allocated on the network-node physnet; + the shared `uplink-` neutron port, trunk subport tag, and OVN localnet LSP tag + all reference that segment and VLAN on the network's logical switch ### RTR-SECOND-01 — second router on the same network is a no-op - given: a network with two subnets, the first already attached to a router @@ -214,7 +224,9 @@ test creates. ### RTR-DETACH-01 — remove_router_interface tears down the uplink - given: a network with a router interface and its uplink - when: the interface is removed -- then: the OVN uplink LSPs are deleted and the shared `uplink-` port removed +- then: the shared port is removed from the network-node trunk, both the + `uplink-<segment-id>` localnet LSP and shared-port LSP are deleted from the + exact logical switch, and the shared `uplink-` neutron port is removed ## Router flavor providers (Palo Alto) diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/catalog.py b/python/neutron-understack/neutron_understack/tests/scenarios/catalog.py index 5f971e306..f58fdf160 100644 --- a/python/neutron-understack/neutron_understack/tests/scenarios/catalog.py +++ b/python/neutron-understack/neutron_understack/tests/scenarios/catalog.py @@ -7,10 +7,11 @@ import re +SCENARIO_ID_PATTERN = r"[A-Z][A-Z0-9]*(?:-[A-Z0-9]+)+" +SCENARIO_ID_RE = re.compile(rf"^{SCENARIO_ID_PATTERN}$") + #: Catalog IDs are declared as headings: "### <ID> — <title>" (title optional). -_CATALOG_RE = re.compile( - r"^#{2,3}\s+([A-Z][A-Z0-9]*(?:-[A-Z0-9]+)+)(?:\s*[—-]\s*(.*))?$" -) +_CATALOG_RE = re.compile(rf"^#{{2,3}}\s+({SCENARIO_ID_PATTERN})(?:\s*[—-]\s*(.*))?$") def parse_catalog(text): diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/conftest.py b/python/neutron-understack/neutron_understack/tests/scenarios/conftest.py index ef9490a1f..ee44c1ce1 100644 --- a/python/neutron-understack/neutron_understack/tests/scenarios/conftest.py +++ b/python/neutron-understack/neutron_understack/tests/scenarios/conftest.py @@ -1,35 +1,84 @@ -"""Scenario-test enforcement + JUnit tagging hooks. +"""Scenario-test collection enforcement and JUnit tagging hooks. -- Fails collection if any test under the scenarios package is missing a - ``@pytest.mark.scenario("<ID>")`` marker, so a scenario test cannot silently - escape the catalog↔marker coverage check in test_scenario_coverage.py. +- Validates every collected scenario marker and rejects unknown catalog IDs. +- When all scenario modules are collected, fails if a catalog entry has no test. - Stamps each scenario test's ID onto ``user_properties`` so it surfaces in the JUnit XML as ``<property name="scenario" value="<ID>"/>``. scripts/ scenario-report.py turns that into a scenario→status traceability matrix. """ +from pathlib import Path + import pytest -# The coverage meta-test verifies the catalog itself and is not a scenario. +from neutron_understack.tests.scenarios.catalog import SCENARIO_ID_RE +from neutron_understack.tests.scenarios.catalog import catalog_ids + +# The catalog meta-test is not itself a scenario. _EXEMPT_FILES = {"test_scenario_coverage.py"} +_SCENARIOS_DIR = Path(__file__).parent +_CATALOG = _SCENARIOS_DIR / "SCENARIOS.md" + + +def _scenario_id(item): + marker = item.get_closest_marker("scenario") + if marker is None: + return None, "missing an @pytest.mark.scenario('<ID>') marker" + if len(marker.args) != 1 or marker.kwargs: + return None, "scenario marker must have exactly one positional ID" + + scenario_id = marker.args[0] + if not isinstance(scenario_id, str) or not SCENARIO_ID_RE.fullmatch(scenario_id): + return None, f"invalid scenario ID {scenario_id!r}" + return scenario_id, None -def pytest_collection_modifyitems(items): - missing = [] +def pytest_collection_modifyitems(config, items): + errors = [] + collected_ids = set() + collected_files = set() for item in items: - path = str(getattr(item, "fspath", "")).replace("\\", "/") + item_path = Path(str(getattr(item, "fspath", ""))).resolve() + path = item_path.as_posix() if "/tests/scenarios/" not in path: continue - if path.rsplit("/", 1)[-1] in _EXEMPT_FILES: + if item_path.name in _EXEMPT_FILES: continue - marker = item.get_closest_marker("scenario") - if marker is None: - missing.append(item.nodeid) + + collected_files.add(item_path) + scenario_id, error = _scenario_id(item) + if error: + errors.append(f"{item.nodeid}: {error}") continue - if marker.args: - item.user_properties.append(("scenario", marker.args[0])) - if missing: + collected_ids.add(scenario_id) + item.user_properties.append(("scenario", scenario_id)) + + catalog = catalog_ids(_CATALOG.read_text()) + unknown = sorted(collected_ids - set(catalog)) + if unknown: + errors.append(f"scenario IDs missing from SCENARIOS.md: {unknown}") + + scenario_files = { + scenario_path.resolve() + for scenario_path in _SCENARIOS_DIR.glob("test_*.py") + if scenario_path.name not in _EXEMPT_FILES + } + invocation_dir = Path(config.invocation_params.dir) + requested_paths = { + (invocation_dir / str(arg).split("::", 1)[0]).resolve() + for arg in config.args + if not str(arg).startswith("-") + } + full_scenario_scope = any( + _SCENARIOS_DIR.resolve().is_relative_to(requested_path) + for requested_path in requested_paths + ) + if full_scenario_scope or (scenario_files and scenario_files <= collected_files): + untested = sorted(set(catalog) - collected_ids) + if untested: + errors.append(f"catalogued scenarios with no collected test: {untested}") + + if errors: raise pytest.UsageError( - "scenario tests missing an @pytest.mark.scenario('<ID>') marker:\n " - + "\n ".join(missing) + "scenario collection validation failed:\n " + "\n ".join(errors) ) diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/test_router_uplink.py b/python/neutron-understack/neutron_understack/tests/scenarios/test_router_uplink.py index 896ab1aeb..ede78ddb5 100644 --- a/python/neutron-understack/neutron_understack/tests/scenarios/test_router_uplink.py +++ b/python/neutron-understack/neutron_understack/tests/scenarios/test_router_uplink.py @@ -8,6 +8,9 @@ import pytest from neutron.common.ovn import utils as ovn_utils +from neutron.db import segments_db +from neutron.objects.trunk import SubPort +from neutron_lib import constants as p_const from neutron_understack.tests.scenarios.base import UnderstackMl2RouterOvnScenarioBase from neutron_understack.tests.scenarios.fakes import FakeOvnClient @@ -60,6 +63,16 @@ def _shared_uplink_ports(self, net_id): ) return [p for p in ports if (p.get("name") or "").startswith("uplink-")] + def _trunk_subports(self, trunk_id): + return [ + { + "port_id": subport.port_id, + "segmentation_type": subport.segmentation_type, + "segmentation_id": subport.segmentation_id, + } + for subport in SubPort.get_objects(self.context, trunk_id=trunk_id) + ] + @pytest.mark.scenario("RTR-ATTACH-01") def test_nonflavored_router_attach_builds_uplink(self): nn_trunk_id = self._make_network_node_trunk() @@ -72,13 +85,29 @@ def test_nonflavored_router_attach_builds_uplink(self): self.context, router["id"], {"subnet_id": subnet_id} ) - # An OVN localnet uplink port was created on this network's switch. + shared_ports = self._shared_uplink_ports(net_id) + assert len(shared_ports) == 1 + shared_port = shared_ports[0] + segment_id = shared_port["name"].removeprefix("uplink-") + segment = segments_db.get_segment_by_id(self.context, segment_id) + assert segment[segments_db.NETWORK_TYPE] == p_const.TYPE_VLAN + assert segment[segments_db.PHYSICAL_NETWORK] == self.NETWORK_NODE_PHYSNET + + assert self._trunk_subports(nn_trunk_id) == [ + { + "port_id": shared_port["id"], + "segmentation_type": p_const.TYPE_VLAN, + "segmentation_id": segment[segments_db.SEGMENTATION_ID], + } + ] + assert len(fake._nb_idl.created_ports) == 1, fake._nb_idl.created_ports created = fake._nb_idl.created_ports[0] assert created["lswitch_name"] == ovn_utils.ovn_name(net_id) - assert created["lport_name"].startswith("uplink-") - # A shared "uplink-" neutron port was created on the network. - assert self._shared_uplink_ports(net_id) + assert created["lport_name"] == shared_port["name"] + assert created["type"] == "localnet" + assert created["tag"] == segment[segments_db.SEGMENTATION_ID] + assert created["options"]["network_name"] == self.NETWORK_NODE_PHYSNET @pytest.mark.scenario("RTR-SECOND-01") def test_second_router_on_network_is_noop(self): @@ -115,14 +144,36 @@ def test_remove_router_interface_tears_down_uplink(self): fake = FakeOvnClient() router = self._create_router() - with self._ovn(fake, nn_trunk_id): + with ( + self._ovn(fake, nn_trunk_id), + mock.patch.object( + self.trunk_plugin, + "remove_subports", + wraps=self.trunk_plugin.remove_subports, + ) as remove_subports, + ): self.l3_plugin.add_router_interface( self.context, router["id"], {"subnet_id": subnet_id} ) + shared_port = self._shared_uplink_ports(net_id)[0] + segment_id = shared_port["name"].removeprefix("uplink-") self.l3_plugin.remove_router_interface( self.context, router["id"], {"subnet_id": subnet_id} ) - # The OVN uplink LSPs were deleted and the shared port removed. - assert fake._nb_idl.deleted_ports, fake._nb_idl.deleted_ports + remove_subports.assert_called_once_with( + context=mock.ANY, + trunk_id=nn_trunk_id, + subports={"sub_ports": [{"port_id": shared_port["id"]}]}, + ) assert self._shared_uplink_ports(net_id) == [] + assert fake._nb_idl.deleted_ports == [ + { + "lport_name": f"uplink-{segment_id}", + "lswitch_name": ovn_utils.ovn_name(net_id), + }, + { + "lport_name": shared_port["id"], + "lswitch_name": ovn_utils.ovn_name(net_id), + }, + ] diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/test_scenario_coverage.py b/python/neutron-understack/neutron_understack/tests/scenarios/test_scenario_coverage.py index 9b37ee916..076c83dff 100644 --- a/python/neutron-understack/neutron_understack/tests/scenarios/test_scenario_coverage.py +++ b/python/neutron-understack/neutron_understack/tests/scenarios/test_scenario_coverage.py @@ -1,57 +1,54 @@ -"""Enforce that the scenario catalog and the tests agree, in both directions. +"""Validate properties of the human-readable scenario catalog. -- Every ``### <ID> — ...`` entry in SCENARIOS.md must be implemented by at least - one test tagged ``@pytest.mark.scenario("<ID>")``. -- Every such marker must reference an ID that exists in SCENARIOS.md. - -This is a static cross-check (it scans the catalog and the test sources), so it -holds regardless of which subset of tests a given run collects. +The collection hook validates marker shape and checks the catalog against the +tests pytest actually collects. That avoids source scanning that can count a +marker in a comment or dead code as an implemented scenario. """ -import re from pathlib import Path +from types import SimpleNamespace + +import pytest from neutron_understack.tests.scenarios.catalog import catalog_ids +from neutron_understack.tests.scenarios.conftest import _scenario_id SCENARIOS_DIR = Path(__file__).parent CATALOG = SCENARIOS_DIR / "SCENARIOS.md" -# Markers in test sources: @pytest.mark.scenario("BM-BIND-01"). -_MARKER_ID_RE = re.compile(r"""\.scenario\(\s*["']([^"']+)["']""") - def _catalog_ids(): return catalog_ids(CATALOG.read_text()) -def _marker_ids(): - ids = set() - for path in SCENARIOS_DIR.glob("test_*.py"): - if path.name == Path(__file__).name: - continue - ids.update(_MARKER_ID_RE.findall(path.read_text())) - return ids - - def test_catalog_has_no_duplicate_ids(): ids = _catalog_ids() duplicates = sorted({i for i in ids if ids.count(i) > 1}) assert not duplicates, f"duplicate scenario IDs in SCENARIOS.md: {duplicates}" -def test_every_catalogued_scenario_has_a_test(): - catalog = set(_catalog_ids()) - markers = _marker_ids() - untested = sorted(catalog - markers) - assert ( - not untested - ), f"catalogued scenarios with no @pytest.mark.scenario test: {untested}" +def _item_with_marker(marker): + return SimpleNamespace(get_closest_marker=lambda _name: marker) + + +def test_valid_scenario_marker_returns_id(): + item = _item_with_marker(pytest.mark.scenario("BM-BIND-01").mark) + assert _scenario_id(item) == ("BM-BIND-01", None) -def test_every_scenario_marker_is_catalogued(): - catalog = set(_catalog_ids()) - markers = _marker_ids() - uncataloged = sorted(markers - catalog) - assert ( - not uncataloged - ), f"@pytest.mark.scenario IDs missing from SCENARIOS.md: {uncataloged}" +@pytest.mark.parametrize( + "marker", + [ + None, + pytest.mark.scenario().mark, + pytest.mark.scenario("BM-BIND-01", "extra").mark, + pytest.mark.scenario(id="BM-BIND-01").mark, + pytest.mark.scenario(123).mark, + pytest.mark.scenario("").mark, + pytest.mark.scenario("bm-bind-01").mark, + ], +) +def test_malformed_scenario_marker_is_rejected(marker): + scenario_id, error = _scenario_id(_item_with_marker(marker)) + assert scenario_id is None + assert error diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/test_trunk_operations.py b/python/neutron-understack/neutron_understack/tests/scenarios/test_trunk_operations.py index dd58c69e6..48f15673d 100644 --- a/python/neutron-understack/neutron_understack/tests/scenarios/test_trunk_operations.py +++ b/python/neutron-understack/neutron_understack/tests/scenarios/test_trunk_operations.py @@ -7,6 +7,11 @@ from unittest import mock import pytest +from neutron.db import segments_db +from neutron.objects.network import NetworkSegment +from neutron.plugins.ml2 import db as ml2_db +from neutron.services.trunk import exceptions as trunk_exc +from neutron_lib import constants as p_const from neutron_lib.callbacks import exceptions as cb_exc from neutron_understack.tests.scenarios.base import DEFAULT_PHYSNET @@ -65,10 +70,38 @@ def _remove_subport(self, trunk_id, subport_id): "neutron_understack.utils.fetch_network_node_trunk_id", return_value=_FAKE_NN_TRUNK, ): - self.trunk_plugin.remove_subports( + return self.trunk_plugin.remove_subports( self.context, trunk_id, {"sub_ports": [{"port_id": subport_id}]} ) + def _trunk_subports(self, trunk_id): + return self.trunk_plugin.get_trunk(self.context, trunk_id)["sub_ports"] + + def _assert_subport_bound(self, trunk_id, subport_id, host, seg_id=SUBPORT_VLAN): + assert { + subport["port_id"]: subport for subport in self._trunk_subports(trunk_id) + }[subport_id] == { + "port_id": subport_id, + "segmentation_type": "vlan", + "segmentation_id": seg_id, + } + + levels = ml2_db.get_binding_level_objs(self.context, subport_id, host) + assert len(levels) == 1, levels + assert levels[0].driver == "understack" + assert levels[0].level == 0 + + segment = segments_db.get_segment_by_id(self.context, levels[0].segment_id) + assert segment[segments_db.NETWORK_TYPE] == p_const.TYPE_VLAN + assert segment[segments_db.PHYSICAL_NETWORK] == DEFAULT_PHYSNET + segment_obj = NetworkSegment.get_object(self.context, id=levels[0].segment_id) + assert segment_obj.is_dynamic + return levels[0].segment_id + + def _assert_subport_unbound(self, subport_id, host, segment_id): + assert not ml2_db.get_binding_level_objs(self.context, subport_id, host) + assert segments_db.get_segment_by_id(self.context, segment_id) is None + @pytest.mark.scenario("TRUNK-SUB-ADD") def test_subport_add_syncs_parent_physnet(self): parent_net = self._make_network(self.fmt, "parent-net", True)["network"]["id"] @@ -80,7 +113,7 @@ def test_subport_add_syncs_parent_physnet(self): self.undersync_mock.reset_mock() self._add_subport(trunk_id, subport_id) - # Attaching the subport reconciles the parent port's switch (VLAN group). + self._assert_subport_bound(trunk_id, subport_id, "host-a") self.undersync_mock.sync.assert_any_call(DEFAULT_PHYSNET) @pytest.mark.scenario("TRUNK-SUB-DEL") @@ -91,11 +124,13 @@ def test_subport_remove_syncs_parent_physnet(self): subport_id = self._plain_port(subport_net) trunk_id = self._make_trunk(parent_id) self._add_subport(trunk_id, subport_id) + segment_id = self._assert_subport_bound(trunk_id, subport_id, "host-a") self.undersync_mock.reset_mock() - self._remove_subport(trunk_id, subport_id) + updated_trunk = self._remove_subport(trunk_id, subport_id) - # Removing the subport reconciles the parent port's switch (VLAN group). + assert updated_trunk["sub_ports"] == [] + self._assert_subport_unbound(subport_id, "host-a", segment_id) self.undersync_mock.sync.assert_any_call(DEFAULT_PHYSNET) @pytest.mark.scenario("TRUNK-PARENT-NOIP") @@ -113,7 +148,7 @@ def test_subport_add_syncs_when_parent_has_no_ip(self): self.undersync_mock.reset_mock() self._add_subport(trunk_id, subport_id) - # The parent having no IP does not suppress the switch reconcile. + self._assert_subport_bound(trunk_id, subport_id, "host-a") self.undersync_mock.sync.assert_any_call(DEFAULT_PHYSNET) @pytest.mark.scenario("TRUNK-DEL-01") @@ -124,6 +159,7 @@ def test_trunk_delete_syncs_parent_physnet(self): subport_id = self._plain_port(subport_net) trunk_id = self._make_trunk(parent_id) self._add_subport(trunk_id, subport_id) + segment_id = self._assert_subport_bound(trunk_id, subport_id, "host-a") self.undersync_mock.reset_mock() with mock.patch( @@ -132,6 +168,9 @@ def test_trunk_delete_syncs_parent_physnet(self): ): self.trunk_plugin.delete_trunk(self.context, trunk_id) + with pytest.raises(trunk_exc.TrunkNotFound): + self.trunk_plugin.get_trunk(self.context, trunk_id) + self._assert_subport_unbound(subport_id, "host-a", segment_id) self.undersync_mock.sync.assert_any_call(DEFAULT_PHYSNET) @pytest.mark.scenario("TRUNK-PARENT-UNBOUND-01") @@ -146,6 +185,14 @@ def test_subport_add_unbound_parent_no_sync(self): self.undersync_mock.reset_mock() self._add_subport(trunk_id, subport_id) + assert self._trunk_subports(trunk_id)[0]["port_id"] == subport_id + assert not ml2_db.get_binding_level_objs(self.context, subport_id, "") + assert ( + segments_db.get_dynamic_segment( + self.context, subport_net, physical_network=DEFAULT_PHYSNET + ) + is None + ) self.undersync_mock.sync.assert_not_called() @pytest.mark.scenario("TRUNK-MULTI-01") @@ -182,6 +229,14 @@ def test_multiple_subports_add_syncs(self): }, ) + self._assert_subport_bound(trunk_id, subport_a, "host-a", seg_id=1500) + segment_b = self._assert_subport_bound( + trunk_id, subport_b, "host-a", seg_id=1600 + ) + segment_a = ml2_db.get_binding_level_objs(self.context, subport_a, "host-a")[ + 0 + ].segment_id + assert segment_a != segment_b self.undersync_mock.sync.assert_any_call(DEFAULT_PHYSNET) @pytest.mark.scenario("TRUNK-SEGID-RANGE-01") @@ -198,3 +253,10 @@ def test_subport_segid_out_of_range_rejected(self): with pytest.raises(cb_exc.CallbackFailure) as exc_info: self._add_subport(trunk_id, subport_id, seg_id=4000) assert "Segmentation ID" in str(exc_info.value) + assert not ml2_db.get_binding_level_objs(self.context, subport_id, "host-a") + assert ( + segments_db.get_dynamic_segment( + self.context, subport_net, physical_network=DEFAULT_PHYSNET + ) + is None + ) From 1a74a7738a7981ccc5d3c460137366b3c5c7ac3f Mon Sep 17 00:00:00 2001 From: Doug Goldstein <doug.goldstein@rackspace.com> Date: Tue, 25 Aug 2026 08:48:44 -0500 Subject: [PATCH 17/21] test(neutron-understack): dedupe _trunk_subports onto the shared base Both the trunk and router-OVN scenarios defined their own _trunk_subports helper (one via get_trunk, one via SubPort.get_objects). Move a single get_trunk-based helper onto the shared mixin and drop the duplicates (and the now-unused SubPort import). --- .../neutron_understack/tests/scenarios/base.py | 7 +++++++ .../tests/scenarios/test_router_uplink.py | 11 ----------- .../tests/scenarios/test_trunk_operations.py | 3 --- 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/base.py b/python/neutron-understack/neutron_understack/tests/scenarios/base.py index 08b34f33e..73f5af325 100644 --- a/python/neutron-understack/neutron_understack/tests/scenarios/base.py +++ b/python/neutron-understack/neutron_understack/tests/scenarios/base.py @@ -143,6 +143,13 @@ def _bind_baremetal_port(self, net_id, physnet, host, fixed_ips=None): assert req.get_response(self.api).status_int == 200 return port_id + def _trunk_subports(self, trunk_id): + """Return a trunk's subports as the trunk plugin reports them. + + Requires ``self.trunk_plugin`` (set by the trunk/router-OVN bases). + """ + return self.trunk_plugin.get_trunk(self.context, trunk_id)["sub_ports"] + class UnderstackMl2ScenarioBase(_UnderstackMl2ScenarioMixin, Ml2PluginV2TestCase): """Base for port-binding scenarios (no L3/router machinery).""" diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/test_router_uplink.py b/python/neutron-understack/neutron_understack/tests/scenarios/test_router_uplink.py index ede78ddb5..d2cd5782f 100644 --- a/python/neutron-understack/neutron_understack/tests/scenarios/test_router_uplink.py +++ b/python/neutron-understack/neutron_understack/tests/scenarios/test_router_uplink.py @@ -9,7 +9,6 @@ import pytest from neutron.common.ovn import utils as ovn_utils from neutron.db import segments_db -from neutron.objects.trunk import SubPort from neutron_lib import constants as p_const from neutron_understack.tests.scenarios.base import UnderstackMl2RouterOvnScenarioBase @@ -63,16 +62,6 @@ def _shared_uplink_ports(self, net_id): ) return [p for p in ports if (p.get("name") or "").startswith("uplink-")] - def _trunk_subports(self, trunk_id): - return [ - { - "port_id": subport.port_id, - "segmentation_type": subport.segmentation_type, - "segmentation_id": subport.segmentation_id, - } - for subport in SubPort.get_objects(self.context, trunk_id=trunk_id) - ] - @pytest.mark.scenario("RTR-ATTACH-01") def test_nonflavored_router_attach_builds_uplink(self): nn_trunk_id = self._make_network_node_trunk() diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/test_trunk_operations.py b/python/neutron-understack/neutron_understack/tests/scenarios/test_trunk_operations.py index 48f15673d..82908fe00 100644 --- a/python/neutron-understack/neutron_understack/tests/scenarios/test_trunk_operations.py +++ b/python/neutron-understack/neutron_understack/tests/scenarios/test_trunk_operations.py @@ -74,9 +74,6 @@ def _remove_subport(self, trunk_id, subport_id): self.context, trunk_id, {"sub_ports": [{"port_id": subport_id}]} ) - def _trunk_subports(self, trunk_id): - return self.trunk_plugin.get_trunk(self.context, trunk_id)["sub_ports"] - def _assert_subport_bound(self, trunk_id, subport_id, host, seg_id=SUBPORT_VLAN): assert { subport["port_id"]: subport for subport in self._trunk_subports(trunk_id) From 2fc2c82f881c9555cce53f6d1eab68404153d634 Mon Sep 17 00:00:00 2001 From: Doug Goldstein <doug.goldstein@rackspace.com> Date: Tue, 25 Aug 2026 09:04:56 -0500 Subject: [PATCH 18/21] test(neutron-understack): rename router scenarios to <TYPE>-ROUTER-<OP> Prefix the non-flavored router scenarios with OVN (they exercise the default OVN L3 provider) and standardize the router family as <TYPE>-ROUTER-<OP>: - RTR-{ATTACH,SECOND,DETACH}-01 -> OVN-ROUTER-{ATTACH,SECOND,DETACH}-01 - VRF-RTR-01 -> VRF-ROUTER-ATTACH-01 - VRF-DETACH-01 -> VRF-ROUTER-DETACH-01 - SVI-RTR-01 -> SVI-ROUTER-ATTACH-01 - PALO-{ADOPT,RELEASE}-01 -> PALO-ROUTER-{ADOPT,RELEASE}-01 SVI-VAL-* and the backlog OVN-ROUTER-* entries updated to match. --- .../tests/scenarios/SCENARIOS.md | 24 +++++++++---------- .../tests/scenarios/test_palo_alto_router.py | 4 ++-- .../tests/scenarios/test_router_uplink.py | 8 +++---- .../scenarios/test_svi_router_interface.py | 6 ++--- .../scenarios/test_vrf_router_interface.py | 6 ++--- 5 files changed, 24 insertions(+), 24 deletions(-) diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md b/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md index 329fddff5..caa814b5f 100644 --- a/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md +++ b/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md @@ -154,7 +154,7 @@ VRF (flavored) router path skips the OVN uplink work, so no OVN IDL fake is needed. The flavored path is simulated by patching `routers._router_has_flavor`, mirroring the existing unit tests. -### VRF-RTR-01 — VRF router attach syncs bound baremetal port physnets +### VRF-ROUTER-ATTACH-01 — VRF router attach syncs bound baremetal port physnets - given: a VXLAN network + subnet with baremetal ports bound to two different physnets (`physnet1`, `physnet2`) - when: a VRF router is created and the subnet is attached on the internal side @@ -165,22 +165,22 @@ mirroring the existing unit tests. called for them). The test asserts the desired behavior and is `xfail(strict=True)`. -### VRF-DETACH-01 — VRF router detach syncs bound baremetal port physnets +### VRF-ROUTER-DETACH-01 — VRF router detach syncs bound baremetal port physnets - given: a VXLAN network with baremetal ports on two physnets and an attached VRF router interface - when: the router interface is removed - then: undersync should sync each physnet still carrying baremetal ports - status: **KNOWN BUG (xfail, rackerlabs/understack#2240)** — the teardown - counterpart of VRF-RTR-01; detach does not sync those physnets today. + counterpart of VRF-ROUTER-ATTACH-01; detach does not sync those physnets today. -### SVI-RTR-01 — SVI router attach syncs bound baremetal port physnets +### SVI-ROUTER-ATTACH-01 — SVI router attach syncs bound baremetal port physnets - given: a VXLAN network + an address-scoped IPv4 subnet, with baremetal ports bound to two different physnets (`physnet1`, `physnet2`) - when: an SVI router is created and the subnet is attached on the internal side - then: undersync syncs each physnet carrying the network's baremetal ports so the switches are reconciled for the new router - status: **KNOWN BUG (xfail, rackerlabs/understack#2240)** — the same gap as - VRF-RTR-01 for the SVI flavor: attaching the SVI router interface does not + VRF-ROUTER-ATTACH-01 for the SVI flavor: attaching the SVI router interface does not sync those physnets today. The SVI flavor is simulated by patching `_router_has_flavor` and `svi._is_svi_router`; the subnet is address-scoped so the SVI precommit scope validation passes. @@ -209,19 +209,19 @@ These load an L3 router + flavors + trunk plugin HCG workaround). `utils.fetch_network_node_trunk_id` is mocked to a trunk the test creates. -### RTR-ATTACH-01 — non-flavored router attach builds the uplink +### OVN-ROUTER-ATTACH-01 — non-flavored router attach builds the uplink - given: a network+subnet, a network-node trunk, and a non-flavored router - when: the subnet is attached on the internal side - then: a dynamic VLAN uplink segment is allocated on the network-node physnet; the shared `uplink-` neutron port, trunk subport tag, and OVN localnet LSP tag all reference that segment and VLAN on the network's logical switch -### RTR-SECOND-01 — second router on the same network is a no-op +### OVN-ROUTER-SECOND-01 — second router on the same network is a no-op - given: a network with two subnets, the first already attached to a router - when: a second router attaches the second subnet - then: no new uplink is built (`is_only_router_port_on_network` is false) -### RTR-DETACH-01 — remove_router_interface tears down the uplink +### OVN-ROUTER-DETACH-01 — remove_router_interface tears down the uplink - given: a network with a router interface and its uplink - when: the interface is removed - then: the shared port is removed from the network-node trunk, both the @@ -234,13 +234,13 @@ These register the Palo Alto provider as an L3 service provider and create a rea flavor + service profile (`driver` = the PaloAlto class, `metainfo.resource_class` = the netdev pool). `IronicClient` is faked (single-node pool). -### PALO-ADOPT-01 — Palo Alto router adopts an Ironic netdev node +### PALO-ROUTER-ADOPT-01 — Palo Alto router adopts an Ironic netdev node - given: the Palo Alto provider registered and a matching flavor - when: a router with that flavor is created - then: the ROUTER BEFORE_CREATE callback adopts the available netdev node for the router (via the faked Ironic client) -### PALO-RELEASE-01 — deleting a Palo Alto router releases its node +### PALO-ROUTER-RELEASE-01 — deleting a Palo Alto router releases its node - given: a Palo Alto router that adopted a node - when: the router is deleted - then: the ROUTER AFTER_DELETE callback returns the node to the pool @@ -285,11 +285,11 @@ SVI validation: which the patched-flavor scenarios do not load. Router (needs an OVN IDL fake for `routers.ovn_client()`): -- RTR-DELETE-01 — `delete_router` cleanup via `handle_router_interface_removal` +- OVN-ROUTER-DELETE-01 — `delete_router` cleanup via `handle_router_interface_removal` (PORT PRECOMMIT_DELETE). Not reachable with the standard L3 plugin, which raises RouterInUse unless interfaces are removed first; needs a trigger that deletes a router-interface port directly. -- RTR-HCG-VXLAN-01 — `link_vxlan_network_ha_chassis_group` populates the unified +- OVN-ROUTER-HCG-VXLAN-01 — `link_vxlan_network_ha_chassis_group` populates the unified HCG for a vxlan external gateway (needs a deeper OVN NB/SB fake). VNI: diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/test_palo_alto_router.py b/python/neutron-understack/neutron_understack/tests/scenarios/test_palo_alto_router.py index 78acfc20a..0ac755828 100644 --- a/python/neutron-understack/neutron_understack/tests/scenarios/test_palo_alto_router.py +++ b/python/neutron-understack/neutron_understack/tests/scenarios/test_palo_alto_router.py @@ -58,7 +58,7 @@ def _palo_flavor(self, resource_class="netdev-fw"): ) return flavor["id"] - @pytest.mark.scenario("PALO-ADOPT-01") + @pytest.mark.scenario("PALO-ROUTER-ADOPT-01") def test_palo_alto_router_adopts_ironic_node(self): fake_ironic = FakeIronicClient() flavor_id = self._palo_flavor() @@ -83,7 +83,7 @@ def test_palo_alto_router_adopts_ironic_node(self): assert len(fake_ironic.adopted) == 1, fake_ironic.adopted assert fake_ironic.adopted[0]["router_id"] == router["id"] - @pytest.mark.scenario("PALO-RELEASE-01") + @pytest.mark.scenario("PALO-ROUTER-RELEASE-01") def test_palo_alto_router_delete_releases_ironic_node(self): fake_ironic = FakeIronicClient() flavor_id = self._palo_flavor() diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/test_router_uplink.py b/python/neutron-understack/neutron_understack/tests/scenarios/test_router_uplink.py index d2cd5782f..be2ca06b2 100644 --- a/python/neutron-understack/neutron_understack/tests/scenarios/test_router_uplink.py +++ b/python/neutron-understack/neutron_understack/tests/scenarios/test_router_uplink.py @@ -1,6 +1,6 @@ """Scenario tests for the non-flavored router uplink path (OVN + trunk). -See neutron_understack/tests/scenarios/SCENARIOS.md (RTR-*) for the catalog. +See neutron_understack/tests/scenarios/SCENARIOS.md (OVN-ROUTER-*) for the catalog. """ import contextlib @@ -62,7 +62,7 @@ def _shared_uplink_ports(self, net_id): ) return [p for p in ports if (p.get("name") or "").startswith("uplink-")] - @pytest.mark.scenario("RTR-ATTACH-01") + @pytest.mark.scenario("OVN-ROUTER-ATTACH-01") def test_nonflavored_router_attach_builds_uplink(self): nn_trunk_id = self._make_network_node_trunk() net_id, subnet_id = self._router_net_with_subnet() @@ -98,7 +98,7 @@ def test_nonflavored_router_attach_builds_uplink(self): assert created["tag"] == segment[segments_db.SEGMENTATION_ID] assert created["options"]["network_name"] == self.NETWORK_NODE_PHYSNET - @pytest.mark.scenario("RTR-SECOND-01") + @pytest.mark.scenario("OVN-ROUTER-SECOND-01") def test_second_router_on_network_is_noop(self): nn_trunk_id = self._make_network_node_trunk() # One network, two subnets (routers can't share a subnet's gateway IP). @@ -126,7 +126,7 @@ def test_second_router_on_network_is_noop(self): assert len(fake._nb_idl.created_ports) == 1, fake._nb_idl.created_ports assert len(self._shared_uplink_ports(net_id)) == 1 - @pytest.mark.scenario("RTR-DETACH-01") + @pytest.mark.scenario("OVN-ROUTER-DETACH-01") def test_remove_router_interface_tears_down_uplink(self): nn_trunk_id = self._make_network_node_trunk() net_id, subnet_id = self._router_net_with_subnet() diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/test_svi_router_interface.py b/python/neutron-understack/neutron_understack/tests/scenarios/test_svi_router_interface.py index d677bd86a..2800b55a0 100644 --- a/python/neutron-understack/neutron_understack/tests/scenarios/test_svi_router_interface.py +++ b/python/neutron-understack/neutron_understack/tests/scenarios/test_svi_router_interface.py @@ -1,7 +1,7 @@ """Scenario tests for SVI router interface attach vs. bound baremetal ports. -Extends the VRF scenario (VRF-RTR-01) to the SVI router flavor. See -neutron_understack/tests/scenarios/SCENARIOS.md (SVI-RTR-*) for the catalog. +Extends the VRF scenario (VRF-ROUTER-ATTACH-01) to the SVI router flavor. See +neutron_understack/tests/scenarios/SCENARIOS.md (SVI-*) for the catalog. """ import contextlib @@ -126,7 +126,7 @@ def _scoped_ipv6_subnet(self, network): "physnets of baremetal ports already bound on the network" ), ) - @pytest.mark.scenario("SVI-RTR-01") + @pytest.mark.scenario("SVI-ROUTER-ATTACH-01") def test_svi_router_interface_syncs_bound_port_physnets(self): net = self._make_network(self.fmt, "vxlan-net", True) net_id = net["network"]["id"] diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/test_vrf_router_interface.py b/python/neutron-understack/neutron_understack/tests/scenarios/test_vrf_router_interface.py index bc14d2471..6a9472407 100644 --- a/python/neutron-understack/neutron_understack/tests/scenarios/test_vrf_router_interface.py +++ b/python/neutron-understack/neutron_understack/tests/scenarios/test_vrf_router_interface.py @@ -1,6 +1,6 @@ """Scenario tests for VRF router interface attach vs. bound baremetal ports. -See neutron_understack/tests/scenarios/SCENARIOS.md (VRF-RTR-*) for the catalog. +See neutron_understack/tests/scenarios/SCENARIOS.md (VRF-ROUTER-*) for the catalog. """ from unittest import mock @@ -25,7 +25,7 @@ class TestVrfRouterInterface(UnderstackMl2RouterScenarioBase): "physnets of baremetal ports already bound on the network" ), ) - @pytest.mark.scenario("VRF-RTR-01") + @pytest.mark.scenario("VRF-ROUTER-ATTACH-01") def test_vrf_router_interface_syncs_bound_port_physnets(self): net = self._make_network(self.fmt, "vxlan-net", True) net_id = net["network"]["id"] @@ -70,7 +70,7 @@ def test_vrf_router_interface_syncs_bound_port_physnets(self): "physnets of baremetal ports still bound on the network" ), ) - @pytest.mark.scenario("VRF-DETACH-01") + @pytest.mark.scenario("VRF-ROUTER-DETACH-01") def test_vrf_router_interface_detach_syncs_bound_port_physnets(self): net = self._make_network(self.fmt, "vxlan-net", True) net_id = net["network"]["id"] From 91b84e638074c211dbb7154caf3191fbe154ab3e Mon Sep 17 00:00:00 2001 From: Doug Goldstein <doug.goldstein@rackspace.com> Date: Tue, 25 Aug 2026 09:06:45 -0500 Subject: [PATCH 19/21] docs(neutron-understack): widen router-flavor section to VRF & SVI The "Router interface (VRF flavor)" catalog section holds both the VRF and SVI flavored router scenarios; retitle it and note the SVI detection patch so the heading matches its contents. --- .../neutron_understack/tests/scenarios/SCENARIOS.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md b/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md index caa814b5f..99e367198 100644 --- a/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md +++ b/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md @@ -147,12 +147,13 @@ adds/removes must reconcile the parent's switch (VLAN group). callback, surfaced as `CallbackFailure`), with no binding level or dynamic segment allocated -## Router interface (VRF flavor) +## Router interface (VRF & SVI flavors) These scenarios load a real L3 router + flavors plugin (`ML2TestFramework`). The -VRF (flavored) router path skips the OVN uplink work, so no OVN IDL fake is -needed. The flavored path is simulated by patching `routers._router_has_flavor`, -mirroring the existing unit tests. +VRF and SVI flavored router paths skip the OVN uplink work, so no OVN IDL fake is +needed. The flavored path is simulated by patching `routers._router_has_flavor` +(and `svi._is_svi_router` for the SVI scenarios), mirroring the existing unit +tests. ### VRF-ROUTER-ATTACH-01 — VRF router attach syncs bound baremetal port physnets - given: a VXLAN network + subnet with baremetal ports bound to two different From 2232ef24f65cf5fac61f69351f78a14c6c84f0f1 Mon Sep 17 00:00:00 2001 From: Doug Goldstein <doug.goldstein@rackspace.com> Date: Tue, 25 Aug 2026 09:08:11 -0500 Subject: [PATCH 20/21] docs(neutron-understack): explain VRF vs SVI flavor relationship Both flavors realize the router as an SVI on the switching fabric; the only difference is which VRF the SVI lands in. The VRF flavor creates a dedicated VRF and places the SVI into it; the SVI flavor drops the SVI into a well-known VRF from the base switch config. Note this in the catalog so the two near-mirror scenarios read as intended. --- .../neutron_understack/tests/scenarios/SCENARIOS.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md b/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md index 99e367198..a22f437a2 100644 --- a/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md +++ b/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md @@ -155,6 +155,13 @@ needed. The flavored path is simulated by patching `routers._router_has_flavor` (and `svi._is_svi_router` for the SVI scenarios), mirroring the existing unit tests. +The VRF and SVI flavors are close cousins: both realize the router as an SVI +(switched virtual interface) on the switching fabric. The difference is only +which VRF the SVI lands in — the VRF flavor first creates a dedicated VRF and +places the SVI into it, while the SVI flavor drops the SVI into a well-known VRF +that is part of the base switch configuration. So their scenarios are near +mirror images of each other. + ### VRF-ROUTER-ATTACH-01 — VRF router attach syncs bound baremetal port physnets - given: a VXLAN network + subnet with baremetal ports bound to two different physnets (`physnet1`, `physnet2`) From 39f195956b9000ef13f3ffb87b3eabf5bd01f1b1 Mon Sep 17 00:00:00 2001 From: Doug Goldstein <doug.goldstein@rackspace.com> Date: Tue, 25 Aug 2026 10:45:43 -0500 Subject: [PATCH 21/21] test(neutron-understack): OVN router detach segment-release (xfail) Document the router-uplink segment leak (understack#2245) as an xfail on this test branch instead of fixing it here: OVN-ROUTER-DETACH-01 asserts the dynamic uplink segment is released on teardown and is marked xfail(strict=True). OVN-ROUTER-ATTACH-01 also now asserts the uplink segment is dynamic. The driver fix lives on a separate branch. --- .../tests/scenarios/SCENARIOS.md | 18 ++++++++++++------ .../tests/scenarios/test_router_uplink.py | 15 +++++++++++++++ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md b/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md index a22f437a2..efff39b52 100644 --- a/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md +++ b/python/neutron-understack/neutron_understack/tests/scenarios/SCENARIOS.md @@ -220,9 +220,9 @@ test creates. ### OVN-ROUTER-ATTACH-01 — non-flavored router attach builds the uplink - given: a network+subnet, a network-node trunk, and a non-flavored router - when: the subnet is attached on the internal side -- then: a dynamic VLAN uplink segment is allocated on the network-node physnet; - the shared `uplink-` neutron port, trunk subport tag, and OVN localnet LSP tag - all reference that segment and VLAN on the network's logical switch +- then: a VLAN uplink segment marked dynamic is allocated on the network-node + physnet; the shared `uplink-` neutron port, trunk subport tag, and OVN localnet + LSP tag all reference that segment and VLAN on the network's logical switch ### OVN-ROUTER-SECOND-01 — second router on the same network is a no-op - given: a network with two subnets, the first already attached to a router @@ -234,7 +234,12 @@ test creates. - when: the interface is removed - then: the shared port is removed from the network-node trunk, both the `uplink-<segment-id>` localnet LSP and shared-port LSP are deleted from the - exact logical switch, and the shared `uplink-` neutron port is removed + exact logical switch, the shared `uplink-` neutron port is removed, and the + dynamic segment is released +- status: **KNOWN BUG (xfail, rackerlabs/understack#2245)** — teardown deletes + the ports but leaks the dynamic uplink VLAN segment today. The test asserts + the desired release and is `xfail(strict=True)`; a separate fix branch + resolves it (flipping this to a pass once merged). ## Router flavor providers (Palo Alto) @@ -245,8 +250,9 @@ flavor + service profile (`driver` = the PaloAlto class, `metainfo.resource_clas ### PALO-ROUTER-ADOPT-01 — Palo Alto router adopts an Ironic netdev node - given: the Palo Alto provider registered and a matching flavor - when: a router with that flavor is created -- then: the ROUTER BEFORE_CREATE callback adopts the available netdev node for - the router (via the faked Ironic client) +- then: the ROUTER BEFORE_CREATE callback requests a node using the service + profile's resource class and adopts it with the router's project, ID, and name + (via the faked Ironic client) ### PALO-ROUTER-RELEASE-01 — deleting a Palo Alto router releases its node - given: a Palo Alto router that adopted a node diff --git a/python/neutron-understack/neutron_understack/tests/scenarios/test_router_uplink.py b/python/neutron-understack/neutron_understack/tests/scenarios/test_router_uplink.py index be2ca06b2..566c98022 100644 --- a/python/neutron-understack/neutron_understack/tests/scenarios/test_router_uplink.py +++ b/python/neutron-understack/neutron_understack/tests/scenarios/test_router_uplink.py @@ -9,6 +9,7 @@ import pytest from neutron.common.ovn import utils as ovn_utils from neutron.db import segments_db +from neutron.objects.network import NetworkSegment from neutron_lib import constants as p_const from neutron_understack.tests.scenarios.base import UnderstackMl2RouterOvnScenarioBase @@ -81,6 +82,8 @@ def test_nonflavored_router_attach_builds_uplink(self): segment = segments_db.get_segment_by_id(self.context, segment_id) assert segment[segments_db.NETWORK_TYPE] == p_const.TYPE_VLAN assert segment[segments_db.PHYSICAL_NETWORK] == self.NETWORK_NODE_PHYSNET + segment_obj = NetworkSegment.get_object(self.context, id=segment_id) + assert segment_obj.is_dynamic assert self._trunk_subports(nn_trunk_id) == [ { @@ -126,6 +129,16 @@ def test_second_router_on_network_is_noop(self): assert len(fake._nb_idl.created_ports) == 1, fake._nb_idl.created_ports assert len(self._shared_uplink_ports(net_id)) == 1 + # BUG: teardown deletes the OVN LSPs and shared port but leaks the dynamic + # uplink VLAN segment -- see SCENARIOS.md "Known bugs". Asserts the desired + # release; strict=True flips to a failure once the fix lands. + @pytest.mark.xfail( + strict=True, + reason=( + "understack#2245: router uplink teardown does not release the " + "dynamic VLAN segment on the network-node physnet" + ), + ) @pytest.mark.scenario("OVN-ROUTER-DETACH-01") def test_remove_router_interface_tears_down_uplink(self): nn_trunk_id = self._make_network_node_trunk() @@ -155,7 +168,9 @@ def test_remove_router_interface_tears_down_uplink(self): trunk_id=nn_trunk_id, subports={"sub_ports": [{"port_id": shared_port["id"]}]}, ) + assert self._trunk_subports(nn_trunk_id) == [] assert self._shared_uplink_ports(net_id) == [] + assert segments_db.get_segment_by_id(self.context, segment_id) is None assert fake._nb_idl.deleted_ports == [ { "lport_name": f"uplink-{segment_id}",