From a55fc938f02c6e45b678cb4d16c0c1e876d9020b Mon Sep 17 00:00:00 2001 From: IluaAir Date: Sat, 6 Jun 2026 11:06:03 +0300 Subject: [PATCH 01/16] Add optional dependencies for development and update .gitignore - Added `.DS_Store` to `.gitignore` to prevent macOS system files from being tracked. - Introduced optional development dependencies in `pyproject.toml`, including `pytest` and `responses`, to facilitate testing and development. - Updated `uv.lock` with new package dependencies and versions for improved compatibility and functionality. - Created new test files `conftest.py` and `test_view.py` to establish testing fixtures and implement unit tests for the `ModelView` class. --- .gitignore | 1 + pyproject.toml | 18 +++- tests/conftest.py | 8 ++ tests/test_view.py | 72 +++++++++++++++ uv.lock | 219 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 317 insertions(+), 1 deletion(-) create mode 100644 tests/conftest.py create mode 100644 tests/test_view.py diff --git a/.gitignore b/.gitignore index 964223f..ab9f627 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ main.py # Virtual environments .venv .idea +.DS_Store # etc files *.txt \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 066d1ab..75bb927 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,9 +20,19 @@ dependencies = [ "ttp>=0.10.0", ] +[project.optional-dependencies] +dev = [ + "pytest>=8", + "responses>=0.25", +] + [project.urls] Repository = "https://github.com/sttarsky/oxipy" +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["."] + [tool.setuptools.packages.find] where = ["."] include = ["oxi*"] @@ -31,4 +41,10 @@ include = ["oxi*"] include-package-data = true [tool.setuptools.package-data] -"oxi" = ["**/*.ttp"] \ No newline at end of file +"oxi" = ["**/*.ttp"] + +[dependency-groups] +dev = [ + "pytest>=9.0.3", + "responses>=0.26.1", +] diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..4c39a0a --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,8 @@ +from pathlib import Path + +FIXTURES = Path(__file__).parent / "fixtures" + + +def load(device: str, name: str = "config.conf") -> str: + """Read a device config fixture from tests/fixtures//.""" + return (FIXTURES / device / name).read_text(encoding="utf-8") diff --git a/tests/test_view.py b/tests/test_view.py new file mode 100644 index 0000000..ea38ac8 --- /dev/null +++ b/tests/test_view.py @@ -0,0 +1,72 @@ +import json + +import pytest + +from oxi.conf import ModelView +from oxi.interfaces.contract import Interfaces, System + + +@pytest.fixture +def system_view(): + system = System(model="RB951", serial_number="ABC123", version="7.12") + return ModelView(system) + + +@pytest.fixture +def interfaces_view(): + items = [ + Interfaces(interface="eth0", ip_address="192.168.1.1", mask=24), + Interfaces(interface="eth1", description="uplink"), + ] + return ModelView(items) + + +class TestSingleModelView: + def test_attribute_proxy(self, system_view): + assert system_view.model == "RB951" + assert system_view.serial_number == "ABC123" + + def test_dump(self, system_view): + assert system_view.dump() == { + "model": "RB951", + "serial_number": "ABC123", + "version": "7.12", + } + + def test_dump_json(self, system_view): + assert json.loads(system_view.dump_json())["model"] == "RB951" + + def test_iter_raises(self, system_view): + with pytest.raises(TypeError): + iter(system_view) + + def test_len_raises(self, system_view): + with pytest.raises(TypeError): + len(system_view) + + def test_getitem_raises(self, system_view): + with pytest.raises(TypeError): + system_view[0] + + +class TestListModelView: + def test_len(self, interfaces_view): + assert len(interfaces_view) == 2 + + def test_iter(self, interfaces_view): + names = [iface.name for iface in interfaces_view] + assert names == ["eth0", "eth1"] + + def test_getitem(self, interfaces_view): + assert interfaces_view[0].name == "eth0" + + def test_slice(self, interfaces_view): + assert len(interfaces_view[:1]) == 1 + + def test_dump_uses_aliases(self, interfaces_view): + dumped = interfaces_view.dump() + assert dumped[0]["interface"] == "eth0" + + def test_dump_json_keeps_unicode(self): + view = ModelView([Interfaces(interface="eth0", description="Дом")]) + assert "Дом" in view.dump_json() diff --git a/uv.lock b/uv.lock index c3bf3aa..36a43d8 100644 --- a/uv.lock +++ b/uv.lock @@ -109,6 +109,27 @@ wheels = [ { 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" }, ] +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + [[package]] name = "idna" version = "3.11" @@ -118,6 +139,15 @@ 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" }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "oxipy" version = "0.1.0" @@ -128,6 +158,12 @@ dependencies = [ { name = "ttp" }, ] +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "responses" }, +] + [package.metadata] requires-dist = [ { name = "pydantic", specifier = ">=2.12.5" }, @@ -135,6 +171,30 @@ requires-dist = [ { name = "ttp", specifier = ">=0.10.0" }, ] +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=9.0.3" }, + { name = "responses", specifier = ">=0.26.1" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + [[package]] name = "pydantic" version = "2.12.5" @@ -268,6 +328,97 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, ] +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + [[package]] name = "requests" version = "2.32.5" @@ -283,6 +434,74 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] +[[package]] +name = "responses" +version = "0.26.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, + { name = "requests" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/58/1fb6de3503428196df78638f991ec8095274f1ee9723e272ee4d9ff0092b/responses-0.26.1.tar.gz", hash = "sha256:2eb3218553cc8f79b57d257bac23af5e1bf381f5b9390b1767816f0843e01dc2", size = 83088, upload-time = "2026-05-21T19:56:39.747Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/31/6a620b4427d546b9e7cca8b3b8c5f0559d9cef2bb9eedcda7f73c1473c19/responses-0.26.1-py3-none-any.whl", hash = "sha256:8aacc4586eb08fb2208ef64a9eb4258d9b0c6e6f4260845f2f018ab847495345", size = 35502, upload-time = "2026-05-21T19:56:38.046Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + [[package]] name = "ttp" version = "0.10.0" -- 2.53.0 From bebbe78163595f07fdf09d86fc4da759bd4163dc Mon Sep 17 00:00:00 2001 From: IluaAir Date: Sat, 6 Jun 2026 11:13:32 +0300 Subject: [PATCH 02/16] Add unit tests for OxiAPI node functionality - Introduced a new test file `test_network.py` to implement unit tests for the `OxiAPI` class. - Added tests for node retrieval, configuration fetching, error handling for missing nodes, and response status checks. - Utilized the `responses` library to mock API responses for comprehensive testing of various scenarios, including successful retrieval and error cases. --- tests/test_network.py | 87 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 tests/test_network.py diff --git a/tests/test_network.py b/tests/test_network.py new file mode 100644 index 0000000..c314048 --- /dev/null +++ b/tests/test_network.py @@ -0,0 +1,87 @@ +import pytest +import responses + +from conftest import load +from oxi import OxiAPI +from oxi.exception import OxiAPIError + +BASE = "https://oxi.example.com" + +NODE_DATA = { + "name": "HQ", + "full_name": "grp/HQ", + "model": "keenetic", + "ip": "192.168.1.1", + "group": "grp", +} + + +@responses.activate +def test_node_show_returns_view(): + responses.get(f"{BASE}/node/show/HQ.json", json=NODE_DATA) + + api = OxiAPI(url=BASE) + node = api.node("HQ") + + assert node.ip == "192.168.1.1" + assert node.model == "keenetic" + assert node.full_name == "grp/HQ" + assert node.group == "grp" + + +@responses.activate +def test_node_config_fetches_and_parses(): + responses.get(f"{BASE}/node/show/HQ.json", json=NODE_DATA) + responses.get(f"{BASE}/node/fetch/grp/HQ", body=load("keenetic")) + + api = OxiAPI(url=BASE) + config = api.node("HQ").config + + assert config.system.model == "Sprinter (KN-3710)" + assert len(config.interfaces) > 0 + + +@responses.activate +def test_node_not_found_maps_to_404(): + responses.get(f"{BASE}/node/show/missing.json", status=404) + + api = OxiAPI(url=BASE) + with pytest.raises(OxiAPIError) as exc: + api.node("missing") + + assert exc.value.status_code == 404 + + +@responses.activate +def test_500_with_node_not_found_html_maps_to_404(): + responses.get( + f"{BASE}/node/show/ghost.json", + status=500, + content_type="text/html", + body="Oxidized::NodeNotFound", + ) + + api = OxiAPI(url=BASE) + with pytest.raises(OxiAPIError) as exc: + api.node("ghost") + + assert exc.value.status_code == 404 + + +@responses.activate +def test_reload_returns_status_code(): + responses.get(f"{BASE}/reload", status=200) + + api = OxiAPI(url=BASE) + assert api.reload() == 200 + + +@responses.activate +def test_unknown_model_raises_value_error(): + data = {**NODE_DATA, "model": "unknown_vendor"} + responses.get(f"{BASE}/node/show/HQ.json", json=data) + responses.get(f"{BASE}/node/fetch/grp/HQ", body="whatever") + + api = OxiAPI(url=BASE) + with pytest.raises(ValueError, match="not found in registry"): + api.node("HQ").config -- 2.53.0 From c4f20d3241b2d48acfb0bbfa9f631a1183ee2d1c Mon Sep 17 00:00:00 2001 From: IluaAir Date: Sat, 6 Jun 2026 11:13:50 +0300 Subject: [PATCH 03/16] Update Huawei TTP template to include model information in version display - Modified the Huawei TTP template to append the model identifier to the version string, enhancing the clarity of system information output for diagnostics. --- oxi/interfaces/models/templates/huawei.ttp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oxi/interfaces/models/templates/huawei.ttp b/oxi/interfaces/models/templates/huawei.ttp index 4686572..e832977 100644 --- a/oxi/interfaces/models/templates/huawei.ttp +++ b/oxi/interfaces/models/templates/huawei.ttp @@ -14,7 +14,7 @@ default_system = { -# VRP (R) software, Version {{ version }} {{ _line_ }} +# VRP (R) software, Version {{ version }} ({{ model }} {{ _line_ }} # ESN of slot {{ slot_number }}: {{ serial_number }} -- 2.53.0 From f446ae52e753ec3f1950ef8a0bc9806788c45447 Mon Sep 17 00:00:00 2001 From: IluaAir Date: Sat, 6 Jun 2026 13:49:55 +0300 Subject: [PATCH 04/16] Add VLAN range expansion utility and corresponding unit tests - Introduced a new utility function `expand_vlan_range` in `utils.py` to expand VLAN range strings into individual VLAN IDs. - Created a new test file `test_units.py` with unit tests for the `expand_vlan_range` function, covering various scenarios including simple ranges, reversed ranges, non-numeric inputs, and list inputs. - Enhanced test coverage for other functionalities in the `device_registry` and template validation classes. --- oxi/interfaces/utils.py | 25 +++++++++++++++ tests/test_units.py | 71 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 oxi/interfaces/utils.py create mode 100644 tests/test_units.py diff --git a/oxi/interfaces/utils.py b/oxi/interfaces/utils.py new file mode 100644 index 0000000..f91466d --- /dev/null +++ b/oxi/interfaces/utils.py @@ -0,0 +1,25 @@ +def expand_vlan_range(value: str | list[str]) -> list[str]: + """Expand values like '1,7,14-15' into individual VLAN IDs.""" + if isinstance(value, list): + value = ",".join(str(item) for item in value) + + result: list[str] = [] + if not value: + return result + for part in value.split(","): + part = part.strip() + if not part: + continue + if "-" in part: + start_s, end_s = part.split("-", 1) + try: + start, end = int(start_s), int(end_s) + except ValueError: + result.append(part) + continue + if start > end: + start, end = end, start + result.extend(str(i) for i in range(start, end + 1)) + else: + result.append(part) + return result diff --git a/tests/test_units.py b/tests/test_units.py new file mode 100644 index 0000000..ad7cf32 --- /dev/null +++ b/tests/test_units.py @@ -0,0 +1,71 @@ +import pytest + +from conftest import load +from oxi.exception import OxiAPIError +from oxi.interfaces import device_registry +from oxi.interfaces.base import BaseDevice +from oxi.interfaces.contract import Interfaces, System +from oxi.interfaces.utils import expand_vlan_range as eltex_expand +from oxi.interfaces.utils import expand_vlan_range as qtech_expand + + +class TestExpandVlanRange: + @pytest.mark.parametrize("expand", [qtech_expand, eltex_expand]) + def test_simple_and_range(self, expand): + assert expand("1,7,14-15") == ["1", "7", "14", "15"] + + @pytest.mark.parametrize("expand", [qtech_expand, eltex_expand]) + def test_reversed_range_is_normalized(self, expand): + assert expand("15-13") == ["13", "14", "15"] + + @pytest.mark.parametrize("expand", [qtech_expand, eltex_expand]) + def test_non_numeric_range_kept_verbatim(self, expand): + assert expand("a-b") == ["a-b"] + + @pytest.mark.parametrize("expand", [qtech_expand, eltex_expand]) + def test_empty(self, expand): + assert expand("") == [] + + @pytest.mark.parametrize("expand", [qtech_expand, eltex_expand]) + def test_list_input(self, expand): + assert expand(["1", "3-4"]) == ["1", "3", "4"] + + +class TestKeeneticDecodeUtf: + @pytest.fixture(scope="class") + def keenetic(self): + return device_registry["keenetic"](load("keenetic")) + + def test_plain_text_passthrough(self, keenetic): + assert keenetic._decode_utf("Plain ASCII") == "Plain ASCII" + + def test_escaped_utf8_is_decoded(self, keenetic): + assert keenetic._decode_utf(r'"\xd0\x94\xd0\xbe\xd0\xbc"') == "Дом" + + +class TestTemplateValidation: + def test_missing_required_group_raises(self): + class OnlySystem(BaseDevice): + template = "dummy.ttp" + + def _load_template(self): + return '' + + with pytest.raises(ValueError, match="missing required groups"): + OnlySystem("data") + + def test_missing_template_file_raises(self): + class NoTemplate(BaseDevice): + template = "does_not_exist.ttp" + + with pytest.raises(FileNotFoundError): + NoTemplate("data") + + +class TestNodeNotFound: + def test_not_found_config_raises_on_parse(self): + device = device_registry["eltex"](load("eltex", "not_found.conf"), name="HQ") + assert device.raw is None + with pytest.raises(OxiAPIError) as exc: + device.parse() + assert exc.value.status_code == 404 -- 2.53.0 From 229bef99f6bd0163e29620445b87f977a6d29886 Mon Sep 17 00:00:00 2001 From: IluaAir Date: Sat, 6 Jun 2026 13:55:01 +0300 Subject: [PATCH 05/16] Add UTF-8 decoding utility and corresponding unit tests - Introduced a new utility function `decode_utf` in `utils.py` to decode escaped UTF-8 descriptions. - Updated unit tests in `test_units.py` to include tests for the `decode_utf` function, covering plain text and escaped UTF-8 scenarios. - Refactored existing tests to streamline the usage of the `expand_vlan_range` function. --- oxi/interfaces/utils.py | 14 ++++++++++++++ tests/test_units.py | 27 +++++++++++---------------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/oxi/interfaces/utils.py b/oxi/interfaces/utils.py index f91466d..7348000 100644 --- a/oxi/interfaces/utils.py +++ b/oxi/interfaces/utils.py @@ -23,3 +23,17 @@ def expand_vlan_range(value: str | list[str]) -> list[str]: else: result.append(part) return result + + +def decode_utf(text: str): + """Decode escaped UTF-8 descriptions.""" + if "\\x" in text: + desc = text.strip('"') + decoded = ( + desc.encode("utf-8") + .decode("unicode_escape") + .encode("latin1") + .decode("utf-8") + ) + return decoded + return text diff --git a/tests/test_units.py b/tests/test_units.py index ad7cf32..0df7e8e 100644 --- a/tests/test_units.py +++ b/tests/test_units.py @@ -5,42 +5,37 @@ from oxi.exception import OxiAPIError from oxi.interfaces import device_registry from oxi.interfaces.base import BaseDevice from oxi.interfaces.contract import Interfaces, System -from oxi.interfaces.utils import expand_vlan_range as eltex_expand -from oxi.interfaces.utils import expand_vlan_range as qtech_expand +from oxi.interfaces.utils import decode_utf, expand_vlan_range class TestExpandVlanRange: - @pytest.mark.parametrize("expand", [qtech_expand, eltex_expand]) + @pytest.mark.parametrize("expand", [expand_vlan_range]) def test_simple_and_range(self, expand): assert expand("1,7,14-15") == ["1", "7", "14", "15"] - @pytest.mark.parametrize("expand", [qtech_expand, eltex_expand]) + @pytest.mark.parametrize("expand", [expand_vlan_range]) def test_reversed_range_is_normalized(self, expand): assert expand("15-13") == ["13", "14", "15"] - @pytest.mark.parametrize("expand", [qtech_expand, eltex_expand]) + @pytest.mark.parametrize("expand", [expand_vlan_range]) def test_non_numeric_range_kept_verbatim(self, expand): assert expand("a-b") == ["a-b"] - @pytest.mark.parametrize("expand", [qtech_expand, eltex_expand]) + @pytest.mark.parametrize("expand", [expand_vlan_range]) def test_empty(self, expand): assert expand("") == [] - @pytest.mark.parametrize("expand", [qtech_expand, eltex_expand]) + @pytest.mark.parametrize("expand", [expand_vlan_range]) def test_list_input(self, expand): assert expand(["1", "3-4"]) == ["1", "3", "4"] -class TestKeeneticDecodeUtf: - @pytest.fixture(scope="class") - def keenetic(self): - return device_registry["keenetic"](load("keenetic")) +class TestDecodeUtf: + def test_plain_text_passthrough(self): + assert decode_utf("Plain ASCII") == "Plain ASCII" - def test_plain_text_passthrough(self, keenetic): - assert keenetic._decode_utf("Plain ASCII") == "Plain ASCII" - - def test_escaped_utf8_is_decoded(self, keenetic): - assert keenetic._decode_utf(r'"\xd0\x94\xd0\xbe\xd0\xbc"') == "Дом" + def test_escaped_utf8_is_decoded(self): + assert decode_utf(r'"\xd0\x94\xd0\xbe\xd0\xbc"') == "Дом" class TestTemplateValidation: -- 2.53.0 From d329ddc4ad879579a436792d44b34ba4cecd270c Mon Sep 17 00:00:00 2001 From: IluaAir Date: Sat, 6 Jun 2026 13:55:51 +0300 Subject: [PATCH 06/16] Add unit tests for device registry model parsing - Introduced a new test file `test_models.py` to implement unit tests for various device models in the `device_registry`. - Added parameterized tests to validate the parsing of device configurations against expected JSON outputs and to ensure required sections are present in the parsed models. - Enhanced test coverage for multiple device types including Mikrotik, Keenetic, Qtech, Huawei, Eltex, and Quasar. --- tests/test_models.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 tests/test_models.py diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 0000000..f9f1f6f --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,38 @@ +import json + +import pytest + +from conftest import FIXTURES, load +from oxi.interfaces import device_registry + +MODEL_CASES = [ + ("mikrotik", "config.conf", "config.expected.json"), + ("keenetic", "config.conf", "config.expected.json"), + ("qtech", "config_1.conf", "config_1.expected.json"), + ("qtech", "config_2.conf", "config_2.expected.json"), + ("huawei", "config.conf", "config.expected.json"), + ("eltex", "config.conf", "config.expected.json"), + ("quasar", "config_1.conf", "config_1.expected.json"), + ("quasar", "config_2.conf", "config_2.expected.json"), +] + + +@pytest.mark.parametrize("model_key, fixture, expected_file", MODEL_CASES) +def test_parse_matches_golden(model_key, fixture, expected_file): + cls = device_registry[model_key] + raw = load(model_key, fixture) + + parsed = cls(raw).parse().model_dump(by_alias=True, mode="json") + + expected = json.loads((FIXTURES / model_key / expected_file).read_text("utf-8")) + assert parsed == expected + + +@pytest.mark.parametrize("model_key, fixture, _expected", MODEL_CASES) +def test_parse_has_required_sections(model_key, fixture, _expected): + cls = device_registry[model_key] + device = cls(load(model_key, fixture)).parse() + + assert device.system is not None + assert isinstance(device.interfaces, list) + assert isinstance(device.vlans, list) -- 2.53.0 From 168111e23c43cd1ff2e0bcde04b8673727bec959 Mon Sep 17 00:00:00 2001 From: IluaAir Date: Sun, 7 Jun 2026 08:41:59 +0300 Subject: [PATCH 07/16] Refactor Keenetic model to utilize centralized UTF-8 decoding utility - Removed the internal `_decode_utf` method from the `Keenetic` class and replaced its usage with the new `decode_utf` utility function for decoding interface descriptions. - Added a new configuration file `config.conf` for Keenetic devices to facilitate testing. - Introduced an expected output JSON file `config.expected.json` to validate the parsing of Keenetic configurations against expected results. --- oxi/interfaces/models/keenetic.py | 24 +- tests/fixtures/keenetic/config.conf | 341 +++++++++++++++++++ tests/fixtures/keenetic/config.expected.json | 161 +++++++++ 3 files changed, 505 insertions(+), 21 deletions(-) create mode 100644 tests/fixtures/keenetic/config.conf create mode 100644 tests/fixtures/keenetic/config.expected.json diff --git a/oxi/interfaces/models/keenetic.py b/oxi/interfaces/models/keenetic.py index 89ea3c7..5764f23 100644 --- a/oxi/interfaces/models/keenetic.py +++ b/oxi/interfaces/models/keenetic.py @@ -1,24 +1,13 @@ from ipaddress import ip_interface from oxi.interfaces import register_parser from oxi.interfaces.base import BaseDevice +from oxi.interfaces.utils import decode_utf @register_parser(["NDMS", "keenetic", "KeeneticOS"]) class Keenetic(BaseDevice): template = "keenetic.ttp" - def _decode_utf(self, text: str): - if "\\x" in text: - desc = text.strip('"') - decoded = ( - desc.encode("utf-8") - .decode("unicode_escape") - .encode("latin1") - .decode("utf-8") - ) - return decoded - return text - def interfaces(self): interfaces: list[dict] = self.raw["interfaces"] for item in interfaces: @@ -29,7 +18,7 @@ class Keenetic(BaseDevice): item["mask"] = ipaddress.network.prefixlen item.pop("netmask", "Key not found") if item.get("description"): - decoded = self._decode_utf(item.get("description", "")) + decoded = decode_utf(item.get("description", "")) item["description"] = decoded return interfaces @@ -37,13 +26,6 @@ class Keenetic(BaseDevice): vlans = self.raw["vlans"] for item in vlans: if item.get("description"): - decoded = self._decode_utf(item.get("description", "")) + decoded = decode_utf(item.get("description", "")) item["description"] = decoded return vlans - - -if __name__ == "__main__": - with open("./test2.txt") as file: - data = file.read() - mikr = Keenetic(data) - print(mikr.parse().model_dump_json()) diff --git a/tests/fixtures/keenetic/config.conf b/tests/fixtures/keenetic/config.conf new file mode 100644 index 0000000..dfab0d1 --- /dev/null +++ b/tests/fixtures/keenetic/config.conf @@ -0,0 +1,341 @@ +! +! release: 4.03.C.6.2-7 +! sandbox: stable +! title: 4.3.6.2 +! arch: mips +! +! ndm: +! exact: 0-a3057529fd +! cdate: 29 Sep 2025 +! +! bsp: +! exact: 0-03b50470c4 +! cdate: 30 Sep 2025 +! +! ndw: +! features: dual_image,led_control,wifi_button,wifi5ghz, +! vht2ghz,mimo2ghz,mimo5ghz,atf2ghz,atf5ghz,wifi6,wifi_ft, +! wpa3,hwnat +! components: base,cloudcontrol,corewireless,ddns,dhcpd, +! dns-filter,dns-https,dns-tls,dot1x,easyconfig,igmp,ip6, +! lang-en,lang-ru,miniupnpd,mws,nathelper-ftp,nathelper- +! h323,nathelper-pptp,nathelper-rtsp,nathelper-sip,ndmp, +! ndns,openvpn,pingcheck,ppe,pppoe,pptp,ssh,trafficcontrol, +! wireguard +! +! ndw3: +! version: 1.101.18.1 +! +! ndw4: +! version: 4.3.C.6.2 +! +! manufacturer: Keenetic Ltd. +! vendor: Keenetic +! series: KN +! model: Sprinter (KN-3710) +! hw_version: 7777777 +! hw_type: router +! hw_id: KN-3710 +! device: Sprinter +! region: EA +! description: Keenetic Sprinter (KN-3710) +! $$$ Agent: http/rci +! $$$ Last change: Fri, 3 Oct 2025 18:37:40 GMT +! $$$ Model: Keenetic Sprinter +! $$$ Username: admin +! $$$ Version: 2.06.1 +system + set net.ipv4.ip_forward 1 + set net.ipv4.neigh.default.gc_thresh1 256 + set net.ipv4.neigh.default.gc_thresh2 1024 + set net.ipv4.neigh.default.gc_thresh3 2048 + set net.ipv4.tcp_fin_timeout 30 + set net.ipv4.tcp_keepalive_time 120 + set net.ipv6.conf.all.forwarding 1 + set net.ipv6.neigh.default.gc_thresh1 256 + set net.ipv6.neigh.default.gc_thresh2 1024 + set net.ipv6.neigh.default.gc_thresh3 2048 + set net.netfilter.nf_conntrack_tcp_timeout_established 1200 + set vm.overcommit_memory 0 + set vm.vfs_cache_pressure 1000 + clock timezone Europe/Berlin + domainname WORKGROUP + hostname test_HW + caption default + description "Keenetic Sprinter (KN-3710)" + ndss dump-report disable +! +dyndns profile _WEBADMIN +! +interface GigabitEthernet0 + up +! +interface GigabitEthernet0/1 + rename 1 + switchport mode access + switchport access vlan 1 + up +! +interface GigabitEthernet0/2 + rename 2 + switchport mode access + switchport access vlan 1 + up +! +interface GigabitEthernet0/3 + rename 3 + switchport mode access + switchport access vlan 1 + up +! +interface GigabitEthernet0/Vlan1 + description "Home VLAN" + ip dhcp client dns-routes + ip name-servers + up +! +interface GigabitEthernet0/Vlan2 + rename ISP + description "\xd0\x9f\xd0\xbe\xd0\xb4\xd0\xba\xd0\xbb\xd1\x8e\xd1\x87\xd0\xb5\xd0\xbd\xd0\xb8\xd0\xb5 Ethernet" + dyndns nobind + mac address factory wan + security-level public + ip address dhcp + ip dhcp client hostname test_HW + ip dhcp client dns-routes + ip mtu 1500 + ip access-group _WEBADMIN_ISP in + ip global 57342 + ip no name-servers + igmp upstream + ipv6 address auto + ipv6 prefix auto + ipv6 no name-servers auto + up +! +interface GigabitEthernet0/0 + rename 0 + role inet for ISP + switchport mode access + switchport access vlan 2 + up +! +interface GigabitEthernet0/Vlan3 + dyndns nobind + ip dhcp client dns-routes + ip name-servers + up +! +interface WifiMaster0 + country-code RU + compatibility BGN+AX + rekey-interval 86400 + up +! +interface WifiMaster0/AccessPoint0 + mac access-list type none + authentication wpa-psk ns3 7777ggggddddsss + encryption enable + encryption wpa2 + ip dhcp client dns-routes + ssid test_HW_2.4G + up +! +interface WifiMaster0/AccessPoint1 + mac access-list type none + security-level private + encryption no enable + ip dhcp client dns-routes + down +! +interface WifiMaster0/AccessPoint2 + mac access-list type none + security-level private + encryption no enable + ip dhcp client dns-routes + down +! +interface WifiMaster0/WifiStation0 + security-level public + encryption no enable + ip dhcp client dns-routes + standby enable + standby timeout 600 + down +! +interface WifiMaster1 + country-code RU + compatibility AN+AC+AX + channel width 40-above/80 + rekey-interval 86400 + up +! +interface WifiMaster1/AccessPoint0 + mac access-list type none + authentication wpa-psk ns3 7777ggggddddsss + encryption enable + encryption wpa2 + ip dhcp client dns-routes + ssid test_HW_5G + up +! +interface WifiMaster1/AccessPoint1 + mac access-list type none + security-level private + encryption no enable + ip dhcp client dns-routes + down +! +interface WifiMaster1/AccessPoint2 + mac access-list type none + security-level private + encryption no enable + ip dhcp client dns-routes + down +! +interface WifiMaster1/WifiStation0 + security-level public + encryption no enable + ip dhcp client dns-routes + standby enable + standby timeout 600 + down +! +interface Bridge0 + rename Home + description "Home network" + dyndns nobind + include GigabitEthernet0/Vlan1 + include WifiMaster0/AccessPoint0 + include WifiMaster1/AccessPoint0 + mac access-list type none + security-level private + ip address 17.36.1.1 255.255.255.0 + ip dhcp client dns-routes + ip access-group _WEBADMIN_Home in + ip name-servers + band-steering + up +! +interface Bridge1 + rename Guest + description "Guest network" + traffic-shape rate 5120 + dyndns nobind + include GigabitEthernet0/Vlan3 + mac access-list type none + peer-isolation + security-level protected + ip address 10.1.30.1 255.255.255.0 + ip dhcp client dns-routes + ip name-servers + down +! +interface Bridge2 + rename Test + mac access-list type none + security-level public + ip dhcp client dns-routes + up +! +interface OpenVPN0 + description test_HW-udp + role misc + security-level public + ip dhcp client dns-routes + ip tcp adjust-mss pmtu + ip name-servers + ipv6 name-servers auto + openvpn accept-routes + openvpn connect + up +! +interface OpenVPN2 + description test_HW-tcp + role misc + dyndns nobind + security-level public + ip dhcp client dns-routes + ip tcp adjust-mss pmtu + openvpn accept-routes + openvpn connect + down +! +interface Wireguard0 + description test_HW + dyndns nobind + security-level public + ip address 10.3.100.1 255.255.255.0 + ip mtu 1324 + ip tcp adjust-mss pmtu + wireguard listen-port 65513 + wireguard peer 7777ggggddddsss= !test_HW + allow-ips 0.0.0.0 0.0.0.0 + connect + ! + up +! +interface Wireguard1 + description test_HW + dyndns nobind + security-level private + ip address 10.1.100.1 255.255.255.0 + ip mtu 1324 + ip access-group _WEBADMIN_Wireguard1 in + ip tcp adjust-mss pmtu + wireguard listen-port 65511 + wireguard peer 7777ggggddddsss= !test_HW + allow-ips 10.1.100.0 255.255.255.0 + allow-ips 17.36.3.0 255.255.255.0 + allow-ips 17.36.1.0 255.255.255.0 + allow-ips 0.0.0.0 0.0.0.0 + connect + ! + up +! +interface Wireguard2 + description test_HW + dyndns nobind + security-level private + ip address 10.2.100.1 255.255.255.0 + ip access-group _WEBADMIN_Wireguard2 in + ip tcp adjust-mss pmtu + wireguard listen-port 65512 + wireguard peer 7777ggggddddsss= !test_HW + allow-ips 0.0.0.0 0.0.0.0 + connect + ! + up +! +ip ssh + port 22 + security-level public + lockout-policy 5 15 3 +! +ip hotspot + policy Home permit + host 7777ggggddddsss permit + host 7777ggggddddsss priority 4 +! +ipv6 subnet Default + bind Home + mode slaac + prefix length 64 + number 0 +! +ppe software +ppe hardware +upnp lan Home +service dhcp +service dns-proxy +service http +service telnet +service ssh +service ntp +service upnp +! +easyconfig disable +components + auto-update disable + auto-update channel stable +! \ No newline at end of file diff --git a/tests/fixtures/keenetic/config.expected.json b/tests/fixtures/keenetic/config.expected.json new file mode 100644 index 0000000..fee724e --- /dev/null +++ b/tests/fixtures/keenetic/config.expected.json @@ -0,0 +1,161 @@ +{ + "system": { + "model": "Sprinter (KN-3710)", + "serial_number": "7777777", + "version": "4.03.C.6.2-7" + }, + "interfaces": [ + { + "interface": "GigabitEthernet0", + "ip_address": null, + "mask": null, + "description": null + }, + { + "interface": "GigabitEthernet0/1", + "ip_address": null, + "mask": null, + "description": null + }, + { + "interface": "GigabitEthernet0/2", + "ip_address": null, + "mask": null, + "description": null + }, + { + "interface": "GigabitEthernet0/3", + "ip_address": null, + "mask": null, + "description": null + }, + { + "interface": "GigabitEthernet0/0", + "ip_address": null, + "mask": null, + "description": null + }, + { + "interface": "WifiMaster0", + "ip_address": null, + "mask": null, + "description": null + }, + { + "interface": "WifiMaster0/AccessPoint0", + "ip_address": null, + "mask": null, + "description": null + }, + { + "interface": "WifiMaster0/AccessPoint1", + "ip_address": null, + "mask": null, + "description": null + }, + { + "interface": "WifiMaster0/AccessPoint2", + "ip_address": null, + "mask": null, + "description": null + }, + { + "interface": "WifiMaster0/WifiStation0", + "ip_address": null, + "mask": null, + "description": null + }, + { + "interface": "WifiMaster1", + "ip_address": null, + "mask": null, + "description": null + }, + { + "interface": "WifiMaster1/AccessPoint0", + "ip_address": null, + "mask": null, + "description": null + }, + { + "interface": "WifiMaster1/AccessPoint1", + "ip_address": null, + "mask": null, + "description": null + }, + { + "interface": "WifiMaster1/AccessPoint2", + "ip_address": null, + "mask": null, + "description": null + }, + { + "interface": "WifiMaster1/WifiStation0", + "ip_address": null, + "mask": null, + "description": null + }, + { + "interface": "Bridge0", + "ip_address": "17.36.1.1", + "mask": 24, + "description": "Home network" + }, + { + "interface": "Bridge1", + "ip_address": "10.1.30.1", + "mask": 24, + "description": "Guest network" + }, + { + "interface": "Bridge2", + "ip_address": null, + "mask": null, + "description": null + }, + { + "interface": "OpenVPN0", + "ip_address": null, + "mask": null, + "description": "test_HW-udp" + }, + { + "interface": "OpenVPN2", + "ip_address": null, + "mask": null, + "description": "test_HW-tcp" + }, + { + "interface": "Wireguard0", + "ip_address": "10.3.100.1", + "mask": 24, + "description": "test_HW" + }, + { + "interface": "Wireguard1", + "ip_address": "10.1.100.1", + "mask": 24, + "description": "test_HW" + }, + { + "interface": "Wireguard2", + "ip_address": "10.2.100.1", + "mask": 24, + "description": "test_HW" + } + ], + "vlans": [ + { + "vlan_id": 1, + "description": "Home VLAN" + }, + { + "vlan_id": 2, + "description": "Подключение Ethernet" + }, + { + "vlan_id": 3, + "description": "Home network" + } + ] +} \ No newline at end of file -- 2.53.0 From 170a2ebf859e2e0d51211f031a985edbfff0865e Mon Sep 17 00:00:00 2001 From: IluaAir Date: Sun, 7 Jun 2026 08:42:41 +0300 Subject: [PATCH 08/16] Refactor Eltex model to use centralized VLAN range expansion utility - Replaced the internal `_expand_vlan_range` function in the `Eltex` class with the new `expand_vlan_range` utility from `utils.py` for improved code maintainability. - Added a new configuration file `config.conf` for Eltex devices to facilitate testing. - Introduced an expected output JSON file `config.expected.json` to validate the parsing of Eltex configurations against expected results. --- oxi/interfaces/models/eltex.py | 34 +----- tests/fixtures/eltex/config.conf | 55 +++++++++ tests/fixtures/eltex/config.expected.json | 137 ++++++++++++++++++++++ tests/fixtures/eltex/not_found.conf | 1 + 4 files changed, 195 insertions(+), 32 deletions(-) create mode 100644 tests/fixtures/eltex/config.conf create mode 100644 tests/fixtures/eltex/config.expected.json create mode 100644 tests/fixtures/eltex/not_found.conf diff --git a/oxi/interfaces/models/eltex.py b/oxi/interfaces/models/eltex.py index ee59af0..4bf652d 100644 --- a/oxi/interfaces/models/eltex.py +++ b/oxi/interfaces/models/eltex.py @@ -1,29 +1,6 @@ from oxi.interfaces import register_parser from oxi.interfaces.base import BaseDevice - - -def _expand_vlan_range(value: str | list[str]) -> list[str]: - if isinstance(value, list): - value = ",".join(str(item) for item in value) - - result: list[str] = [] - for part in value.split(","): - part = part.strip() - if not part: - continue - if "-" not in part: - result.append(part) - continue - start_s, end_s = part.split("-", 1) - try: - start, end = int(start_s), int(end_s) - except ValueError: - result.append(part) - continue - if start > end: - start, end = end, start - result.extend(str(vlan_id) for vlan_id in range(start, end + 1)) - return result +from oxi.interfaces.utils import expand_vlan_range @register_parser("eltex") @@ -54,15 +31,8 @@ class Eltex(BaseDevice): tail = item.get("vlan_tail") if tail: ids = [*ids, tail] if isinstance(ids, list) else f"{ids},{tail}" - for vid in _expand_vlan_range(ids): + for vid in expand_vlan_range(ids): if vid in named_vlan: continue vlans.append({"vlan_id": vid}) return vlans - - -if __name__ == "__main__": - with open("./test_not_found.txt") as file: - data = file.read() - eltex = Eltex(data) - print(eltex.parse()) diff --git a/tests/fixtures/eltex/config.conf b/tests/fixtures/eltex/config.conf new file mode 100644 index 0000000..c212bc3 --- /dev/null +++ b/tests/fixtures/eltex/config.conf @@ -0,0 +1,55 @@ +! +Active-image: flash://system/images/mes3300-669-3R3.ros +! Version: 6.6.9.3 +! Commit: 3a5c2e39 +! Build: 3 (master) +! MD5 Digest: 7bc289cc18be560954bd5cb0afd9b2d5 +! Date: 22-Sep-2025 +! Time: 12:38:20 +! Inactive-image: flash://system/images/_image1.bin +! Version: 6.6.2 +! Commit: 3ebc7503 +! Build: 5 (master) +! MD5 Digest: a3f15a788c97c71e07e90d84c0ff3b12 +! Date: 20-Nov-2023 +! Time: 16:39:20 +! +! Unit MAC address Hardware version Serial number +! ---- ----------------- ---------------- ------------- +! 1 90:54:b7:6b:9d:40 01.01.01 ESG7007778 +! 2 90:54:b7:6b:bb:80 01.01.01 ESG7007777 +! +! +! +interface TenGigabitEthernet1/0/2 + shutdown + description FREE + storm-control broadcast pps 3000 + storm-control multicast pps 3000 +exit +! +interface TenGigabitEthernet1/0/11 + shutdown + description FREE + storm-control broadcast pps 3000 + storm-control multicast pps 3000 +exit +! +interface vlan 1700 + name sw-test_HW + ip address 13.36.8.1 255.255.255.0 +exit +! +vlan database + vlan 114-115,120,130,414,610,999-1000,1701-1703,1705,1801,2001,2011 + vlan 2021-2022,3157-3158,3333-3334 +exit +! +interface vlan 666 + name test +exit +! +interface vlan 777 + name test2 +exit +! diff --git a/tests/fixtures/eltex/config.expected.json b/tests/fixtures/eltex/config.expected.json new file mode 100644 index 0000000..c6b9ead --- /dev/null +++ b/tests/fixtures/eltex/config.expected.json @@ -0,0 +1,137 @@ +{ + "system": { + "model": "", + "serial_number": "ESG7007778", + "version": "6.6.9.3" + }, + "interfaces": [ + { + "interface": "TenGigabitEthernet1/0/2", + "ip_address": null, + "mask": null, + "description": "FREE" + }, + { + "interface": "TenGigabitEthernet1/0/11", + "ip_address": null, + "mask": null, + "description": "FREE" + }, + { + "interface": "vlan 1700", + "ip_address": "13.36.8.1", + "mask": 24, + "description": "sw-test_HW" + }, + { + "interface": "vlan 666", + "ip_address": null, + "mask": null, + "description": "test" + }, + { + "interface": "vlan 777", + "ip_address": null, + "mask": null, + "description": "test2" + } + ], + "vlans": [ + { + "vlan_id": 1700, + "description": "sw-test_HW" + }, + { + "vlan_id": 114, + "description": null + }, + { + "vlan_id": 115, + "description": null + }, + { + "vlan_id": 120, + "description": null + }, + { + "vlan_id": 130, + "description": null + }, + { + "vlan_id": 414, + "description": null + }, + { + "vlan_id": 610, + "description": null + }, + { + "vlan_id": 999, + "description": null + }, + { + "vlan_id": 1000, + "description": null + }, + { + "vlan_id": 1701, + "description": null + }, + { + "vlan_id": 1702, + "description": null + }, + { + "vlan_id": 1703, + "description": null + }, + { + "vlan_id": 1705, + "description": null + }, + { + "vlan_id": 1801, + "description": null + }, + { + "vlan_id": 2001, + "description": null + }, + { + "vlan_id": 2011, + "description": null + }, + { + "vlan_id": 2021, + "description": null + }, + { + "vlan_id": 2022, + "description": null + }, + { + "vlan_id": 3157, + "description": null + }, + { + "vlan_id": 3158, + "description": null + }, + { + "vlan_id": 3333, + "description": null + }, + { + "vlan_id": 3334, + "description": null + }, + { + "vlan_id": 666, + "description": "test" + }, + { + "vlan_id": 777, + "description": "test2" + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/eltex/not_found.conf b/tests/fixtures/eltex/not_found.conf new file mode 100644 index 0000000..205464c --- /dev/null +++ b/tests/fixtures/eltex/not_found.conf @@ -0,0 +1 @@ +node not found \ No newline at end of file -- 2.53.0 From 1bc01c9c1b89064afa39d2d4f1a0b35c2cad9c31 Mon Sep 17 00:00:00 2001 From: IluaAir Date: Sun, 7 Jun 2026 08:44:35 +0300 Subject: [PATCH 09/16] Add Huawei configuration files for testing - Introduced a new configuration file `config.conf` for Huawei devices, detailing interface settings and VLAN configurations. - Added an expected output JSON file `config.expected.json` to validate the parsing of Huawei configurations against expected results, including system model, serial number, and interface details. --- tests/fixtures/huawei/config.conf | 38 +++++++++ tests/fixtures/huawei/config.expected.json | 95 ++++++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 tests/fixtures/huawei/config.conf create mode 100644 tests/fixtures/huawei/config.expected.json diff --git a/tests/fixtures/huawei/config.conf b/tests/fixtures/huawei/config.conf new file mode 100644 index 0000000..990fb8f --- /dev/null +++ b/tests/fixtures/huawei/config.conf @@ -0,0 +1,38 @@ +# Huawei Versatile Routing Platform Software +# VRP (R) software, Version 5.170 (S5731 V200R019C00SPC500) +# Copyright (C) 2000-2019 HUAWEI TECH Co., Ltd. +# +# DDR Memory Size : 4096 M bytes +# FLASH Total Memory Size : 1024 M bytes +# FLASH Available Memory Size : 739 M bytes +# Pcb Version : VER.B +# BootROM Version : 0000.04e4 +# BootLoad Version : 0213.0000 +# CPLD Version : 0104 +# Software Version : VRP (R) Software, Version 5.170 (V200R019C00SPC500) +# FLASH Version : 0000.0000 +# PWR1 information +# Pcb Version : PWR VER.D +# PWR2 information +# Pcb Version : PWR VER.D +# FAN1 information +# Pcb Version : NA +# FAN2 information +# Pcb Version : NA +# ESN of slot 1: 102266666666 +# ESN of slot 2: 102288888888 +interface GigabitEthernet0/0/33 + port link-type access + port default vlan 101 + loopback-detect enable + stp disable + storm-control broadcast min-rate 1500 max-rate 2500 + storm-control multicast min-rate 1000 max-rate 2000 + storm-control action error-down +# +interface Vlanif120 + description SSH + ip address 10.26.196.254 255.255.255.0 +# +vlan batch 13 26 101 to 103 110 120 130 201 to 204 209 to 212 350 360 +# \ No newline at end of file diff --git a/tests/fixtures/huawei/config.expected.json b/tests/fixtures/huawei/config.expected.json new file mode 100644 index 0000000..39280d0 --- /dev/null +++ b/tests/fixtures/huawei/config.expected.json @@ -0,0 +1,95 @@ +{ + "system": { + "model": "S5731", + "serial_number": "102266666666", + "version": "5.170" + }, + "interfaces": [ + { + "interface": "GigabitEthernet0/0/33", + "ip_address": null, + "mask": null, + "description": null + }, + { + "interface": "Vlanif120", + "ip_address": "10.26.196.254", + "mask": 24, + "description": "SSH" + } + ], + "vlans": [ + { + "vlan_id": 13, + "description": null + }, + { + "vlan_id": 26, + "description": null + }, + { + "vlan_id": 101, + "description": null + }, + { + "vlan_id": 102, + "description": null + }, + { + "vlan_id": 103, + "description": null + }, + { + "vlan_id": 110, + "description": null + }, + { + "vlan_id": 120, + "description": null + }, + { + "vlan_id": 130, + "description": null + }, + { + "vlan_id": 201, + "description": null + }, + { + "vlan_id": 202, + "description": null + }, + { + "vlan_id": 203, + "description": null + }, + { + "vlan_id": 204, + "description": null + }, + { + "vlan_id": 209, + "description": null + }, + { + "vlan_id": 210, + "description": null + }, + { + "vlan_id": 211, + "description": null + }, + { + "vlan_id": 212, + "description": null + }, + { + "vlan_id": 350, + "description": null + }, + { + "vlan_id": 360, + "description": null + } + ] +} \ No newline at end of file -- 2.53.0 From 0ef5e7798ae72a5fe1a980851fedf7496d382673 Mon Sep 17 00:00:00 2001 From: IluaAir Date: Sun, 7 Jun 2026 08:47:08 +0300 Subject: [PATCH 10/16] Refactor Qtech model to utilize centralized VLAN range expansion utility - Replaced the internal `_expand_vlan_range` function in the `Qtech` class with the new `expand_vlan_range` utility from `utils.py` for improved code maintainability. - Added new configuration files `config_1.conf` and `config_2.conf` for Qtech devices to facilitate testing. - Introduced expected output JSON files `config_1.expected.json` and `config_2.expected.json` to validate the parsing of Qtech configurations against expected results. --- oxi/interfaces/models/qtech.py | 43 +---- tests/fixtures/qtech/config_1.conf | 42 +++++ tests/fixtures/qtech/config_1.expected.json | 185 ++++++++++++++++++++ tests/fixtures/qtech/config_2.conf | 36 ++++ tests/fixtures/qtech/config_2.expected.json | 185 ++++++++++++++++++++ 5 files changed, 450 insertions(+), 41 deletions(-) create mode 100644 tests/fixtures/qtech/config_1.conf create mode 100644 tests/fixtures/qtech/config_1.expected.json create mode 100644 tests/fixtures/qtech/config_2.conf create mode 100644 tests/fixtures/qtech/config_2.expected.json diff --git a/oxi/interfaces/models/qtech.py b/oxi/interfaces/models/qtech.py index cf2247b..fc70cf9 100644 --- a/oxi/interfaces/models/qtech.py +++ b/oxi/interfaces/models/qtech.py @@ -1,32 +1,6 @@ from oxi.interfaces import register_parser from oxi.interfaces.base import BaseDevice - - -def _expand_vlan_range(value: str | list[str]) -> list[str]: - """Expand values like '1,7,14-15' into individual VLAN IDs.""" - if isinstance(value, list): - value = ",".join(str(item) for item in value) - - result: list[str] = [] - if not value: - return result - for part in value.split(","): - part = part.strip() - if not part: - continue - if "-" in part: - start_s, end_s = part.split("-", 1) - try: - start, end = int(start_s), int(end_s) - except ValueError: - result.append(part) - continue - if start > end: - start, end = end, start - result.extend(str(i) for i in range(start, end + 1)) - else: - result.append(part) - return result +from oxi.interfaces.utils import expand_vlan_range @register_parser(["QTECH"]) @@ -48,21 +22,8 @@ class Qtech(BaseDevice): tail = item.get("vlan_tail") if tail: ids = [*ids, tail] if isinstance(ids, list) else f"{ids},{tail}" - for vid in _expand_vlan_range(ids): + for vid in expand_vlan_range(ids): if vid in named_vlan: continue vlans.append({"vlan_id": vid}) return vlans - - -if __name__ == "__main__": - with open("./test3.txt") as file: - data = file.read() - qtech = Qtech(data) - qt = qtech.parse() - print(qt) - with open("./test3-1.txt") as file: - data = file.read() - qtech = Qtech(data) - qt = qtech.parse() - print(qt) diff --git a/tests/fixtures/qtech/config_1.conf b/tests/fixtures/qtech/config_1.conf new file mode 100644 index 0000000..df0dcb1 --- /dev/null +++ b/tests/fixtures/qtech/config_1.conf @@ -0,0 +1,42 @@ +! QTECH LLC Internetwork Operating System Software +! QSW-8330-40T-DC Series Software, Version 2.2.0C Build 96279, RELEASE SOFTWARE +! ROM: System Bootstrap, Version 0.4.7,hardware version:A +! Serial num:6060606060606060, ID num:555555555555 +! System image file is "Switch.bin" +! QTECH LLC QSW-8330-40T-DC RISC +! 524288K bytes of memory,16384K bytes of flash +! Base ethernet MAC Address: 08:c6:b3:08:cf:ff +! snmp info: +! vend_ID:27514 product_ID:404 system_ID:1.3.6.1.4.1.27514 +interface GigaEthernet1/0/9 + shutdown + description FREE + switchport pvid 102 + storm-control broadcast threshold 15 + storm-control broadcast action shutdown + storm-control broadcast auto_resume 60s + storm-control multicast threshold 10 + storm-control multicast action shutdown + storm-control multicast auto_resume 60s + qos policy IPP3 ingress +! +interface VLAN1 + ip address 192.168.0.1 255.255.0.0 + ip mtu 1500 + no ip directed-broadcast +! +interface VLAN1002 + description test-1002 + ip address 13.36.8.1 255.255.255.0 + ip mtu 1500 + no ip directed-broadcast +! +vlan 772 + name test +! +vlan 888 + name test_super +! +vlan 1,7,14-15,44,101-102,115,117-124,130-136,139,167,200-205,772 +,1607 +vlan 888,2016,2085-2088 diff --git a/tests/fixtures/qtech/config_1.expected.json b/tests/fixtures/qtech/config_1.expected.json new file mode 100644 index 0000000..e9721f4 --- /dev/null +++ b/tests/fixtures/qtech/config_1.expected.json @@ -0,0 +1,185 @@ +{ + "system": { + "model": "QSW-8330-40T-DC", + "serial_number": "6060606060606060", + "version": "96279" + }, + "interfaces": [ + { + "interface": "GigaEthernet1/0/9", + "ip_address": null, + "mask": null, + "description": "FREE" + }, + { + "interface": "VLAN1", + "ip_address": "192.168.0.1", + "mask": 16, + "description": null + }, + { + "interface": "VLAN1002", + "ip_address": "13.36.8.1", + "mask": 24, + "description": "test-1002" + } + ], + "vlans": [ + { + "vlan_id": 772, + "description": "test" + }, + { + "vlan_id": 888, + "description": "test_super" + }, + { + "vlan_id": 1, + "description": null + }, + { + "vlan_id": 7, + "description": null + }, + { + "vlan_id": 14, + "description": null + }, + { + "vlan_id": 15, + "description": null + }, + { + "vlan_id": 44, + "description": null + }, + { + "vlan_id": 101, + "description": null + }, + { + "vlan_id": 102, + "description": null + }, + { + "vlan_id": 115, + "description": null + }, + { + "vlan_id": 117, + "description": null + }, + { + "vlan_id": 118, + "description": null + }, + { + "vlan_id": 119, + "description": null + }, + { + "vlan_id": 120, + "description": null + }, + { + "vlan_id": 121, + "description": null + }, + { + "vlan_id": 122, + "description": null + }, + { + "vlan_id": 123, + "description": null + }, + { + "vlan_id": 124, + "description": null + }, + { + "vlan_id": 130, + "description": null + }, + { + "vlan_id": 131, + "description": null + }, + { + "vlan_id": 132, + "description": null + }, + { + "vlan_id": 133, + "description": null + }, + { + "vlan_id": 134, + "description": null + }, + { + "vlan_id": 135, + "description": null + }, + { + "vlan_id": 136, + "description": null + }, + { + "vlan_id": 139, + "description": null + }, + { + "vlan_id": 167, + "description": null + }, + { + "vlan_id": 200, + "description": null + }, + { + "vlan_id": 201, + "description": null + }, + { + "vlan_id": 202, + "description": null + }, + { + "vlan_id": 203, + "description": null + }, + { + "vlan_id": 204, + "description": null + }, + { + "vlan_id": 205, + "description": null + }, + { + "vlan_id": 1607, + "description": null + }, + { + "vlan_id": 2016, + "description": null + }, + { + "vlan_id": 2085, + "description": null + }, + { + "vlan_id": 2086, + "description": null + }, + { + "vlan_id": 2087, + "description": null + }, + { + "vlan_id": 2088, + "description": null + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/qtech/config_2.conf b/tests/fixtures/qtech/config_2.conf new file mode 100644 index 0000000..cf22ea4 --- /dev/null +++ b/tests/fixtures/qtech/config_2.conf @@ -0,0 +1,36 @@ +! QTECH LLC Internetwork Operating System Software +! QSW-8330-40T-DC Series Software, Version 2.2.0C Build 96279, RELEASE SOFTWARE +! ROM: System Bootstrap, Version 0.4.7,hardware version:A +! Serial num:6060606060606060, ID num:555555555555 +! System image file is "Switch.bin" +! QTECH LLC QSW-8330-40T-DC RISC +! 524288K bytes of memory,16384K bytes of flash +! Base ethernet MAC Address: 08:c6:b3:08:cf:f7 +! snmp info: +! vend_ID:27514 product_ID:404 system_ID:1.3.6.1.4.1.27514 +interface GigaEthernet1/0/9 + shutdown + description FREE + switchport pvid 102 + storm-control broadcast threshold 15 + storm-control broadcast action shutdown + storm-control broadcast auto_resume 60s + storm-control multicast threshold 10 + storm-control multicast action shutdown + storm-control multicast auto_resume 60s + qos policy IPP3 ingress +! +interface VLAN1 + ip address 192.168.0.1 255.255.0.0 + ip mtu 1500 + no ip directed-broadcast +! +interface VLAN1002 + description test-1002 + ip address 13.36.8.1 255.255.255.0 + ip mtu 1500 + no ip directed-broadcast +! +vlan 1,7,14-15,44,101-102,115,117-124,130-136,139,167,200-205,772 +,1607 +vlan 888,2016,2085-2088 diff --git a/tests/fixtures/qtech/config_2.expected.json b/tests/fixtures/qtech/config_2.expected.json new file mode 100644 index 0000000..e9a4a80 --- /dev/null +++ b/tests/fixtures/qtech/config_2.expected.json @@ -0,0 +1,185 @@ +{ + "system": { + "model": "QSW-8330-40T-DC", + "serial_number": "6060606060606060", + "version": "96279" + }, + "interfaces": [ + { + "interface": "GigaEthernet1/0/9", + "ip_address": null, + "mask": null, + "description": "FREE" + }, + { + "interface": "VLAN1", + "ip_address": "192.168.0.1", + "mask": 16, + "description": null + }, + { + "interface": "VLAN1002", + "ip_address": "13.36.8.1", + "mask": 24, + "description": "test-1002" + } + ], + "vlans": [ + { + "vlan_id": 1, + "description": null + }, + { + "vlan_id": 7, + "description": null + }, + { + "vlan_id": 14, + "description": null + }, + { + "vlan_id": 15, + "description": null + }, + { + "vlan_id": 44, + "description": null + }, + { + "vlan_id": 101, + "description": null + }, + { + "vlan_id": 102, + "description": null + }, + { + "vlan_id": 115, + "description": null + }, + { + "vlan_id": 117, + "description": null + }, + { + "vlan_id": 118, + "description": null + }, + { + "vlan_id": 119, + "description": null + }, + { + "vlan_id": 120, + "description": null + }, + { + "vlan_id": 121, + "description": null + }, + { + "vlan_id": 122, + "description": null + }, + { + "vlan_id": 123, + "description": null + }, + { + "vlan_id": 124, + "description": null + }, + { + "vlan_id": 130, + "description": null + }, + { + "vlan_id": 131, + "description": null + }, + { + "vlan_id": 132, + "description": null + }, + { + "vlan_id": 133, + "description": null + }, + { + "vlan_id": 134, + "description": null + }, + { + "vlan_id": 135, + "description": null + }, + { + "vlan_id": 136, + "description": null + }, + { + "vlan_id": 139, + "description": null + }, + { + "vlan_id": 167, + "description": null + }, + { + "vlan_id": 200, + "description": null + }, + { + "vlan_id": 201, + "description": null + }, + { + "vlan_id": 202, + "description": null + }, + { + "vlan_id": 203, + "description": null + }, + { + "vlan_id": 204, + "description": null + }, + { + "vlan_id": 205, + "description": null + }, + { + "vlan_id": 772, + "description": null + }, + { + "vlan_id": 1607, + "description": null + }, + { + "vlan_id": 888, + "description": null + }, + { + "vlan_id": 2016, + "description": null + }, + { + "vlan_id": 2085, + "description": null + }, + { + "vlan_id": 2086, + "description": null + }, + { + "vlan_id": 2087, + "description": null + }, + { + "vlan_id": 2088, + "description": null + } + ] +} \ No newline at end of file -- 2.53.0 From a617bd6ecdc26709f75735185c9eb9e89b33abec Mon Sep 17 00:00:00 2001 From: IluaAir Date: Sun, 7 Jun 2026 08:47:51 +0300 Subject: [PATCH 11/16] Add description fields to Eltex interface template - Updated the Eltex TTP template to include 'name' and 'description' fields for interfaces, enhancing clarity and detail in the generated configurations. --- oxi/interfaces/models/templates/eltex.ttp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/oxi/interfaces/models/templates/eltex.ttp b/oxi/interfaces/models/templates/eltex.ttp index ce66112..b47e14b 100644 --- a/oxi/interfaces/models/templates/eltex.ttp +++ b/oxi/interfaces/models/templates/eltex.ttp @@ -26,6 +26,8 @@ Active-image: {{ ignore }} {{ _start_ }} interface {{ interface | ORPHRASE }} ip address {{ ip_address }} {{ mask | to_cidr }} + name {{ description }} + description {{ description }} -- 2.53.0 From 2ea056aa170f9834a658362d698af94520501da9 Mon Sep 17 00:00:00 2001 From: IluaAir Date: Sun, 7 Jun 2026 09:06:24 +0300 Subject: [PATCH 12/16] Refactor Mikrotik model and update TTP template for improved configuration handling - Removed unused imports and main execution block from the Mikrotik model for cleaner code. - Updated the Mikrotik TTP template to adjust the order of parameters in the 'add' command, enhancing clarity in the generated configurations. - Added a new configuration file `config.conf` for Mikrotik devices to facilitate testing and validation of parsing functionality. --- oxi/interfaces/models/mikrotik.py | 9 -- oxi/interfaces/models/templates/mikrotik.ttp | 2 +- tests/fixtures/mikrotik/config.conf | 123 +++++++++++++++++++ 3 files changed, 124 insertions(+), 10 deletions(-) create mode 100644 tests/fixtures/mikrotik/config.conf diff --git a/oxi/interfaces/models/mikrotik.py b/oxi/interfaces/models/mikrotik.py index 25a6eae..6d5acd4 100644 --- a/oxi/interfaces/models/mikrotik.py +++ b/oxi/interfaces/models/mikrotik.py @@ -1,4 +1,3 @@ -import os from oxi.interfaces import register_parser from oxi.interfaces.base import BaseDevice @@ -6,11 +5,3 @@ from oxi.interfaces.base import BaseDevice @register_parser(["routeros", "ros", "mikrotik"]) class Mikrotik(BaseDevice): template = "mikrotik.ttp" - - -if __name__ == "__main__": - print(os.path.abspath(os.curdir)) - with open("./test.txt") as file: - data = file.read() - mikr = Mikrotik(data) - print(mikr.parse().json()) diff --git a/oxi/interfaces/models/templates/mikrotik.ttp b/oxi/interfaces/models/templates/mikrotik.ttp index a6cbace..47b5028 100644 --- a/oxi/interfaces/models/templates/mikrotik.ttp +++ b/oxi/interfaces/models/templates/mikrotik.ttp @@ -42,6 +42,6 @@ add comment={{ comment | _start_ | ORPHRASE | exclude("disabled=") | strip('"')} ## disabled with comment with/without quotes add comment={{ comment | _start_ | ORPHRASE | exclude("disabled=") | strip('"')}} disabled={{ disabled | replace("yes","True") | strip('"') }} interface={{ interface }} name={{ name | ORPHRASE | strip('"') }} vlan-id={{ vlan_id }} ## disabled no comment -add interface={{ interface | _start_ }} name={{ name | ORPHRASE | strip('"') }} vlan-id={{ vlan_id }} disabled={{ disabled | replace("yes","True") | strip('"') }} +add disabled={{ disabled | _start_ | replace("yes","True") | strip('"') }} interface={{ interface }} name={{ name | ORPHRASE | strip('"') }} vlan-id={{ vlan_id }} diff --git a/tests/fixtures/mikrotik/config.conf b/tests/fixtures/mikrotik/config.conf new file mode 100644 index 0000000..326f5b3 --- /dev/null +++ b/tests/fixtures/mikrotik/config.conf @@ -0,0 +1,123 @@ +# 2026-02-18 22:32:59 by RouterOS 7.19.3 +# software id = 0V5S-56MC +# version: 7.12 +# model = C52iG-5HaxD2HaxD +# serial number = HE108BBGW0B +/interface bridge +add name=bridge.LAN +/interface ethernet +set [ find default-name=ether1 ] mac-address=C4:AD:32:B2:A1:9A poe-out=off +set [ find default-name=ether4 ] comment=test +/interface vlan +add comment="super test vlan" interface=ether4 name="test vlan" vlan-id=255 +add disabled=yes interface=ether5 name="test test vlan" vlan-id=254 +/interface list +add name=LAN +add name=WAN +/interface wifi channel +add band=2ghz-ax disabled=no name=ch-2ghz width=20/40mhz +add band=5ghz-ax disabled=no name=ch-5ghz width=20/40mhz +/interface wifi security +add authentication-types=wpa2-psk name=common-auth wps=disable +/interface wifi configuration +add name=common-auth security=common-auth ssid=test_HW +/interface wifi +set [ find default-name=wifi1 ] channel=ch-5ghz configuration=common-auth configuration.mode=ap disabled=no +set [ find default-name=wifi2 ] channel=ch-2ghz configuration=common-auth configuration.mode=ap disabled=no +/ip pool +add name=dhcp_pool0 ranges=172.16.3.2-172.16.3.254 +add name=dhcp_pool1 ranges=192.168.3.2-192.168.3.254 +/ip dhcp-server +add address-pool=dhcp_pool0 interface=bridge.LAN lease-time=10h name=dhcp1 +/ppp profile +add name=new_antizapret on-down="/ip dns cache flush\r\ + \n" on-up="/ip dns cache flush\r\ + \n" +add name=robovps on-down="/ip dns cache flush" on-up="/ip dns cache flush" use-ipv6=no +/routing table +add disabled=no fib name=test_HW_table +/interface bridge port +add bridge=bridge.LAN interface=ether2 learn=yes +add bridge=bridge.LAN interface=ether3 +add bridge=bridge.LAN interface=ether4 +add bridge=bridge.LAN interface=ether5 +add bridge=bridge.LAN interface=wifi1 +add bridge=bridge.LAN interface=wifi2 +/ipv6 settings +set disable-ipv6=yes forward=no +/interface detect-internet +set detect-interface-list=all +/interface list member +add interface=bridge.LAN list=LAN +add interface=ether1 list=WAN +/interface ovpn-server server +add mac-address=FE:25:E0:B8:66:01 name=ovpn-server1 +/ip address +add address=172.16.3.1/24 interface=bridge.LAN network=172.16.3.0 +add address=10.38.3.245/24 interface=ether1 network=10.38.3.0 +add address=10.1.100.2/24 interface=wireguard2 network=10.1.100.0 +add address=100.10.10.1/24 disabled=yes interface=ether4 network=100.10.10.0 +/ip dhcp-server lease +add address=172.16.3.20 client-id=1:d8:3a:dd:22:28:1d mac-address=D8:3A:DD:21:28:1D server=dhcp1 +add address=172.16.3.4 client-id=1:2c:cd:29:1a:ea:6d comment=test_HW mac-address=1E:CD:29:8A:EA:6D server=dhcp1 +/ip dhcp-server network +add address=172.16.3.0/24 dns-server=172.16.3.1 gateway=172.16.3.1 +/ip dns +set allow-remote-requests=yes servers=217.10.44.35 +/ip dns static +add address=172.16.3.20 regexp=".*\\.home\$" type=A +add address=172.16.3.20 regexp=".*\\.home.uk\$" type=A +add address=172.16.3.20 disabled=yes regexp=".*\\.home.uk\$" type=A +/ip firewall address-list +add address=172.16.3.0/24 list=test_HW +add address=172.16.2.0/24 list=test_HW +add address=172.16.1.0/24 list=test_HW +add address=255.255.255.255 list=test_HW +/ip firewall filter +add action=drop chain=forward out-interface=ether1 src-address=172.16.3.11 +add action=accept chain=input src-address=255.255.255.255 +add action=accept chain=forward in-interface=wireguard2 +add action=accept chain=forward connection-state=established,related in-interface=ether1 +add action=accept chain=input connection-state=established,related in-interface=ether1 +add action=accept chain=input in-interface=bridge.LAN +add action=drop chain=input in-interface=ether1 +add action=drop chain=forward in-interface=ether1 +/ip firewall mangle +add action=passthrough chain=prerouting connection-mark=test_HW src-address=172.16.3.20 +/ip firewall nat +add action=masquerade chain=srcnat out-interface=ether1 +add action=masquerade chain=srcnat comment=test_HW out-interface=test_HW +/ip firewall service-port +set ftp disabled=yes +set tftp disabled=yes +set h323 disabled=yes +/ip route +add disabled=no distance=1 dst-address=172.16.1.0/24 gateway=10.1.100.1 +add disabled=no distance=1 dst-address=172.16.2.0/24 gateway=10.1.100.1 +add disabled=no distance=1 dst-address=255.255.255.255/32 gateway=10.38.3.1 +add disabled=no distance=3 dst-address=0.0.0.0/0 gateway=10.38.3.1 routing-table=main scope=30 suppress-hw-offload=no target-scope=10 +add disabled=yes distance=2 dst-address=0.0.0.0/0 gateway=10.38.3.1 routing-table=main scope=30 suppress-hw-offload=no target-scope=10 +add comment=test_HW disabled=no distance=1 dst-address=185.255.255.255/32 gateway=10.38.3.1 routing-table=main scope=30 suppress-hw-offload=no target-scope=10 +add comment=test_HW disabled=no distance=1 dst-address=192.168.255.255/32 gateway=10.38.3.1 routing-table=main scope=30 suppress-hw-offload=no target-scope=10 +/ip service +set telnet disabled=yes +/system clock +set time-zone-name=Europe/Berlin +/system identity +set name=test_HW +/system logging +set 0 topics=info,!wireless +/system note +set show-at-login=no +/system ntp client +set enabled=yes +/system ntp client servers +add address=pool.ntp.org +/tool mac-server +set allowed-interface-list=LAN +/tool mac-server mac-winbox +set allowed-interface-list=LAN +/tool mac-server ping +set enabled=no +/user aaa +set default-group=full use-radius=yes \ No newline at end of file -- 2.53.0 From 9c90279868f400c1a478dc49f99ef3c17469bb3e Mon Sep 17 00:00:00 2001 From: IluaAir Date: Sun, 7 Jun 2026 09:06:52 +0300 Subject: [PATCH 13/16] Add normalization method for TTP group results in BaseDevice class - Introduced a static method `_as_list` to normalize TTP group results, ensuring consistent list output regardless of input type (dict or list). - Updated the `_validate_contract` method to utilize `_as_list` for processing interfaces and VLANs, improving code clarity and reliability. --- oxi/interfaces/base.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/oxi/interfaces/base.py b/oxi/interfaces/base.py index 0e3cbe2..3d51188 100644 --- a/oxi/interfaces/base.py +++ b/oxi/interfaces/base.py @@ -67,6 +67,19 @@ class BaseDevice(ABC): """ return self.raw.get("system", None) + @staticmethod + def _as_list(data) -> list: + """Normalize a TTP group result to a list. + + TTP returns a single dict when a group matches exactly one entry and a + list when it matches several. Callers always expect a list. + """ + if data is None: + return [] + if isinstance(data, dict): + return [data] + return data + def _validate_contract(self) -> dict: if self.raw is None: msg = ( @@ -76,7 +89,7 @@ class BaseDevice(ABC): ) raise OxiAPIError(msg, status_code=404) system_data = self.system() - interfaces_data = self.interfaces() or [] + interfaces_data = self._as_list(self.interfaces()) result = { "system": System(**system_data), "interfaces": [Interfaces(**item) for item in interfaces_data], @@ -89,7 +102,7 @@ class BaseDevice(ABC): f"{self.__class__.__name__}: template '{self.template}' declares optional group " f"'vlans', but TTP did not return it." ) - vlans_data = self.vlans() or [] + vlans_data = self._as_list(self.vlans()) result["vlans"] = [Vlans(**item) for item in vlans_data] return result -- 2.53.0 From acb3a6291ce4fcbebe5f6f1e7cf28a0a93210cc4 Mon Sep 17 00:00:00 2001 From: IluaAir Date: Sun, 7 Jun 2026 09:07:17 +0300 Subject: [PATCH 14/16] Add expected configuration output for Mikrotik devices - Introduced a new JSON file `config.expected.json` containing expected system and interface configurations for Mikrotik devices, including model, serial number, IP addresses, and VLAN details. - This addition facilitates testing and validation of the parsing functionality for Mikrotik configurations. --- tests/fixtures/mikrotik/config.expected.json | 43 ++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 tests/fixtures/mikrotik/config.expected.json diff --git a/tests/fixtures/mikrotik/config.expected.json b/tests/fixtures/mikrotik/config.expected.json new file mode 100644 index 0000000..04427e6 --- /dev/null +++ b/tests/fixtures/mikrotik/config.expected.json @@ -0,0 +1,43 @@ +{ + "system": { + "model": "C52iG-5HaxD2HaxD", + "serial_number": "HE108BBGW0B", + "version": "7.12" + }, + "interfaces": [ + { + "interface": "bridge.LAN", + "ip_address": "172.16.3.1", + "mask": 24, + "description": null + }, + { + "interface": "ether1", + "ip_address": "10.38.3.245", + "mask": 24, + "description": null + }, + { + "interface": "wireguard2", + "ip_address": "10.1.100.2", + "mask": 24, + "description": null + }, + { + "interface": "ether4", + "ip_address": "100.10.10.1", + "mask": 24, + "description": null + } + ], + "vlans": [ + { + "vlan_id": 255, + "description": "test vlan" + }, + { + "vlan_id": 254, + "description": "test test vlan" + } + ] +} \ No newline at end of file -- 2.53.0 From 686cd6d7157250e2fab9d62db3992346d999c8e9 Mon Sep 17 00:00:00 2001 From: IluaAir Date: Sun, 7 Jun 2026 09:12:47 +0300 Subject: [PATCH 15/16] Remove main execution block from Quasar model and add configuration files for testing - Eliminated the main execution block from the Quasar model for cleaner code. - Introduced new configuration files `config_1.conf` and `config_2.conf` for Quasar devices, detailing interface settings and IP configurations. - Added expected output JSON files `config_1.expected.json` and `config_2.expected.json` to validate the parsing of Quasar configurations against expected results. --- oxi/interfaces/models/quasar.py | 14 --- tests/fixtures/quasar/config_1.conf | 41 +++++++++ tests/fixtures/quasar/config_1.expected.json | 58 ++++++++++++ tests/fixtures/quasar/config_2.conf | 43 +++++++++ tests/fixtures/quasar/config_2.expected.json | 94 ++++++++++++++++++++ 5 files changed, 236 insertions(+), 14 deletions(-) create mode 100644 tests/fixtures/quasar/config_1.conf create mode 100644 tests/fixtures/quasar/config_1.expected.json create mode 100644 tests/fixtures/quasar/config_2.conf create mode 100644 tests/fixtures/quasar/config_2.expected.json diff --git a/oxi/interfaces/models/quasar.py b/oxi/interfaces/models/quasar.py index 5d320f8..a5c8859 100644 --- a/oxi/interfaces/models/quasar.py +++ b/oxi/interfaces/models/quasar.py @@ -21,17 +21,3 @@ class Quasar(BaseDevice): if ether_interface: interfaces.append(ether_interface) return interfaces - - -if __name__ == "__main__": - with open("./test7.txt") as file: - data = file.read() - quasar = Quasar(data) - qt = quasar.parse() - print(qt) - print() - with open("./test8.txt") as file: - data = file.read() - quasar = Quasar(data) - qt = quasar.parse() - print(qt) diff --git a/tests/fixtures/quasar/config_1.conf b/tests/fixtures/quasar/config_1.conf new file mode 100644 index 0000000..3a37374 --- /dev/null +++ b/tests/fixtures/quasar/config_1.conf @@ -0,0 +1,41 @@ +# Copyright © 2021-2022, TechArgos LLC +# ----------- ----------------- +# Subsystem Version +# ----------- ----------------- +# Engine 0.2.17.2022-10-21 +# DPlane 0.2.18.2022-05-16 +# BfMonitor 1.1.2.2022-08-25 +# CLI.core 1.0.3.2022-09-30 +# CLI.engine 1.2.2.2022-10-12 +# RConsole 0.3.4.2022-01-12 +# RcAppParams 0.3.1.2022-01-12 +# SNMP 0.0.11.2022-04-26 +# Zabbix 0.2.13.2022-01-26 +# WebUI 1.1.3.2022-10-19 +# BF.core 9.3.1.2021-01-30 +# +# ------------------------- --------------- +# Platform EEPROM field Value +# ------------------------- --------------- +# Product Name Quasar-T-Q-0002 +# Product Number HB4NC011234M +# Local MAC N/A +# Product Serial Number WEE1C1CC0004A +# Product Version 0.0 +# System Manufacturing Date 2026-06-07 + config ethernet ipv4 address 25.25.1.221/24 gateway 25.25.1.254 enable + + config interface 7/4 fec none mode force-up enable + config interface 8/1 fec none mode force-up enable + config interface 8/2 fec none mode force-up enable + config interface 8/3 fec none mode force-up enable + config interface 8/4 fec none mode force-up enable + config interface 9/1 fec none mode force-up enable + config interface 9/2 fec none mode force-up enable + config interface 7/4 description "IN DWDM / OUT TEST_HW_08_N0_p1" + config interface 8/1 description "IN DWDM / OUT TEST_HW_09_N1_p0" + config interface 8/2 description "IN DWDM / OUT TEST_HW_09_N0_p0" + config interface 8/3 description "IN DWDM / OUT TEST_HW_10_N1_p0" + config interface 8/4 description "IN DWDM / OUT TEST_HW_10_N0_p0" + config interface 9/1 description "IN DWDM / OUT TEST_HW_11_N0_p0" + config interface 9/2 description "IN DWDM / OUT TEST_HW_11_N0_p1" diff --git a/tests/fixtures/quasar/config_1.expected.json b/tests/fixtures/quasar/config_1.expected.json new file mode 100644 index 0000000..592ffbd --- /dev/null +++ b/tests/fixtures/quasar/config_1.expected.json @@ -0,0 +1,58 @@ +{ + "system": { + "model": "Quasar-T-Q-0002", + "serial_number": "WEE1C1CC0004A", + "version": "0.2.17.2022-10-21" + }, + "interfaces": [ + { + "interface": "7/4", + "ip_address": null, + "mask": null, + "description": "IN DWDM / OUT TEST_HW_08_N0_p1" + }, + { + "interface": "8/1", + "ip_address": null, + "mask": null, + "description": "IN DWDM / OUT TEST_HW_09_N1_p0" + }, + { + "interface": "8/2", + "ip_address": null, + "mask": null, + "description": "IN DWDM / OUT TEST_HW_09_N0_p0" + }, + { + "interface": "8/3", + "ip_address": null, + "mask": null, + "description": "IN DWDM / OUT TEST_HW_10_N1_p0" + }, + { + "interface": "8/4", + "ip_address": null, + "mask": null, + "description": "IN DWDM / OUT TEST_HW_10_N0_p0" + }, + { + "interface": "9/1", + "ip_address": null, + "mask": null, + "description": "IN DWDM / OUT TEST_HW_11_N0_p0" + }, + { + "interface": "9/2", + "ip_address": null, + "mask": null, + "description": "IN DWDM / OUT TEST_HW_11_N0_p1" + }, + { + "interface": "ethernet", + "ip_address": "25.25.1.221", + "mask": 24, + "description": null + } + ], + "vlans": [] +} \ No newline at end of file diff --git a/tests/fixtures/quasar/config_2.conf b/tests/fixtures/quasar/config_2.conf new file mode 100644 index 0000000..9d21864 --- /dev/null +++ b/tests/fixtures/quasar/config_2.conf @@ -0,0 +1,43 @@ +# Copyright © 2021-2024, TechArgos LLC +# ----------- ------------------ +# Component Version +# ----------- ------------------ +# Assembly 0.2.23_9.9.1_GA +# Engine 2.23.15.2024-08-22 +# DPlane 0.2.20.2023-10-17 +# BfMonitor 1.4.0.2024-08-27 +# CLI.core 1.0.3.2022-09-30 +# CLI.engine 1.3.4.2024-08-22 +# RConsole 1.0.1.2022-12-23 +# RcAppParams 1.0.1.2022-12-23 +# SNMP 0.3.0.2024-07-08 +# Zabbix 0.2.13.2022-01-26 +# WebUI 1.3.34.2024-08-30 +# NMS.agent 1.2.12.2024-08-21 +# SysLog 0.7.12.2024-02-02 +# NTPd 0.1.5.2024-07-15 +# AAA 0.0.8.2023-11-16 +# +# ------------------------- -------------------- +# Platform EEPROM field Value +# ------------------------- -------------------- +# Product Name D5232C-T +# Part Number TA-PB-D5232C-T-AC-PI +# Local MAC N/A +# Product Serial Number WHF1C87123456A +# Product Version 0.0 +# System Manufacturing Date 2026-06-07 + config ethernet ipv4 address 25.25.18.19/24 gateway 25.25.18.254 enable + config interface 1/1 description "TEST_HW_1_1" + config interface 1/3 description "TEST_HW_1_3" + config interface 2/1 description "TEST_HW_2_1" + config interface 2/2 description "TEST_HW_2_2" + config interface 2/3 description "TEST_HW_2_3" + config interface 2/4 description "TEST_HW_2_4" + config interface 3/1 description "TEST_HW_3_1" + config interface 3/3 description "TEST_HW_3_3" + config interface 3/4 description "TEST_HW_3_4" + config interface 4/1 description "TEST_HW_4_1" + config interface 4/2 description "TEST_HW_4_2" + config interface 4/3 description "TEST_HW_4_3" + config interface 4/4 description "TEST_HW_4_4" \ No newline at end of file diff --git a/tests/fixtures/quasar/config_2.expected.json b/tests/fixtures/quasar/config_2.expected.json new file mode 100644 index 0000000..1ddaf5a --- /dev/null +++ b/tests/fixtures/quasar/config_2.expected.json @@ -0,0 +1,94 @@ +{ + "system": { + "model": "D5232C-T", + "serial_number": "WHF1C87123456A", + "version": "0.2.23_9.9.1_GA" + }, + "interfaces": [ + { + "interface": "1/1", + "ip_address": null, + "mask": null, + "description": "TEST_HW_1_1" + }, + { + "interface": "1/3", + "ip_address": null, + "mask": null, + "description": "TEST_HW_1_3" + }, + { + "interface": "2/1", + "ip_address": null, + "mask": null, + "description": "TEST_HW_2_1" + }, + { + "interface": "2/2", + "ip_address": null, + "mask": null, + "description": "TEST_HW_2_2" + }, + { + "interface": "2/3", + "ip_address": null, + "mask": null, + "description": "TEST_HW_2_3" + }, + { + "interface": "2/4", + "ip_address": null, + "mask": null, + "description": "TEST_HW_2_4" + }, + { + "interface": "3/1", + "ip_address": null, + "mask": null, + "description": "TEST_HW_3_1" + }, + { + "interface": "3/3", + "ip_address": null, + "mask": null, + "description": "TEST_HW_3_3" + }, + { + "interface": "3/4", + "ip_address": null, + "mask": null, + "description": "TEST_HW_3_4" + }, + { + "interface": "4/1", + "ip_address": null, + "mask": null, + "description": "TEST_HW_4_1" + }, + { + "interface": "4/2", + "ip_address": null, + "mask": null, + "description": "TEST_HW_4_2" + }, + { + "interface": "4/3", + "ip_address": null, + "mask": null, + "description": "TEST_HW_4_3" + }, + { + "interface": "4/4", + "ip_address": null, + "mask": null, + "description": "TEST_HW_4_4" + }, + { + "interface": "ethernet", + "ip_address": "25.25.18.19", + "mask": 24, + "description": null + } + ], + "vlans": [] +} \ No newline at end of file -- 2.53.0 From 8edd1a296ce34dadfe2627be45bd653a3ae0cbb7 Mon Sep 17 00:00:00 2001 From: IluaAir Date: Thu, 11 Jun 2026 16:16:14 +0300 Subject: [PATCH 16/16] Add H3C device support with configuration and expected output files - Introduced support for H3C devices by adding a new configuration file `config.conf` containing interface and VLAN settings. - Created an expected output JSON file `config.expected.json` to validate the parsing of H3C configurations. - Updated the test model cases to include H3C for comprehensive testing of device parsing functionality. --- oxi/interfaces/models/h3c.py | 9 +- oxi/interfaces/models/templates/eltex.ttp | 6 +- oxi/interfaces/models/templates/h3c.ttp | 1 + oxi/interfaces/models/templates/huawei.ttp | 2 +- tests/fixtures/h3c/config.conf | 169 +++++++++++++++++++++ tests/fixtures/h3c/config.expected.json | 155 +++++++++++++++++++ tests/test_models.py | 1 + 7 files changed, 331 insertions(+), 12 deletions(-) create mode 100644 tests/fixtures/h3c/config.conf create mode 100644 tests/fixtures/h3c/config.expected.json diff --git a/oxi/interfaces/models/h3c.py b/oxi/interfaces/models/h3c.py index db2444f..3ad40da 100644 --- a/oxi/interfaces/models/h3c.py +++ b/oxi/interfaces/models/h3c.py @@ -14,11 +14,4 @@ class H3C(BaseDevice): vlans.append(item) continue vlans.extend({"vlan_id": vlan_id} for vlan_id in vlan_ids) - return vlans - - -if __name__ == "__main__": - with open("./test5.txt") as file: - data = file.read() - h3c = H3C(data) - print(h3c.parse()) \ No newline at end of file + return vlans \ No newline at end of file diff --git a/oxi/interfaces/models/templates/eltex.ttp b/oxi/interfaces/models/templates/eltex.ttp index b47e14b..57240b6 100644 --- a/oxi/interfaces/models/templates/eltex.ttp +++ b/oxi/interfaces/models/templates/eltex.ttp @@ -26,13 +26,13 @@ Active-image: {{ ignore }} {{ _start_ }} interface {{ interface | ORPHRASE }} ip address {{ ip_address }} {{ mask | to_cidr }} - name {{ description }} - description {{ description }} + name {{ description | ORPHRASE}} + description {{ description | ORPHRASE }} interface vlan {{ vlan_id | _start_ }} - name {{ name }} + name {{ name | ORPHRASE }} vlan {{ _db_ | _start_ }} vlan {{ vlan_ids | joinmatches(',') | unrange("-", ",") | split(",")}} diff --git a/oxi/interfaces/models/templates/h3c.ttp b/oxi/interfaces/models/templates/h3c.ttp index afbe090..dcf1e2c 100644 --- a/oxi/interfaces/models/templates/h3c.ttp +++ b/oxi/interfaces/models/templates/h3c.ttp @@ -21,6 +21,7 @@ default_system = { interface {{ interface }} + description {{ description | ORPHRASE }} ip address {{ ip_address }} {{ mask | to_cidr }} diff --git a/oxi/interfaces/models/templates/huawei.ttp b/oxi/interfaces/models/templates/huawei.ttp index e832977..4464e2c 100644 --- a/oxi/interfaces/models/templates/huawei.ttp +++ b/oxi/interfaces/models/templates/huawei.ttp @@ -20,7 +20,7 @@ default_system = { interface {{ interface }} - description {{ description }} + description {{ description | ORPHRASE }} ip address {{ ip_address }} {{ mask | to_cidr }} diff --git a/tests/fixtures/h3c/config.conf b/tests/fixtures/h3c/config.conf new file mode 100644 index 0000000..81eca56 --- /dev/null +++ b/tests/fixtures/h3c/config.conf @@ -0,0 +1,169 @@ +# H3C Comware Software, Version 7.1.070, Release 6616P01 +# Copyright (c) 2004-2021 New H3C Technologies Co., Ltd. All rights reserved. +# Last reboot reason : User reboot +# +# Boot image: flash:/s9820-cmw710-boot-r6616p01.bin +# Boot image version: 7.1.070, Release 6616P01 +# Compiled May 06 2021 11:00:00 +# System image: flash:/s9820-cmw710-system-r6616p01.bin +# System image version: 7.1.070, Release 6616P01 +# Compiled May 06 2021 11:00:00 +# +# +# MPU(M) Slot 1: +# H3C S9820-64H MPU(M) with 1 C2538 Processor(s) +# BOARD TYPE: S9820-64H +# DRAM: 8192M bytes +# FLASH: 3630M bytes +# NVRAM: 0K bytes +# PCB 1 Version: VER.A +# PCB 2 Version: VER.B +# PCB 3 Version: VER.A +# PCB 4 Version: VER.A +# Basic BootWare Version: 105 +# Extended BootWare Version: 108 +# CPLD 1 Version: 002 +# CPLD 2 Version: 027 +# CPLD 3 Version: 002 +# CPLD 4 Version: 002 +# FPGA 1 Version: 001 +# Release Version: H3C S9820-64H-6616P01 +# Patch Version: None +# Reboot Cause: UserReboot +# [SubSlot 0] 64QSFP28 +# +# MPU(S) Slot 2: +# H3C S9820-64H MPU(S) with 1 C2538 Processor(s) +# BOARD TYPE: S9820-64H +# DRAM: 8192M bytes +# FLASH: 3630M bytes +# NVRAM: 0K bytes +# PCB 1 Version: VER.A +# PCB 2 Version: VER.B +# PCB 3 Version: VER.A +# PCB 4 Version: VER.A +# Basic BootWare Version: 105 +# Extended BootWare Version: 108 +# CPLD 1 Version: 002 +# CPLD 2 Version: 027 +# CPLD 3 Version: 002 +# CPLD 4 Version: 002 +# FPGA 1 Version: 001 +# Release Version: H3C S9820-64H-6616P01 +# Patch Version: None +# Reboot Cause: IRFMergeReboot +# [SubSlot 0] 64QSFP28 +# Slot Type State Subslot Soft Ver Patch Ver +# 1 S9820-64H Master 0 S9820-64H-6616P01 None +# 2 S9820-64H Standby 0 S9820-64H-6616P01 None +# +vlan 1 +# +vlan 12 + name BGP to OSPF1 +# +vlan 13 + name BGP to OSPF2 +# +vlan 15 + name HW_TEST_1 + description HW_TEST_1 +# +vlan 222 + name MGMT + description MGMT +# +vlan 222 +# +vlan 1112 to 1116 +# +vlan 1122 + name DATA +# +vlan 1123 +# +vlan 1200 +# +vlan 1512 +# +vlan 1513 + description cluster HW_TEST_1 +# +vlan 2000 + description cluster HW_TEST_2 +# +vlan 3377 + name VRF3377 +# +irf-port 1/2 + port group interface HundredGigE1/0/63 + port group interface HundredGigE1/0/64 +# +irf-port 2/1 + port group interface HundredGigE2/0/63 + port group interface HundredGigE2/0/64 +# +interface Bridge-Aggregation1 + description HW_TEST_1 + port link-type trunk + undo port trunk permit vlan 1 + port trunk permit vlan 221 1112 to 1116 1512 2000 + link-aggregation mode dynamic +# +interface Bridge-Aggregation2 + description HW_TEST_2 + port link-type trunk + undo port trunk permit vlan 1 + port trunk permit vlan 221 1112 to 1116 1512 + link-aggregation mode dynamic +# +interface Bridge-Aggregation3 + description HW_TEST_3 + port link-type trunk + undo port trunk permit vlan 1 + port trunk permit vlan 221 1112 to 1116 1512 2000 + link-aggregation mode dynamic +# +interface Bridge-Aggregation4 + description HW_TEST_4 + port link-type trunk + undo port trunk permit vlan 1 + port trunk permit vlan 221 1112 to 1116 1512 + link-aggregation mode dynamic + # +interface NULL0 +# +interface Vlan-interface1 + dhcp client identifier ascii 0098a92d5735b0-VLAN0001 +# +interface Vlan-interface12 + description BGP to OSPF1 + mtu 9008 + ip address 15.12.16.246 255.255.255.252 +# +interface Vlan-interface3000 + description L3 to HW_TEST_3 + ip binding vpn-instance HW_TEST_3 + ip address 192.168.19.254 255.255.255.128 +# +interface HundredGigE1/0/3 + port link-mode bridge + description HW_TEST_1 + port link-type trunk + undo port trunk permit vlan 1 + port trunk permit vlan 221 1112 to 1116 1512 + storm-constrain broadcast pps 1100 1000 + storm-constrain multicast pps 1100 1000 + storm-constrain control shutdown + port link-aggregation group 2 +# +interface HundredGigE1/0/63 + description HW_TEST_2 +# +interface M-GigabitEthernet0/0/0 + ip address 192.168.10.101 255.255.255.0 + dhcp client identifier hex 0298a92d5735b0 +# +interface M-GigabitEthernet0/0/1 + dhcp client identifier hex 0298a92d5735b0 +# diff --git a/tests/fixtures/h3c/config.expected.json b/tests/fixtures/h3c/config.expected.json new file mode 100644 index 0000000..f255686 --- /dev/null +++ b/tests/fixtures/h3c/config.expected.json @@ -0,0 +1,155 @@ +{ + "system": { + "model": "S9820-64H", + "serial_number": "", + "version": "7.1.070" + }, + "interfaces": [ + { + "interface": "Bridge-Aggregation1", + "ip_address": null, + "mask": null, + "description": "HW_TEST_1" + }, + { + "interface": "Bridge-Aggregation2", + "ip_address": null, + "mask": null, + "description": "HW_TEST_2" + }, + { + "interface": "Bridge-Aggregation3", + "ip_address": null, + "mask": null, + "description": "HW_TEST_3" + }, + { + "interface": "Bridge-Aggregation4", + "ip_address": null, + "mask": null, + "description": "HW_TEST_4" + }, + { + "interface": "NULL0", + "ip_address": null, + "mask": null, + "description": null + }, + { + "interface": "Vlan-interface1", + "ip_address": null, + "mask": null, + "description": null + }, + { + "interface": "Vlan-interface12", + "ip_address": "15.12.16.246", + "mask": 30, + "description": "BGP to OSPF1" + }, + { + "interface": "Vlan-interface3000", + "ip_address": "192.168.19.254", + "mask": 25, + "description": "L3 to HW_TEST_3" + }, + { + "interface": "HundredGigE1/0/3", + "ip_address": null, + "mask": null, + "description": "HW_TEST_1" + }, + { + "interface": "HundredGigE1/0/63", + "ip_address": null, + "mask": null, + "description": "HW_TEST_2" + }, + { + "interface": "M-GigabitEthernet0/0/0", + "ip_address": "192.168.10.101", + "mask": 24, + "description": null + }, + { + "interface": "M-GigabitEthernet0/0/1", + "ip_address": null, + "mask": null, + "description": null + } + ], + "vlans": [ + { + "vlan_id": 1, + "description": null + }, + { + "vlan_id": 12, + "description": null + }, + { + "vlan_id": 13, + "description": null + }, + { + "vlan_id": 15, + "description": "HW_TEST_1" + }, + { + "vlan_id": 222, + "description": "MGMT" + }, + { + "vlan_id": 222, + "description": null + }, + { + "vlan_id": 1122, + "description": "DATA" + }, + { + "vlan_id": 1123, + "description": null + }, + { + "vlan_id": 1200, + "description": null + }, + { + "vlan_id": 1512, + "description": null + }, + { + "vlan_id": 1513, + "description": null + }, + { + "vlan_id": 2000, + "description": null + }, + { + "vlan_id": 3377, + "description": "HW_TEST_1" + }, + { + "vlan_id": 1112, + "description": null + }, + { + "vlan_id": 1113, + "description": null + }, + { + "vlan_id": 1114, + "description": null + }, + { + "vlan_id": 1115, + "description": null + }, + { + "vlan_id": 1116, + "description": null + } + ] +} \ No newline at end of file diff --git a/tests/test_models.py b/tests/test_models.py index f9f1f6f..d2222a3 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -12,6 +12,7 @@ MODEL_CASES = [ ("qtech", "config_2.conf", "config_2.expected.json"), ("huawei", "config.conf", "config.expected.json"), ("eltex", "config.conf", "config.expected.json"), + ("h3c", "config.conf", "config.expected.json"), ("quasar", "config_1.conf", "config_1.expected.json"), ("quasar", "config_2.conf", "config_2.expected.json"), ] -- 2.53.0