14 Commits

Author SHA1 Message Date
180e830fed Merge pull request 'dev' (#4) from dev into main
All checks were successful
CI / lint (push) Successful in 24s
CI / test (3.10) (push) Successful in 26s
CI / test (3.11) (push) Successful in 39s
CI / test (3.12) (push) Successful in 42s
CI / test (3.13) (push) Successful in 37s
CI / ci-success (push) Successful in 2s
Reviewed-on: #4
2026-07-19 10:32:23 +03:00
IluaAir
9575450811 Update CI workflow to use custom runner and container
All checks were successful
CI / lint (pull_request) Successful in 3m11s
CI / test (3.10) (pull_request) Successful in 27s
CI / test (3.11) (pull_request) Successful in 36s
CI / test (3.12) (pull_request) Successful in 36s
CI / test (3.13) (pull_request) Successful in 37s
CI / ci-success (pull_request) Successful in 1s
- Changed the runner from `ubuntu-latest` to `rassbery` for the `ci-success` job.
- Specified a custom container `catthehacker/ubuntu:act-22.04` for the job environment, enhancing compatibility and consistency in the CI process.
2026-07-19 10:24:25 +03:00
IluaAir
27d8931dee Update Mikrotik template to use 'shutdown' attribute for interface management
- Replaced 'disabled' with 'shutdown' in the Mikrotik template to align with the new interface state management.
- Updated expected configuration JSON to set 'shutdown' to true for accurate testing and validation of interface configurations.
2026-07-16 00:21:10 +03:00
IluaAir
099869ff6d Add shutdown command to Huawei interface template and update configuration files
- Included the `shutdown` command in the Huawei interface template to manage interface states.
- Added a new interface configuration in `config.conf` with the `shutdown` command.
- Updated the expected output JSON to reflect the new interface and its shutdown state for accurate testing and validation of configurations.
2026-07-15 16:26:46 +03:00
IluaAir
6846b30df0 Add shutdown command to Qtech interface template and update expected configuration files
- Included the `shutdown` command in the Qtech interface template to manage interface states.
- Updated expected output JSON files to set the `shutdown` attribute to true for accurate testing and validation of interface configurations.
2026-07-15 15:51:48 +03:00
IluaAir
6009f0bf9c Add shutdown command to H3C interface template and update related configuration files
- Included the `shutdown` command in the H3C interface template to reflect the new interface state management.
- Updated the `config.conf` file to include the `shutdown` command for the `HundredGigE1/0/3` interface.
- Modified the expected output JSON to set the `shutdown` attribute to true, ensuring accurate testing and validation of interface configurations.
2026-07-15 15:48:28 +03:00
IluaAir
da2bf2f876 Update version to 0.1.1 and add coverage file to .gitignore
- Incremented the project version in `pyproject.toml` from 0.1.0 to 0.1.1.
- Added `.coverage` to `.gitignore` to prevent tracking of coverage reports.
2026-07-15 00:47:56 +03:00
IluaAir
a521c60d76 Add shutdown attribute to Interfaces class and update related templates
- Added a `shutdown` boolean attribute to the `Interfaces` class to manage interface states.
- Updated the `eltex.ttp` template to include the `shutdown` command based on the new attribute.
- Modified expected configuration JSON files for various devices to reflect the new `shutdown` attribute, ensuring accurate testing and validation of interface configurations.
2026-07-15 00:47:10 +03:00
IluaAir
fb2d135187 Add CI success verification step to workflows
- Introduced a new job `ci-success` in both Gitea and GitHub CI workflows to verify that all previous jobs (lint and test) succeeded.
- The job runs on `ubuntu-latest` and exits with a failure status if any prior job has failed or been cancelled, enhancing the reliability of the CI process.
2026-06-12 20:36:27 +03:00
53b79ce04f Merge pull request 'dev' (#3) from dev into main
All checks were successful
CI / lint (push) Successful in 29s
CI / test (3.10) (push) Successful in 28s
CI / test (3.11) (push) Successful in 43s
CI / test (3.12) (push) Successful in 41s
CI / test (3.13) (push) Successful in 43s
Reviewed-on: #3
2026-06-12 15:59:57 +03:00
IluaAir
074f2e9340 Update .gitignore and remove .python-version file
All checks were successful
CI / lint (pull_request) Successful in 45s
CI / test (3.10) (pull_request) Successful in 28s
CI / test (3.11) (pull_request) Successful in 42s
CI / test (3.12) (pull_request) Successful in 47s
CI / test (3.13) (pull_request) Successful in 39s
- Added .python-version to .gitignore to prevent tracking of Python version files.
- Deleted the .python-version file as it is no longer needed for the project.
2026-06-12 00:03:55 +03:00
IluaAir
dd7f117380 Add CI workflows for Gitea and GitHub
- Created a CI workflow for Gitea with linting and testing jobs using Python versions 3.10 to 3.13.
- Established a CI workflow for GitHub with similar linting and testing jobs, ensuring consistency across platforms.
- Both workflows utilize the `uv` tool for dependency management and testing, enhancing the project's CI/CD capabilities.
2026-06-11 23:57:11 +03:00
IluaAir
3c0e70b320 Update project configuration and dependencies
- Added `.vscode` to `.gitignore` to exclude Visual Studio Code settings.
- Updated `pyproject.toml` to include `ruff` for linting and configured its settings.
- Modified `uv.lock` to include `ruff` in both optional and development dependencies.
- Refactored type hints in several files to use `str | None` for optional parameters.
- Cleaned up unused imports and whitespace in various modules for improved code clarity.
2026-06-11 23:47:39 +03:00
IluaAir
c40cae1561 Update dependencies and clean up Huawei model code
- Updated `pytest` to version 9.0.3 and `responses` to version 0.26.1 in `pyproject.toml`.
- Removed the main execution block from the Huawei model for cleaner code structure.
2026-06-11 23:22:45 +03:00
39 changed files with 278 additions and 50 deletions

50
.gitea/workflows/ci.yml Normal file
View File

@@ -0,0 +1,50 @@
name: CI
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
jobs:
lint:
runs-on: rassbery
container: catthehacker/ubuntu:act-22.04
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v6
- name: Sync dependencies
run: uv sync --group dev
- name: Ruff lint
run: uv run ruff check --output-format=github .
- name: Ruff format check
run: uv run ruff format --check .
test:
runs-on: rassbery
container: catthehacker/ubuntu:act-22.04
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
python-version: ${{ matrix.python-version }}
- name: Sync dependencies
run: uv sync --group dev
- name: Run tests
run: uv run pytest -q
ci-success:
if: always()
needs: [lint, test]
runs-on: rassbery
container: catthehacker/ubuntu:act-22.04
steps:
- name: Verify all jobs succeeded
if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}
run: exit 1

47
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,47 @@
name: CI
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v6
- name: Sync dependencies
run: uv sync --group dev
- name: Ruff lint
run: uv run ruff check --output-format=github .
- name: Ruff format check
run: uv run ruff format --check .
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
python-version: ${{ matrix.python-version }}
- name: Sync dependencies
run: uv sync --group dev
- name: Run tests
run: uv run pytest -q
ci-success:
if: always()
needs: [lint, test]
runs-on: ubuntu-latest
steps:
- name: Verify all jobs succeeded
if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}
run: exit 1

4
.gitignore vendored
View File

@@ -6,10 +6,12 @@ dist/
wheels/
*.egg-info
main.py
.coverage
# Virtual environments
.venv
.python-version
.idea
.DS_Store
.vscode
# etc files
*.txt

View File

@@ -1 +0,0 @@
3.13

View File

@@ -1,6 +1,5 @@
from .core import OxiAPI
__all__ = [
"OxiAPI",
]

View File

@@ -1,4 +1,3 @@
from typing import Optional
from requests.adapters import HTTPAdapter
from urllib3.util import Retry
@@ -6,7 +5,7 @@ from urllib3.util import Retry
class OxiAdapter(HTTPAdapter):
def __init__(
self,
timeout: Optional[int] = None,
timeout: int | None = None,
max_retries: int = 3,
*args,
**kwargs,

View File

@@ -1,6 +1,7 @@
from functools import cached_property
import json
from typing import TYPE_CHECKING, Generic, Iterator, TypeVar
from collections.abc import Iterator
from functools import cached_property
from typing import TYPE_CHECKING, Generic, TypeVar
from pydantic import BaseModel

View File

@@ -1,8 +1,8 @@
from typing import Optional
from requests import HTTPError, Session
from oxi.adapter import OxiAdapter
from oxi.exception import OxiAPIError
from .node import Node
@@ -10,8 +10,8 @@ class OxiAPI:
def __init__(
self,
url: str,
username: Optional[str] = None,
password: Optional[str] = None,
username: str | None = None,
password: str | None = None,
verify: bool = True,
):
self.base_url = url.rstrip("/")
@@ -20,8 +20,8 @@ class OxiAPI:
def __create_session(
self,
username: Optional[str] = None,
password: Optional[str] = None,
username: str | None = None,
password: str | None = None,
verify: bool = True,
) -> Session:
session = Session()

View File

@@ -1,4 +1,4 @@
from typing import TYPE_CHECKING, Optional
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from requests import HTTPError
@@ -35,7 +35,7 @@ def _looks_like_node_not_found_html(e: "HTTPError") -> bool:
class OxiAPIError(Exception):
def __init__(self, message: str, status_code: Optional[int] = None):
def __init__(self, message: str, status_code: int | None = None):
super().__init__(message)
self.status_code = status_code
self.message = message

View File

@@ -1,4 +1,4 @@
from typing import Callable, Type
from collections.abc import Callable
from .base import BaseDevice
@@ -7,7 +7,7 @@ device_registry = {}
def register_parser(
name: list[str] | str,
) -> Callable[[Type[BaseDevice]], Type[BaseDevice]]:
) -> Callable[[type[BaseDevice]], type[BaseDevice]]:
def wrapper(cls):
name_list = []
if isinstance(name, str):

View File

@@ -1,10 +1,11 @@
import xml.etree.ElementTree as ET
from abc import ABC, abstractmethod
from pathlib import Path
from ttp import ttp
from oxi.exception import OxiAPIError
from oxi.interfaces.contract import Device
import xml.etree.ElementTree as ET
from oxi.interfaces.contract import Interfaces, System, Vlans
from oxi.interfaces.contract import Device, Interfaces, System, Vlans
class BaseDevice(ABC):
@@ -40,7 +41,7 @@ class BaseDevice(ABC):
Raises:
ValueError: if raw data cannot be validated by the contract.
"""
""" # noqa: E501
return self.raw.get("vlans", [])
def interfaces(self) -> list[dict]:
@@ -52,7 +53,7 @@ class BaseDevice(ABC):
Raises:
ValueError: if raw data cannot be validated by the contract.
"""
""" # noqa: E501
return self.raw.get("interfaces", [])
def system(self) -> dict:
@@ -82,11 +83,7 @@ class BaseDevice(ABC):
def _validate_contract(self) -> dict:
if self.raw is None:
msg = (
f"Node {self.name} not found"
if self.name
else "Node not found"
)
msg = f"Node {self.name} not found" if self.name else "Node not found"
raise OxiAPIError(msg, status_code=404)
system_data = self.system()
interfaces_data = self._as_list(self.interfaces())
@@ -99,8 +96,8 @@ class BaseDevice(ABC):
if "vlans" in self._declared_sections:
if "vlans" not in self.raw:
raise ValueError(
f"{self.__class__.__name__}: template '{self.template}' declares optional group "
f"'vlans', but TTP did not return it."
f"{self.__class__.__name__}: template '{self.template}' "
f"declares optional group 'vlans', but TTP did not return it."
)
vlans_data = self._as_list(self.vlans())
result["vlans"] = [Vlans(**item) for item in vlans_data]

View File

@@ -1,4 +1,5 @@
from ipaddress import IPv4Address
from pydantic import BaseModel, ConfigDict, Field
@@ -25,6 +26,7 @@ class Interfaces(Base):
ip_address: IPv4Address | None = None
mask: int | None = None
description: str | None = None
shutdown: bool = False
class Vlans(Base):

View File

@@ -3,5 +3,5 @@ import pkgutil
package = __package__
for loader, module_name, is_pkg in pkgutil.iter_modules(__path__):
for _, module_name, _ in pkgutil.iter_modules(__path__):
importlib.import_module(f"{package}.{module_name}")

View File

@@ -9,10 +9,3 @@ class Huawei(BaseDevice):
def vlans(self) -> list[dict]:
vlan_ids = self.raw.get("vlans", {}).get("vlan_ids", [])
return [{"vlan_id": vlan} for vlan in vlan_ids]
if __name__ == "__main__":
with open("./test4.txt") as file:
data = file.read()
huawei = Huawei(data)
print(huawei.parse())

View File

@@ -1,4 +1,5 @@
from ipaddress import ip_interface
from oxi.interfaces import register_parser
from oxi.interfaces.base import BaseDevice
from oxi.interfaces.utils import decode_utf

View File

@@ -25,6 +25,7 @@ Active-image: {{ ignore }} {{ _start_ }}
<group name="interfaces">
interface {{ interface | ORPHRASE }}
shutdown {{ shutdown | set(True) }}
ip address {{ ip_address }} {{ mask | to_cidr }}
name {{ description | ORPHRASE}}
description {{ description | ORPHRASE }}

View File

@@ -23,6 +23,7 @@ default_system = {
interface {{ interface }}
description {{ description | ORPHRASE }}
ip address {{ ip_address }} {{ mask | to_cidr }}
shutdown {{ shutdown | set(True) }}
</group>
<group name="vlans">

View File

@@ -21,6 +21,7 @@ default_system = {
<group name="interfaces">
interface {{ interface }}
description {{ description | ORPHRASE }}
shutdown {{ shutdown | set(True) }}
ip address {{ ip_address }} {{ mask | to_cidr }}
</group>
<group name="vlans">

View File

@@ -28,9 +28,9 @@ add address={{ ip_address | _start_ }}/{{ mask }} interface={{ name }} network={
## not disabled and comment with/without quotes
add address={{ ip_address | _start_ }}/{{ mask }} comment={{ description | ORPHRASE | exclude("disabled=") | strip('"')}} interface={{ name }} network={{ network }}
## disabled no comment
add address={{ ip_address | _start_ }}/{{ mask }} disabled={{ disabled | replace("yes","True") | strip('"')}} interface={{ name }} network={{ network }}
add address={{ ip_address | _start_ }}/{{ mask }} disabled={{ shutdown | replace("yes","True") | strip('"')}} interface={{ name }} network={{ network }}
## disabled with comment with/without quotes
add address={{ ip_address | _start_ }}/{{ mask }} comment={{ description | ORPHRASE | exclude("disabled=") | strip('"') }} disabled={{ disabled }} interface={{ name }} network={{ network }}
add address={{ ip_address | _start_ }}/{{ mask }} comment={{ description | ORPHRASE | exclude("disabled=") | strip('"') }} disabled={{ shutdown | replace("yes","True") | strip('"')}} interface={{ name }} network={{ network }}
</group>
<group name="vlans">

View File

@@ -29,6 +29,7 @@ default_system = {
<group name="interfaces">
interface {{ interface | ORPHRASE }}
shutdown {{ shutdown | set(True) }}
description {{ description | ORPHRASE }}
ip address {{ ip_address }} {{ mask | to_cidr }}
</group>

View File

@@ -6,7 +6,6 @@ from oxi.exception import OxiAPIError
from .view import NodeView
if TYPE_CHECKING:
from requests import Session

View File

@@ -3,7 +3,6 @@ from typing import TYPE_CHECKING
from .conf import NodeConfig
if TYPE_CHECKING:
from requests import Session

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "oxipy"
version = "0.1.0"
version = "0.1.3"
description = "Python client for Oxidized API with TTP-based config parsing"
readme = "README.md"
license = "Apache-2.0"
@@ -22,8 +22,8 @@ dependencies = [
[project.optional-dependencies]
dev = [
"pytest>=8",
"responses>=0.25",
"pytest>=9.0.3",
"responses>=0.26.1",
]
[project.urls]
@@ -47,4 +47,12 @@ include-package-data = true
dev = [
"pytest>=9.0.3",
"responses>=0.26.1",
"ruff>=0.15.17",
]
[tool.ruff]
target-version = "py310"
line-length = 88
[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B"]

View File

@@ -7,30 +7,35 @@
"interfaces": [
{
"interface": "TenGigabitEthernet1/0/2",
"shutdown": true,
"ip_address": null,
"mask": null,
"description": "FREE"
},
{
"interface": "TenGigabitEthernet1/0/11",
"shutdown": true,
"ip_address": null,
"mask": null,
"description": "FREE"
},
{
"interface": "vlan 1700",
"shutdown": false,
"ip_address": "13.36.8.1",
"mask": 24,
"description": "sw-test_HW"
},
{
"interface": "vlan 666",
"shutdown": false,
"ip_address": null,
"mask": null,
"description": "test"
},
{
"interface": "vlan 777",
"shutdown": false,
"ip_address": null,
"mask": null,
"description": "test2"

View File

@@ -152,6 +152,7 @@ interface HundredGigE1/0/3
port link-type trunk
undo port trunk permit vlan 1
port trunk permit vlan 221 1112 to 1116 1512
shutdown
storm-constrain broadcast pps 1100 1000
storm-constrain multicast pps 1100 1000
storm-constrain control shutdown

View File

@@ -7,72 +7,84 @@
"interfaces": [
{
"interface": "Bridge-Aggregation1",
"shutdown": false,
"ip_address": null,
"mask": null,
"description": "HW_TEST_1"
},
{
"interface": "Bridge-Aggregation2",
"shutdown": false,
"ip_address": null,
"mask": null,
"description": "HW_TEST_2"
},
{
"interface": "Bridge-Aggregation3",
"shutdown": false,
"ip_address": null,
"mask": null,
"description": "HW_TEST_3"
},
{
"interface": "Bridge-Aggregation4",
"shutdown": false,
"ip_address": null,
"mask": null,
"description": "HW_TEST_4"
},
{
"interface": "NULL0",
"shutdown": false,
"ip_address": null,
"mask": null,
"description": null
},
{
"interface": "Vlan-interface1",
"shutdown": false,
"ip_address": null,
"mask": null,
"description": null
},
{
"interface": "Vlan-interface12",
"shutdown": false,
"ip_address": "15.12.16.246",
"mask": 30,
"description": "BGP to OSPF1"
},
{
"interface": "Vlan-interface3000",
"shutdown": false,
"ip_address": "192.168.19.254",
"mask": 25,
"description": "L3 to HW_TEST_3"
},
{
"interface": "HundredGigE1/0/3",
"shutdown": true,
"ip_address": null,
"mask": null,
"description": "HW_TEST_1"
},
{
"interface": "HundredGigE1/0/63",
"shutdown": false,
"ip_address": null,
"mask": null,
"description": "HW_TEST_2"
},
{
"interface": "M-GigabitEthernet0/0/0",
"shutdown": false,
"ip_address": "192.168.10.101",
"mask": 24,
"description": null
},
{
"interface": "M-GigabitEthernet0/0/1",
"shutdown": false,
"ip_address": null,
"mask": null,
"description": null

View File

@@ -30,6 +30,17 @@ interface GigabitEthernet0/0/33
storm-control multicast min-rate 1000 max-rate 2000
storm-control action error-down
#
interface GigabitEthernet1/0/43
description FREE
shutdown
port link-type access
port default vlan 102
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

View File

@@ -7,12 +7,21 @@
"interfaces": [
{
"interface": "GigabitEthernet0/0/33",
"shutdown": false,
"ip_address": null,
"mask": null,
"description": null
},
{
"interface": "GigabitEthernet1/0/43",
"shutdown": true,
"ip_address": null,
"mask": null,
"description": "FREE"
},
{
"interface": "Vlanif120",
"shutdown": false,
"ip_address": "10.26.196.254",
"mask": 24,
"description": "SSH"

View File

@@ -7,138 +7,161 @@
"interfaces": [
{
"interface": "GigabitEthernet0",
"shutdown": false,
"ip_address": null,
"mask": null,
"description": null
},
{
"interface": "GigabitEthernet0/1",
"shutdown": false,
"ip_address": null,
"mask": null,
"description": null
},
{
"interface": "GigabitEthernet0/2",
"shutdown": false,
"ip_address": null,
"mask": null,
"description": null
},
{
"interface": "GigabitEthernet0/3",
"shutdown": false,
"ip_address": null,
"mask": null,
"description": null
},
{
"interface": "GigabitEthernet0/0",
"shutdown": false,
"ip_address": null,
"mask": null,
"description": null
},
{
"interface": "WifiMaster0",
"shutdown": false,
"ip_address": null,
"mask": null,
"description": null
},
{
"interface": "WifiMaster0/AccessPoint0",
"shutdown": false,
"ip_address": null,
"mask": null,
"description": null
},
{
"interface": "WifiMaster0/AccessPoint1",
"shutdown": true,
"ip_address": null,
"mask": null,
"description": null
},
{
"interface": "WifiMaster0/AccessPoint2",
"shutdown": true,
"ip_address": null,
"mask": null,
"description": null
},
{
"interface": "WifiMaster0/WifiStation0",
"shutdown": true,
"ip_address": null,
"mask": null,
"description": null
},
{
"interface": "WifiMaster1",
"shutdown": false,
"ip_address": null,
"mask": null,
"description": null
},
{
"interface": "WifiMaster1/AccessPoint0",
"shutdown": false,
"ip_address": null,
"mask": null,
"description": null
},
{
"interface": "WifiMaster1/AccessPoint1",
"shutdown": true,
"ip_address": null,
"mask": null,
"description": null
},
{
"interface": "WifiMaster1/AccessPoint2",
"shutdown": true,
"ip_address": null,
"mask": null,
"description": null
},
{
"interface": "WifiMaster1/WifiStation0",
"shutdown": true,
"ip_address": null,
"mask": null,
"description": null
},
{
"interface": "Bridge0",
"shutdown": false,
"ip_address": "17.36.1.1",
"mask": 24,
"description": "Home network"
},
{
"interface": "Bridge1",
"shutdown": true,
"ip_address": "10.1.30.1",
"mask": 24,
"description": "Guest network"
},
{
"interface": "Bridge2",
"shutdown": false,
"ip_address": null,
"mask": null,
"description": null
},
{
"interface": "OpenVPN0",
"shutdown": false,
"ip_address": null,
"mask": null,
"description": "test_HW-udp"
},
{
"interface": "OpenVPN2",
"shutdown": true,
"ip_address": null,
"mask": null,
"description": "test_HW-tcp"
},
{
"interface": "Wireguard0",
"shutdown": false,
"ip_address": "10.3.100.1",
"mask": 24,
"description": "test_HW"
},
{
"interface": "Wireguard1",
"shutdown": false,
"ip_address": "10.1.100.1",
"mask": 24,
"description": "test_HW"
},
{
"interface": "Wireguard2",
"shutdown": false,
"ip_address": "10.2.100.1",
"mask": 24,
"description": "test_HW"

View File

@@ -7,24 +7,28 @@
"interfaces": [
{
"interface": "bridge.LAN",
"shutdown": false,
"ip_address": "172.16.3.1",
"mask": 24,
"description": null
},
{
"interface": "ether1",
"shutdown": false,
"ip_address": "10.38.3.245",
"mask": 24,
"description": null
},
{
"interface": "wireguard2",
"shutdown": false,
"ip_address": "10.1.100.2",
"mask": 24,
"description": null
},
{
"interface": "ether4",
"shutdown": true,
"ip_address": "100.10.10.1",
"mask": 24,
"description": null

View File

@@ -7,18 +7,21 @@
"interfaces": [
{
"interface": "GigaEthernet1/0/9",
"shutdown": true,
"ip_address": null,
"mask": null,
"description": "FREE"
},
{
"interface": "VLAN1",
"shutdown": false,
"ip_address": "192.168.0.1",
"mask": 16,
"description": null
},
{
"interface": "VLAN1002",
"shutdown": false,
"ip_address": "13.36.8.1",
"mask": 24,
"description": "test-1002"

View File

@@ -7,18 +7,21 @@
"interfaces": [
{
"interface": "GigaEthernet1/0/9",
"shutdown": true,
"ip_address": null,
"mask": null,
"description": "FREE"
},
{
"interface": "VLAN1",
"shutdown": false,
"ip_address": "192.168.0.1",
"mask": 16,
"description": null
},
{
"interface": "VLAN1002",
"shutdown": false,
"ip_address": "13.36.8.1",
"mask": 24,
"description": "test-1002"

View File

@@ -7,48 +7,56 @@
"interfaces": [
{
"interface": "7/4",
"shutdown": false,
"ip_address": null,
"mask": null,
"description": "IN DWDM / OUT TEST_HW_08_N0_p1"
},
{
"interface": "8/1",
"shutdown": false,
"ip_address": null,
"mask": null,
"description": "IN DWDM / OUT TEST_HW_09_N1_p0"
},
{
"interface": "8/2",
"shutdown": false,
"ip_address": null,
"mask": null,
"description": "IN DWDM / OUT TEST_HW_09_N0_p0"
},
{
"interface": "8/3",
"shutdown": false,
"ip_address": null,
"mask": null,
"description": "IN DWDM / OUT TEST_HW_10_N1_p0"
},
{
"interface": "8/4",
"shutdown": false,
"ip_address": null,
"mask": null,
"description": "IN DWDM / OUT TEST_HW_10_N0_p0"
},
{
"interface": "9/1",
"shutdown": false,
"ip_address": null,
"mask": null,
"description": "IN DWDM / OUT TEST_HW_11_N0_p0"
},
{
"interface": "9/2",
"shutdown": false,
"ip_address": null,
"mask": null,
"description": "IN DWDM / OUT TEST_HW_11_N0_p1"
},
{
"interface": "ethernet",
"shutdown": false,
"ip_address": "25.25.1.221",
"mask": 24,
"description": null

View File

@@ -7,84 +7,98 @@
"interfaces": [
{
"interface": "1/1",
"shutdown": false,
"ip_address": null,
"mask": null,
"description": "TEST_HW_1_1"
},
{
"interface": "1/3",
"shutdown": false,
"ip_address": null,
"mask": null,
"description": "TEST_HW_1_3"
},
{
"interface": "2/1",
"shutdown": false,
"ip_address": null,
"mask": null,
"description": "TEST_HW_2_1"
},
{
"interface": "2/2",
"shutdown": false,
"ip_address": null,
"mask": null,
"description": "TEST_HW_2_2"
},
{
"interface": "2/3",
"shutdown": false,
"ip_address": null,
"mask": null,
"description": "TEST_HW_2_3"
},
{
"interface": "2/4",
"shutdown": false,
"ip_address": null,
"mask": null,
"description": "TEST_HW_2_4"
},
{
"interface": "3/1",
"shutdown": false,
"ip_address": null,
"mask": null,
"description": "TEST_HW_3_1"
},
{
"interface": "3/3",
"shutdown": false,
"ip_address": null,
"mask": null,
"description": "TEST_HW_3_3"
},
{
"interface": "3/4",
"shutdown": false,
"ip_address": null,
"mask": null,
"description": "TEST_HW_3_4"
},
{
"interface": "4/1",
"shutdown": false,
"ip_address": null,
"mask": null,
"description": "TEST_HW_4_1"
},
{
"interface": "4/2",
"shutdown": false,
"ip_address": null,
"mask": null,
"description": "TEST_HW_4_2"
},
{
"interface": "4/3",
"shutdown": false,
"ip_address": null,
"mask": null,
"description": "TEST_HW_4_3"
},
{
"interface": "4/4",
"shutdown": false,
"ip_address": null,
"mask": null,
"description": "TEST_HW_4_4"
},
{
"interface": "ethernet",
"shutdown": false,
"ip_address": "25.25.18.19",
"mask": 24,
"description": null

View File

@@ -1,8 +1,8 @@
import json
import pytest
from conftest import FIXTURES, load
from oxi.interfaces import device_registry
MODEL_CASES = [

View File

@@ -1,7 +1,7 @@
import pytest
import responses
from conftest import load
from oxi import OxiAPI
from oxi.exception import OxiAPIError
@@ -84,4 +84,4 @@ def test_unknown_model_raises_value_error():
api = OxiAPI(url=BASE)
with pytest.raises(ValueError, match="not found in registry"):
api.node("HQ").config
_ = api.node("HQ").config

View File

@@ -1,10 +1,9 @@
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 decode_utf, expand_vlan_range

38
uv.lock generated
View File

@@ -158,23 +158,34 @@ dependencies = [
{ name = "ttp" },
]
[package.dev-dependencies]
[package.optional-dependencies]
dev = [
{ name = "pytest" },
{ name = "responses" },
]
[package.dev-dependencies]
dev = [
{ name = "pytest" },
{ name = "responses" },
{ name = "ruff" },
]
[package.metadata]
requires-dist = [
{ name = "pydantic", specifier = ">=2.12.5" },
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.3" },
{ name = "requests", specifier = ">=2.32.5" },
{ name = "responses", marker = "extra == 'dev'", specifier = ">=0.26.1" },
{ name = "ttp", specifier = ">=0.10.0" },
]
provides-extras = ["dev"]
[package.metadata.requires-dev]
dev = [
{ name = "pytest", specifier = ">=9.0.3" },
{ name = "responses", specifier = ">=0.26.1" },
{ name = "ruff", specifier = ">=0.15.17" },
]
[[package]]
@@ -448,6 +459,31 @@ 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 = "ruff"
version = "0.15.17"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8c/a9/3abdf488f1bf3d24c699415e454ed554a6350d5d89ce183be1ee0a3361ac/ruff-0.15.17.tar.gz", hash = "sha256:2ec446937fd16c8c4de2674a209cc5af64d9c6f17d21fbf1151054fa0bcf5219", size = 4743346, upload-time = "2026-06-11T17:54:47.663Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/db/4d/e11259f5da07cb6afb2d074c31bf09da9671993f7329d4f15d2fdc458301/ruff-0.15.17-py3-none-linux_armv6l.whl", hash = "sha256:d9feddb927fc68bd295f5eebc587a7e42cfaf9b65f60ca4a2386febff575da8f", size = 10856677, upload-time = "2026-06-11T17:54:49.533Z" },
{ url = "https://files.pythonhosted.org/packages/29/3e/772d679e1a0dc058e58875bd2c0cb713a0530877b4a76fee3c7966df0d49/ruff-0.15.17-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:25805a226d741c47d274a35ad5c10a7dde175fcddfa511d7cf3da0a21eb3eab7", size = 11223443, upload-time = "2026-06-11T17:55:00.573Z" },
{ url = "https://files.pythonhosted.org/packages/68/58/bd41f7688b2fd5623012605130ed70e60aa7f2244baa3d5066bdd61530c8/ruff-0.15.17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f6ad73b14c2d18a3bf8ad7cb6974294d7f613a7898604826058e6ac64918ef4d", size = 10566458, upload-time = "2026-06-11T17:55:07.52Z" },
{ url = "https://files.pythonhosted.org/packages/d8/5b/733371013fcf1ec339e477ece6ab42bfe10bdd9bba8ee88a9516aa56bfc0/ruff-0.15.17-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ba0c1e4f95bcb3869d0d30cbd5917071ef2e28665abfec970cdab0492c713ed", size = 10914483, upload-time = "2026-06-11T17:55:05.501Z" },
{ url = "https://files.pythonhosted.org/packages/bd/cc/6f24251cc0252f7239391ccb85833f320efad14ebe5b443943f37ced6332/ruff-0.15.17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:81647960f10bff57d2e51cadd0c3950fe598400c852863a038720ef5b8cca91e", size = 10647497, upload-time = "2026-06-11T17:54:57.733Z" },
{ url = "https://files.pythonhosted.org/packages/68/dd/0d10c17ce1a1624d6fc3156309c3f834fdb5dfaad026ec90c85684f3990e/ruff-0.15.17-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e01a84ddbc8c16c23055ba3924476850f1bbc1917cebbb9376665a63e74260d", size = 11416967, upload-time = "2026-06-11T17:54:51.461Z" },
{ url = "https://files.pythonhosted.org/packages/2f/91/556bfb156f6144f355e831c23db00b2fc4120f86b3ce81cc5f7fd2df51f3/ruff-0.15.17-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fe9f653152f8f294f9f7e03bf3a453d8b4a27f7a59c78c8666167f2b17b96c", size = 12335770, upload-time = "2026-06-11T17:54:45.793Z" },
{ url = "https://files.pythonhosted.org/packages/88/82/8b5999aa13355e926f06d9f42a32dcca862f623bf0363785ff89d607dffd/ruff-0.15.17-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c0fe88a7676e7a05b73174d4d4a59cb2ac21ff8263583f87a81a6018475a978", size = 11575441, upload-time = "2026-06-11T17:54:32.661Z" },
{ url = "https://files.pythonhosted.org/packages/11/93/f10377bb04109ca0e8cbc483ff1982c54b6d418210041776f93e8cdc7fa9/ruff-0.15.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecfc3c7878fff94633ab0348524e093f9ce3243080416dd7d14f8ba400174719", size = 11557614, upload-time = "2026-06-11T17:54:34.698Z" },
{ url = "https://files.pythonhosted.org/packages/c7/a6/eeeae7f7d5493df41649ab3db92f086b2d0a30199e4efdf8e3dd7a033f24/ruff-0.15.17-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:b8461180b22420b1bdc289909410930761629fddf2a5aaf60fae1ab26cedc4c4", size = 11544450, upload-time = "2026-06-11T17:54:39.042Z" },
{ url = "https://files.pythonhosted.org/packages/32/88/5991ce565129a24dd4a00db1254b3b5db2e53018cbe4018ea5a89738e727/ruff-0.15.17-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6eccbe50a038b503e7140b441aa9c7fc8c1f36edf23ebef9f4165c2f28f568b7", size = 10892524, upload-time = "2026-06-11T17:55:09.432Z" },
{ url = "https://files.pythonhosted.org/packages/f5/1d/0fdd248313425f55223968af04b0a42125466a8d88d21c1d99c6af0a51e8/ruff-0.15.17-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:382fc0521025f5a8ad447d8bdd523545d0d7646adb718eb1c2dac5065ec27c0f", size = 10659573, upload-time = "2026-06-11T17:54:36.824Z" },
{ url = "https://files.pythonhosted.org/packages/9e/0e/072e8260deb9461062ce9311ced27a8e541229a6ffd483013dd37661e43e/ruff-0.15.17-py3-none-musllinux_1_2_i686.whl", hash = "sha256:456d41fcd1b2777ad63f09a6e7121d43f7b688bbc76a800c10f7f8fb1f912c3f", size = 11127818, upload-time = "2026-06-11T17:55:03.124Z" },
{ url = "https://files.pythonhosted.org/packages/ab/b4/55060a34163121498014696b5f656db5b8c6963768f227dbf0d76b311073/ruff-0.15.17-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b1a04bcc94ae6194e9db05d16ad31f298a7194bfbcb08258bbe589cee1d587b8", size = 11655901, upload-time = "2026-06-11T17:54:53.562Z" },
{ url = "https://files.pythonhosted.org/packages/49/71/9b29d6b87cef468d697f43c6a91e3fae4a80185779d7d5a4ef27d173439f/ruff-0.15.17-py3-none-win32.whl", hash = "sha256:596065960ab1ff593f744220c9fe6580eda00a95003cffa9f4048bb5b1bf0392", size = 10925574, upload-time = "2026-06-11T17:54:55.723Z" },
{ url = "https://files.pythonhosted.org/packages/3d/b2/8fc77f3723228836fa5d12497eb71c808f83782e10d058d2b15cfa14640b/ruff-0.15.17-py3-none-win_amd64.whl", hash = "sha256:6769e5fa1710b179b92e0bfa5a51735b35baea9013dadb06d5f44cbcf9547084", size = 12058788, upload-time = "2026-06-11T17:54:41.042Z" },
{ url = "https://files.pythonhosted.org/packages/2d/c7/c53e8dbff9c9dc4b7928773421ae294a5d28fcb8dcda1a089579d3a7e510/ruff-0.15.17-py3-none-win_arm64.whl", hash = "sha256:f3be1fbb34bcdfd146240d8fb92a709d4c2c8191348580a3c044ec60fa0b4456", size = 11355275, upload-time = "2026-06-11T17:54:43.635Z" },
]
[[package]]
name = "tomli"
version = "2.4.1"