From e8c33b0e647710a35d41a9a4d222f0ae1c9ab32c Mon Sep 17 00:00:00 2001 From: IluaAir Date: Mon, 25 May 2026 15:41:50 +0300 Subject: [PATCH 1/4] Add VLAN range expansion utility and improve VLAN processing in Qtech model - Introduced a new helper function `_expand_vlan_range` to convert VLAN range strings into a list of individual VLAN IDs. - Enhanced the `vlans` method in the `Qtech` class to utilize the new function, improving the handling of VLAN IDs and ensuring proper processing of both individual and range-based VLAN inputs. --- oxi/interfaces/models/qtech.py | 58 ++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/oxi/interfaces/models/qtech.py b/oxi/interfaces/models/qtech.py index ab62ba8..605fb9d 100644 --- a/oxi/interfaces/models/qtech.py +++ b/oxi/interfaces/models/qtech.py @@ -3,6 +3,30 @@ from oxi.interfaces import register_parser from oxi.interfaces.base import BaseDevice +def _expand_vlan_range(value: str) -> list[str]: + """Разворачивает строку вида '1,7,14-15,200-205' в список ['1','7','14','15',...].""" + 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 + + @register_parser(["QTECH"]) class Qtech(BaseDevice): template = "qtech.ttp" @@ -13,22 +37,23 @@ class Qtech(BaseDevice): def vlans(self) -> list[dict]: vlans_ttp = self.raw["vlans"] - vlans = [] - named_vlan = set() + vlans: list[dict] = [] + named_vlan: set[str] = set() for item in vlans_ttp: - if item.get("vlan_id"): - named_vlan.add(item.get("vlan_id")) + vlan_id = item.get("vlan_id") + if vlan_id and "," not in vlan_id and "-" not in vlan_id: + named_vlan.add(vlan_id) vlans.append(item) - else: - ids = item.get("vlan_ids", "") - tail = item.get("vlan_tail") - if tail: - ids = f"{ids},{tail}" - for vid in ids.split(","): - vid = vid.strip() - if vid in named_vlan: - continue - vlans.append({"vlan_id": vid}) + continue + + ids = item.get("vlan_ids") or vlan_id or "" + tail = item.get("vlan_tail") + if tail: + ids = f"{ids},{tail}" + for vid in _expand_vlan_range(ids): + if vid in named_vlan: + continue + vlans.append({"vlan_id": vid}) return vlans @@ -39,3 +64,8 @@ if __name__ == "__main__": 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) From 41c4cc48e9393029065d465224ca7368d36953ec Mon Sep 17 00:00:00 2001 From: IluaAir Date: Mon, 25 May 2026 16:01:38 +0300 Subject: [PATCH 2/4] Update project description and enhance documentation for clarity - Revised the project description in `pyproject.toml` to better reflect the functionality of the `oxipy` client. - Improved the README.md by adding detailed explanations of the project structure, installation instructions, and usage examples. - Updated documentation files to enhance clarity and organization, including sections on extending models and writing TTP templates. - Adjusted various TTP templates to ensure consistency and accuracy in the parsing of device configurations. --- README.md | 294 ++++++++---------- docs/extending-models.md | 209 ++++++------- docs/templates.md | 289 +++++++++-------- oxi/interfaces/base.py | 25 +- oxi/interfaces/models/eltex.py | 53 +++- oxi/interfaces/models/h3c.py | 12 +- oxi/interfaces/models/huawei.py | 4 +- oxi/interfaces/models/qtech.py | 17 +- oxi/interfaces/models/quasar.py | 7 +- oxi/interfaces/models/templates/_template.ttp | 45 +-- oxi/interfaces/models/templates/eltex.ttp | 41 +-- oxi/interfaces/models/templates/h3c.ttp | 40 +-- oxi/interfaces/models/templates/huawei.ttp | 39 +-- oxi/interfaces/models/templates/qtech.ttp | 9 + oxi/interfaces/models/templates/quasar.ttp | 41 +-- pyproject.toml | 2 +- uv.lock | 39 ++- 17 files changed, 524 insertions(+), 642 deletions(-) diff --git a/README.md b/README.md index 92028b5..e065633 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,42 @@ # oxipy -Python-клиент для работы с Oxidized API — системой управления конфигурацией сетевых устройств. Предоставляет удобный интерфейс для получения конфигураций узлов, их парсинга и работы с результатами. +`oxipy` is a Python client for the [Oxidized](https://github.com/ytti/oxidized) API. +It fetches device configurations from Oxidized and parses them into structured +Pydantic models using bundled TTP templates. -## Содержание +Oxidized remains responsible for collecting and storing configuration backups. +`oxipy` focuses on consuming those backups from Python code and exposing common +configuration sections such as system data, interfaces, and VLANs. -- [Установка](#установка) -- [Быстрый старт](#быстрый-старт) +## Contents + +- [Installation](#installation) +- [Quick Start](#quick-start) - [API Reference](#api-reference) - [OxiAPI](#oxiapi) - [NodeView](#nodeview) - [NodeConfig](#nodeconfig) - [ModelView](#modelview) -- [Поддерживаемые устройства](#поддерживаемые-устройства) -- [Дополнительно](#дополнительно) +- [Supported Devices](#supported-devices) +- [Additional Documentation](#additional-documentation) ---- +## Installation -## Установка +The package is distributed through a private Gitea Package Registry and from the +source repository. It is not published to PyPI. -> Пакет распространяется через Gitea Package Registry и исходники репозитория. -> В PyPI пакет не публикуется. +**Requirements:** Python 3.10+ -**Требования:** Python 3.11+ +### From Gitea Package Registry -### Из Gitea Package Registry - -Добавьте registry в конфигурацию pip и установите пакет: +Install the package by pointing `pip` to the private registry: ```bash pip install oxipy \ --index-url https://gitea.imbastark.ru/api/packages/Netbox/pypi/simple/ ``` -Или пропишите registry постоянно в `pip.conf` / `pip.ini`, чтобы не указывать `--index-url` каждый раз: +You can also configure the registry permanently in `pip.conf` or `pip.ini`: ```ini # ~/.config/pip/pip.conf (Linux/macOS) @@ -42,35 +46,35 @@ pip install oxipy \ extra-index-url = https://gitea.imbastark.ru/api/packages/Netbox/pypi/simple/ ``` -После этого достаточно: +After that, install normally: ```bash pip install oxipy ``` -Если registry требует аутентификации, передайте токен: +If the registry requires authentication, pass a token in the index URL: ```bash pip install oxipy \ --index-url https://__token__:@gitea.imbastark.ru/api/packages/Netbox/pypi/simple/ ``` -### Из репозитория Gitea +### From Gitea Source -Установка напрямую через pip без клонирования: +Install directly from the repository: ```bash pip install git+https://gitea.imbastark.ru/Netbox/oxipy.git ``` -Конкретный тег или ветка: +Install a specific tag or branch: ```bash pip install git+https://gitea.imbastark.ru/Netbox/oxipy.git@v0.1.0 pip install git+https://gitea.imbastark.ru/Netbox/oxipy.git@dev ``` -Для разработки (editable install): +For local development: ```bash git clone https://gitea.imbastark.ru/Netbox/oxipy @@ -78,9 +82,7 @@ cd oxipy pip install -e . ``` ---- - -## Быстрый старт +## Quick Start ```python from oxi import OxiAPI @@ -89,39 +91,39 @@ api = OxiAPI(url="https://oxi.example.com", verify=False) node = api.node("Router_HOME") -print(node.ip) -print(node.model) -print(node.full_name) - ->>> 192.168.1.1 ->>> keenetic ->>> router/HQ +print(node.ip) +print(node.model) +print(node.full_name) print(node.config.system.model) -print(node.config.interfaces.json()) -print(node.config.vlans.json()) - ->>> Sprinter (KN-3710) ->>> -[ - {"name":"Bridge1","ip_address":"192.168.1.1","mask":24,"description":"\"Guest network\""}, - {"name":"Bridge0","ip_address":"172.16.1.1","mask":24,"description":"\"Home network\""} -] ->>> -[ - {"vlan_id":1,"name":"Home VLAN"}, - {"vlan_id":2,"name":"Подключение Ethernet"}, - {"vlan_id":3,"name":"Home network"} -] +print(node.config.interfaces.dump_json()) +print(node.config.vlans.dump_json()) ``` ---- +Example output: + +```text +192.168.1.1 +keenetic +router/HQ +Sprinter (KN-3710) +[ + {"interface": "Bridge1", "ip_address": "192.168.1.1", "mask": 24, "description": "Guest network"}, + {"interface": "Bridge0", "ip_address": "172.16.1.1", "mask": 24, "description": "Home network"} +] +[ + {"vlan_id": 1, "description": "Home VLAN"}, + {"vlan_id": 2, "description": "Ethernet uplink"}, + {"vlan_id": 3, "description": "Home network"} +] +``` ## API Reference ### OxiAPI -Точка входа. Управляет HTTP-сессией и предоставляет доступ к узлам. +`OxiAPI` is the entry point. It manages the HTTP session and provides access to +Oxidized nodes. ```python OxiAPI( @@ -132,61 +134,54 @@ OxiAPI( ) ``` +| Parameter | Type | Description | +| --- | --- | --- | +| `url` | `str` | Base URL of the Oxidized API, for example `https://oxi.example.com`. | +| `username` | `str | None` | Optional username for HTTP basic authentication. | +| `password` | `str | None` | Optional password for HTTP basic authentication. | +| `verify` | `bool` | Whether to verify TLS certificates. Defaults to `True`. | -| Параметр | Тип | Описание | -| ---------- | ------ | --------------------------------------------------------- | -| `url` | `str` | Базовый URL Oxi API, например `https://oxi.example.com` | -| `username` | `str` | Имя пользователя для базовой аутентификации (опционально) | -| `password` | `str` | Пароль для базовой аутентификации (опционально) | -| `verify` | `bool` | Проверять SSL-сертификат. `True` по умолчанию | - - -**Пример:** +Example: ```python -# Без аутентификации +# Without authentication api = OxiAPI(url="https://oxi.example.com") -# С базовой аутентификацией +# With HTTP basic authentication api = OxiAPI( url="https://oxi.example.com", username="admin", password="secret", ) -# Использование как контекстного менеджера (автоматически закрывает сессию) +# As a context manager. The HTTP session is closed automatically. with OxiAPI(url="https://oxi.example.com") as api: node = api.node("HQ") print(node.ip) - ->>> 192.168.1.1 ``` #### `api.node(name)` -Возвращает `[NodeView](#nodeview)` для указанного узла. +Returns a `NodeView` for the requested Oxidized node. ```python node = api.node("HQ") ``` ---- - ### NodeView -Представление узла сети. Содержит метаданные и ленивый доступ к конфигурации. +`NodeView` represents one network device. It contains metadata returned by +Oxidized and lazy access to the fetched configuration. +| Property | Type | Description | +| --- | --- | --- | +| `ip` | `str` | Node IP address. | +| `full_name` | `str` | Full node name in Oxidized. | +| `group` | `str` | Oxidized group the node belongs to. | +| `model` | `str` | Device model key used to select a parser. | +| `config` | `NodeConfig` | Device configuration, fetched and parsed on first access. | -| Свойство | Тип | Описание | -| ----------- | ------------ | ---------------------------------------------------- | -| `ip` | `str` | IP-адрес узла | -| `full_name` | `str` | Полное имя узла в Oxi | -| `group` | `str` | Группа, к которой принадлежит узел | -| `model` | `str` | Модель устройства (используется для парсинга) | -| `config` | `NodeConfig` | Конфигурация узла (загружается при первом обращении) | - - -**Пример:** +Example: ```python node = api.node("HQ") @@ -194,135 +189,106 @@ node = api.node("HQ") print(node.ip) print(node.group) print(node.model) - ->>> 192.168.1.1 ->>> branch-office ->>> keenetic ``` ---- - ### NodeConfig -Загружает и парсит конфигурацию устройства. Использует TTP-шаблоны, соответствующие модели устройства. +`NodeConfig` fetches and parses a device configuration. The parser is selected +from the device registry by the node `model` value returned by Oxidized. -Доступ к секциям конфигурации осуществляется через свойства, возвращающие `[ModelView](#modelview)`. +Configuration sections are exposed through properties that return `ModelView` +objects. +| Property | Returns | Description | +| --- | --- | --- | +| `system` | `ModelView[System]` | System information. | +| `interfaces` | `ModelView[list[Interfaces]]` | Parsed interface list. | +| `vlans` | `ModelView[list[Vlans]]` | Parsed VLAN list, if the template provides VLAN data. | +| `text` | `str` | Raw configuration text fetched from Oxidized. | -| Свойство | Возвращает | Описание | -| ------------ | ----------------------------- | ---------------------------------- | -| `system` | `ModelView[System]` | Системная информация об устройстве | -| `interfaces` | `ModelView[list[Interfaces]]` | Список интерфейсов | -| `vlans` | `ModelView[list[Vlans]]` | Список VLAN (если есть) | -| `text` | `str` | Сырой текст конфигурации | - - -**Пример:** +Example: ```python cfg = node.config -# Системная информация -print(cfg.system.model) -print(cfg.system.serial_number) -print(cfg.system.version) +print(cfg.system.model) +print(cfg.system.serial_number) +print(cfg.system.version) ->>> Mikrotik RB951Ui-2nD ->>> B88C0B31117B ->>> 7.16.1 - -# Итерация по интерфейсам for iface in cfg.interfaces: print(iface.name, iface.ip_address, iface.mask) -# Индексация first_iface = cfg.interfaces[0] print(first_iface.name) - -# Количество интерфейсов print(len(cfg.interfaces)) -# JSON-дамп любой секции -print(cfg.interfaces.json()) -print(cfg.vlans.json()) -print(cfg.system.json()) +print(cfg.interfaces.dump_json()) +print(cfg.vlans.dump_json()) +print(cfg.system.dump_json()) -# Сырая конфигурация текстом print(cfg.text) ``` ---- +`NodeConfig` also provides `dump()` and `dump_json()` methods for the whole +parsed device object. ### ModelView -Обёртка над Pydantic-моделью или списком моделей. Обеспечивает сериализацию, итерацию и прозрачный доступ к атрибутам. +`ModelView` wraps either a single Pydantic model or a list of Pydantic models. +It provides serialization, iteration for list sections, and transparent access +to model attributes. +| Method / operation | Applies to | Description | +| --- | --- | --- | +| `.dump()` | single model and list | Returns a Python `dict` or `list` using aliases. | +| `.dump_json()` | single model and list | Returns a JSON string using aliases. | +| `.` | single model and list | Proxies attribute access to the wrapped model. | +| `iter(view)` | list only | Iterates over wrapped models. | +| `len(view)` | list only | Returns the number of wrapped models. | +| `view[i]` | list only | Returns an item or slice. | -| Метод / свойство | Применимо к | Описание | -| ---------------- | ------------ | ------------------------------------------------- | -| `.json()` | оба варианта | Возвращает JSON-строку (с `by_alias=True`) | -| `.` | оба варианта | Проксирует обращение к атрибутам вложенной модели | -| `iter(view)` | список | Итерация по элементам списка моделей | -| `len(view)` | список | Количество элементов в списке | -| `view[i]` | список | Получение элемента по индексу или срез | +`__iter__`, `__len__`, and `__getitem__` are available only for list-backed +sections such as `interfaces` and `vlans`. Calling them on `system` raises +`TypeError`. - -> `__iter__`, `__len__` и `__getitem__` доступны только для `interfaces` и `vlans` (они оборачивают список). Вызов этих методов на `system` (одиночная модель) вызовет `TypeError`. - -**Примеры:** +Examples: ```python -# Одиночная модель — system -view = node.config.system -print(view.json()) ->>> '{"model":"RB951Ui-2nD","serial_number":"B88C0B31117B","version":"7.12.1"}' -print(view.model) # 'RB951Ui-2nD' -print(view.serial_number) # 'B88C0B31117B' +system = node.config.system +print(system.dump_json()) +print(system.model) +print(system.serial_number) ->>> RB951Ui-2nD ->>> B88C0B31117B -# Список — interfaces interfaces = node.config.interfaces -# Итерация for iface in interfaces: print(iface.name, iface.ip_address) -# Длина -print(len(interfaces)) # 5 - -# Индексация и срезы -first = interfaces[0] -top3 = interfaces[:3] - -# JSON всего списка -print(interfaces.json()) +print(len(interfaces)) +print(interfaces[0]) +print(interfaces[:3]) +print(interfaces.dump()) ``` ---- +## Supported Devices -## Поддерживаемые устройства +Registry keys are compared with the Oxidized node `model` value +case-insensitively. +| Device | Registry keys | +| --- | --- | +| Keenetic | `ndms`, `keenetic`, `keeneticos` | +| MikroTik | `routeros`, `ros`, `mikrotik` | +| Qtech | `qtech` | +| Huawei | `huawei`, `vrp` | +| Eltex | `eltex` | +| H3C | `h3c` | +| Quasar | `qos`, `quasar` | -| Устройство | Ключи реестра | -| ---------- | -------------------------------- | -| Keenetic | `ndms`, `keenetic`, `keeneticos` | -| MikroTik | `routeros`, `ros`, `mikrotik` | -| Qtech | `qtech` | -| Huawei | `huawei`, `vrp` | -| Eltex | `eltex` | -| H3C | `h3c` | -| Quasar | `qos`, `quasar` | +You can add support for another device family by creating a new device model +and TTP template. See [Extending Device Models](docs/extending-models.md). +## Additional Documentation -Ключи реестра — это значения поля `model`, возвращаемого API для узла. Регистр не учитывается. - -Добавить поддержку нового устройства можно самостоятельно — подробнее в разделе [Расширение моделей](docs/extending-models.md). - ---- - -## Дополнительно - -- [Написание TTP-шаблонов](docs/templates.md) -- [Расширение и переопределение моделей устройств](docs/extending-models.md) - +- [Writing TTP Templates](docs/templates.md) +- [Extending Device Models](docs/extending-models.md) diff --git a/docs/extending-models.md b/docs/extending-models.md index dfe30c8..f6dd620 100644 --- a/docs/extending-models.md +++ b/docs/extending-models.md @@ -1,46 +1,49 @@ -# Расширение и переопределение моделей устройств +# Extending Device Models -oxipy предоставляет гибкий механизм расширения через наследование от `BaseDevice`. После того как TTP-шаблон разобрал конфигурацию в сырой словарь `self.raw`, данные проходят через три метода экземпляра — `system()`, `interfaces()`, `vlans()` — перед тем как попасть в контракт. Переопределяя эти методы, можно трансформировать, фильтровать и обогащать данные без изменения шаблона или контракта. +`oxipy` parses an Oxidized configuration in two stages. A TTP template first +extracts raw dictionaries from the text, then a device model normalizes those +dictionaries before Pydantic validates them against the public contract. -## Содержание +Device models extend `BaseDevice`. Override `system()`, `interfaces()`, or +`vlans()` when the raw TTP result needs vendor-specific cleanup. -- [Архитектура: путь данных](#архитектура-путь-данных) -- [Регистрация нового устройства](#регистрация-нового-устройства) -- [Переопределение методов (monkey patching)](#переопределение-методов-monkey-patching) +## Contents + +- [Data Flow](#data-flow) +- [Registering a Device](#registering-a-device) +- [Method Overrides](#method-overrides) - [interfaces()](#interfaces) - [vlans()](#vlans) - [system()](#system) -- [Полный пример: новое устройство](#полный-пример-новое-устройство) -- [Контракт: ожидаемые структуры](#контракт-ожидаемые-структуры) +- [Complete Example](#complete-example) +- [Expected Contract](#expected-contract) ---- +## Data Flow -## Архитектура: путь данных - -``` -текст конфигурации - │ - ▼ - TTP-шаблон (.ttp) - │ парсит в сырой словарь - ▼ +```text +configuration text + | + v + TTP template (.ttp) + | + v self.raw: dict - │ - ├──► system() → dict - ├──► interfaces() → list[dict] - └──► vlans() → list[dict] - │ - ▼ - _validate_contract() - │ создаёт Pydantic-модели - ▼ - Device(system, interfaces, vlans) + | + +--> system() -> dict + +--> interfaces() -> list[dict] + +--> vlans() -> list[dict] + | + v + Pydantic validation + | + v + Device(system, interfaces, vlans) ``` -Методы `system()`, `interfaces()`, `vlans()` — это точки расширения. Базовая реализация просто возвращает данные из `self.raw`: +The extension methods are intentionally small. The base implementation returns +data directly from `self.raw`: ```python -# BaseDevice (упрощённо) def interfaces(self) -> list[dict]: return self.raw.get("interfaces", []) @@ -51,18 +54,16 @@ def system(self) -> dict: return self.raw.get("system", None) ``` ---- +## Registering a Device -## Регистрация нового устройства +To add support for a new vendor: -Чтобы добавить поддержку нового вендора: - -1. Создайте файл в `oxi/interfaces/models/`, например `cisco.py`. -2. Создайте шаблон `oxi/interfaces/models/templates/cisco.ttp`. -3. Унаследуйте класс от `BaseDevice` и зарегистрируйте его декоратором `@register_parser`. +1. Create a Python file in `oxi/interfaces/models/`, for example `cisco.py`. +2. Create a template in `oxi/interfaces/models/templates/`, for example + `cisco.ttp`. +3. Subclass `BaseDevice` and register it with `@register_parser`. ```python -# oxi/interfaces/models/cisco.py from oxi.interfaces import register_parser from oxi.interfaces.base import BaseDevice @@ -72,26 +73,26 @@ class CiscoIOS(BaseDevice): template = "cisco.ttp" ``` -Декоратор `@register_parser` принимает список строк — это ключи, по которым устройство ищется в реестре. Поле `model` от API сравнивается с этими ключами без учёта регистра. +`@register_parser` accepts a string or a list of strings. These values are the +registry keys used to match the Oxidized node `model` field. Matching is +case-insensitive. -После добавления файла он автоматически импортируется через `pkgutil` при старте приложения — явно импортировать не нужно. +Model modules are imported automatically through `pkgutil` when +`oxi.interfaces` is loaded, so you do not need to import your model class +manually. ---- - -## Переопределение методов (monkey patching) +## Method Overrides ### interfaces() -Используйте переопределение, когда нужно: +Override `interfaces()` when you need to: -- Преобразовать формат IP-адреса (например, `netmask` → `prefix_length`). -- Декодировать escape-последовательности в описаниях. -- Переименовать ключи, не совпадающие с контрактом. -- Фильтровать служебные интерфейсы. +- Convert dotted decimal netmasks to prefix lengths. +- Decode escaped descriptions. +- Rename keys that do not match the contract. +- Filter service-only interfaces. -**Пример: конвертация маски подсети в префикс** - -TTP возвращает `netmask` как `255.255.255.0`, а контракт `Interfaces` ожидает `mask` как целое число (prefix length): +Example: convert a netmask to a prefix length. ```python from ipaddress import ip_interface @@ -114,19 +115,17 @@ class MyVendor(BaseDevice): return result ``` -**Пример: фильтрация служебных интерфейсов** +Example: filter management interfaces. ```python def interfaces(self) -> list[dict]: return [ item for item in self.raw.get("interfaces", []) - if not item.get("name", "").startswith("lo") + if not item.get("interface", "").startswith("Mgmt") ] ``` -**Пример: декодирование Unicode escape-последовательностей** - -Некоторые устройства (например, Keenetic) хранят кириллические описания как `\xd0\xb8\xd0\xbc\xd1\x8f`: +Example: decode escaped UTF-8 descriptions. ```python def _decode_utf(self, text: str) -> str: @@ -140,6 +139,7 @@ def _decode_utf(self, text: str) -> str: ) return text + def interfaces(self) -> list[dict]: interfaces = self.raw.get("interfaces", []) for item in interfaces: @@ -148,75 +148,83 @@ def interfaces(self) -> list[dict]: return interfaces ``` ---- - ### vlans() -Аналогично `interfaces()`. Используйте для нормализации ID, декодирования названий, обогащения данными из других секций. +Override `vlans()` to normalize VLAN IDs, expand compressed ranges, decode +names, or merge details from multiple template groups. -**Пример: добавление префикса к имени VLAN** +Example: add a generated VLAN name. ```python def vlans(self) -> list[dict]: result = [] for item in self.raw.get("vlans", []): - item["description"] = f"VLAN_{item.get('id', '?')}" + item["description"] = f"VLAN_{item.get('vlan_id', '?')}" result.append(item) return result ``` -**Пример: объединение данных из нескольких секций** +Example: merge data from another raw group. ```python def vlans(self) -> list[dict]: - vlans = {v["id"]: v for v in self.raw.get("vlans", [])} - # обогащаем данными из другой секции, если она есть + vlans = {item["vlan_id"]: item for item in self.raw.get("vlans", [])} for extra in self.raw.get("vlan_details", []): - vlan_id = extra.get("id") + vlan_id = extra.get("vlan_id") if vlan_id in vlans: vlans[vlan_id].update(extra) return list(vlans.values()) ``` ---- +Example: expand a comma-separated VLAN range. + +```python +def _expand_vlan_range(value: str) -> list[str]: + result = [] + for part in value.split(","): + if "-" not in part: + result.append(part.strip()) + continue + start, end = (int(item) for item in part.split("-", 1)) + result.extend(str(vlan_id) for vlan_id in range(start, end + 1)) + return result +``` ### system() -Переопределяйте, если структура системной секции отличается от ожидаемой контрактом, или нужно вычислить поля: +Override `system()` when the system section needs computed fields or data from +another raw group. -**Пример: собрать серийный номер из нескольких полей** +Example: assemble a serial number from two fields. ```python def system(self) -> dict: raw_system = self.raw.get("system", {}) - # Устройство возвращает серийный номер в двух частях part1 = raw_system.get("serial_part1", "") part2 = raw_system.get("serial_part2", "") raw_system["serial_number"] = f"{part1}-{part2}" return raw_system ``` -**Пример: нормализация строки версии** +Example: normalize a version string. ```python def system(self) -> dict: raw_system = self.raw.get("system", {}) - # Убираем лишнее из "7.12.1 (stable)" → "7.12.1" version = raw_system.get("version", "") raw_system["version"] = version.split()[0] if version else version return raw_system ``` ---- +## Complete Example -## Полный пример: новое устройство +Assume a Cisco IOS-like device where: -Допустим, нужно добавить поддержку Cisco IOS, где: -- IP-адрес и маска разделены пробелом в конфигурации (`ip address 10.0.0.1 255.255.255.0`). -- Описание интерфейса может содержать несколько слов. -- Серийный номер разделён дефисом в двух строках. +- IP address and netmask are separated by a space. +- Interface descriptions can contain several words. +- System fields are present in separate lines. -**Шаблон** (`oxi/interfaces/models/templates/cisco.ttp`): +Template: `oxi/interfaces/models/templates/cisco.ttp` ```xml @@ -240,12 +248,12 @@ interface {{ interface | _start_ }} -vlan {{ id | _start_ }} - name {{ description }} +vlan {{ vlan_id | _start_ }} + name {{ name | ORPHRASE }} ``` -**Класс устройства** (`oxi/interfaces/models/cisco.py`): +Device model: `oxi/interfaces/models/cisco.py` ```python from ipaddress import ip_interface @@ -260,12 +268,10 @@ class CiscoIOS(BaseDevice): def interfaces(self) -> list[dict]: result = [] for item in self.raw.get("interfaces", []): - # Конвертируем маску подсети в длину префикса if item.get("ip_address") and item.get("netmask"): iface = ip_interface(f"{item['ip_address']}/{item['netmask']}") item["mask"] = iface.network.prefixlen item.pop("netmask", None) - # Фильтруем интерфейсы управления if item.get("interface", "").startswith("Mgmt"): continue result.append(item) @@ -273,53 +279,48 @@ class CiscoIOS(BaseDevice): def system(self) -> dict: raw_system = self.raw.get("system", {}) - # Нормализуем версию: "15.2(4)M3" → оставляем как есть - # Убираем лишние пробелы в модели if raw_system.get("model"): raw_system["model"] = raw_system["model"].strip() return raw_system ``` ---- +## Expected Contract -## Контракт: ожидаемые структуры +Methods must return structures accepted by `oxi.interfaces.contract`. -Методы должны возвращать данные в следующем формате. Контракт жёстко проверяется Pydantic. - -### `system()` → `dict` +### `system() -> dict` ```python { - "model": "RB951Ui-2nD", # str, обязательно - "serial_number": "B88C0B31117B", # str, обязательно - "version": "7.12.1", # str, обязательно + "model": "RB951Ui-2nD", + "serial_number": "B88C0B31117B", + "version": "7.12.1", } ``` -### `interfaces()` → `list[dict]` +### `interfaces() -> list[dict]` ```python [ { - "interface": "ether1", # str, обязательно (alias для поля name) - "ip_address": "192.168.1.1", # str | None - "mask": 24, # int | None (длина префикса) - "description": "LAN", # str | None + "interface": "ether1", + "ip_address": "192.168.1.1", + "mask": 24, + "description": "LAN", }, - ... ] ``` -### `vlans()` → `list[dict]` +### `vlans() -> list[dict]` ```python [ { - "id": 10, # int, обязательно (alias для поля vlan_id) - "description": "MGMT", # str | None (alias для поля name) + "vlan_id": 10, + "description": "MGMT", }, - ... ] ``` -> Если имя ключа в словаре совпадает с **alias** поля Pydantic-модели, а не с именем атрибута — используйте alias. Модели сконфигурированы с `populate_by_name=True`, поэтому принимаются оба варианта. +The Pydantic models use `populate_by_name=True` for aliased models, so both +field names and aliases are accepted where aliases exist. diff --git a/docs/templates.md b/docs/templates.md index 86cf78d..3a4a23e 100644 --- a/docs/templates.md +++ b/docs/templates.md @@ -1,86 +1,86 @@ -# Написание TTP-шаблонов +# Writing TTP Templates -oxipy использует библиотеку [TTP (Template Text Parser)](https://ttp.readthedocs.io/) для парсинга конфигураций сетевых устройств в структурированные данные. Шаблоны хранятся в директории `oxi/interfaces/models/templates/`. +`oxipy` uses [TTP (Template Text Parser)](https://ttp.readthedocs.io/) to turn +network device configurations fetched from Oxidized into structured data. +Templates are stored in `oxi/interfaces/models/templates/`. -## Содержание +## Contents -- [Структура шаблона](#структура-шаблона) -- [Обязательные группы](#обязательные-группы) -- [Секция system](#секция-system) -- [Секция interfaces](#секция-interfaces) -- [Секция vlans](#секция-vlans) -- [TTP: основные возможности](#ttp-основные-возможности) -- [Переменные по умолчанию](#переменные-по-умолчанию) -- [Практические примеры](#практические-примеры) -- [Валидация шаблона](#валидация-шаблона) +- [Template Structure](#template-structure) +- [Required Groups](#required-groups) +- [The system Group](#the-system-group) +- [The interfaces Group](#the-interfaces-group) +- [The vlans Group](#the-vlans-group) +- [Useful TTP Features](#useful-ttp-features) +- [Default Variables](#default-variables) +- [Full Example](#full-example) +- [Validation](#validation) ---- +## Template Structure -## Структура шаблона - -Каждый шаблон — это `.ttp`-файл, состоящий из следующих блоков: +Each template is a `.ttp` file with a small set of conventional blocks: ```xml - Описание шаблона (опционально) + Optional template documentation. - + - + - + - + ``` -Файл-заготовка находится в `oxi/interfaces/models/templates/_template.ttp`. +Use `oxi/interfaces/models/templates/_template.ttp` as the starting point for a +new parser. ---- +## Required Groups -## Обязательные группы +The framework requires two groups in every template: -Фреймворк требует наличия в шаблоне **двух обязательных групп**: +| Group | Required | Description | +| --- | --- | --- | +| `system` | Yes | Device system information. | +| `interfaces` | Yes | Interface configuration. | +| `vlans` | No | VLAN configuration. | -| Группа | Обязательна | Описание | -|--------------|-------------|-------------------------------| -| `system` | Да | Системная информация | -| `interfaces` | Да | Конфигурация интерфейсов | -| `vlans` | Нет | Конфигурация VLAN | +If a required group is missing from the template or from the TTP result, +`BaseDevice` raises `ValueError`. -Если обязательная группа отсутствует в шаблоне или TTP не вернул её данные, будет выброшено `ValueError`. +If a template declares an optional `vlans` group, `oxipy` expects TTP to return +that group. Omit the group completely for devices where VLAN parsing is not +implemented. ---- +## The system Group -## Секция system +The `system` group must return one dictionary with these fields: -Должна возвращать словарь со следующими полями: +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `model` | `str` | Yes | Device model. | +| `serial_number` | `str` | Yes | Device serial number. | +| `version` | `str` | Yes | Firmware, software, or build version chosen by the parser. | -| Поле | Тип | Обязательное | Описание | -|-----------------|------|--------------|---------------------| -| `model` | str | Да | Модель устройства | -| `serial_number` | str | Да | Серийный номер | -| `version` | str | Да | Версия прошивки | +Example for MikroTik: -**Пример (MikroTik):** - -Конфигурация: -``` +```text # version: 7.12.1 (stable) # model = RB951Ui-2nD # serial number = B88C0B31117B ``` -Шаблон: -``` +```xml # version: {{ version }}{{ ignore('.*') }} # model = {{ model }} @@ -88,17 +88,15 @@ oxipy использует библиотеку [TTP (Template Text Parser)](htt ``` -**Пример (Keenetic):** +Example for Keenetic: -Конфигурация: -``` +```text ! release: 4.1.7.1-1 ! model: Keenetic Extra ! hw_version: F02B4E7A1C90 ``` -Шаблон: -``` +```xml ! release: {{ version }} ! model: {{ model | ORPHRASE }} @@ -106,34 +104,34 @@ oxipy использует библиотеку [TTP (Template Text Parser)](htt ``` ---- +## The interfaces Group -## Секция interfaces +The `interfaces` group must return a list of dictionaries. Each dictionary +describes one interface. -Должна возвращать список словарей. Каждый словарь описывает один интерфейс. +The `Interfaces` contract expects these fields: -Поля, которые ожидает контракт `Interfaces`: +| Contract field | TTP name / alias | Type | Required | +| --- | --- | --- | --- | +| `name` | `interface` | `str` | Yes | +| `ip_address` | `ip_address` | `IPv4Address | None` | No | +| `mask` | `mask` | `int | None` | No | +| `description` | `description` | `str | None` | No | -| Поле | TTP-имя / alias | Тип | Обязательное | -|---------------|-----------------|------------------|--------------| -| `name` | `interface` | str | Да | -| `ip_address` | `ip_address` | IPv4Address | Нет | -| `mask` | `mask` | int (prefix len) | Нет | -| `description` | `description` | str | Нет | +The Pydantic field `name` has the alias `interface`, so templates should usually +emit `interface`. You can also emit `name` because the models allow population +by field name, or you can normalize keys in the device class by overriding +`interfaces()`. -> **Важно:** поле `name` в Pydantic-модели имеет алиас `interface`, поэтому в шаблоне переменную нужно называть именно `interface` **или** переопределить метод `interfaces()` в классе модели (см. [Расширение моделей](extending-models.md)). +Example for MikroTik: -**Пример (MikroTik):** - -Конфигурация: -``` +```text /ip address add address=192.168.1.1/24 interface=ether1 network=192.168.1.0 add address=10.0.0.1/30 comment="WAN link" interface=ether2 network=10.0.0.0 ``` -Шаблон: -``` +```xml /ip address add address={{ ip_address | _start_ }}/{{ mask }} interface={{ interface }} network={{ network }} @@ -141,108 +139,104 @@ add address={{ ip_address | _start_ }}/{{ mask }} comment={{ description | ORPHR ``` -**Пример (Keenetic):** +Example for CLI-style devices: -Конфигурация: -``` -interface GigabitEthernet0/0 - description "WAN" - ip address 10.0.0.2 255.255.255.252 -interface GigabitEthernet0/1 - ip address 192.168.1.1 255.255.255.0 +```text +interface Vlanif120 + description SSH + ip address 10.26.196.254 255.255.255.0 ``` -Шаблон: -``` +```xml -interface {{ name | _start_ | exclude("Vlan") }} - description {{ description | ORPHRASE }} - ip address {{ ip_address }} {{ netmask }} +interface {{ interface | _start_ }} + description {{ description | ORPHRASE }} + ip address {{ ip_address }} {{ mask | to_cidr }} ``` -Здесь переменная называется `name`, а не `interface` — это покрывается переопределением метода `interfaces()` в классе `Keenetic`. +Use TTP's `to_cidr` formatter when the device uses dotted decimal masks. ---- +## The vlans Group -## Секция vlans +The `vlans` group is optional. If it is declared, it must return a list of VLAN +dictionaries. -Необязательная группа. Если объявлена в шаблоне, фреймворк ожидает её наличия в результате TTP. +The `Vlans` contract expects these fields: -Поля контракта `Vlans`: +| Contract field | Alias | Type | Required | +| --- | --- | --- | --- | +| `vlan_id` | none | `int` | Yes | +| `name` | `description` | `str | None` | No | -| Поле | TTP-имя / alias | Тип | Обязательное | -|-----------|-----------------|------|--------------| -| `vlan_id` | `id` | int | Да | -| `name` | `description` | str | Нет | +`name` has the alias `description`, so either key is accepted. Existing parsers +use both forms depending on the vendor format. -> `vlan_id` имеет алиас `id`, поэтому в шаблоне переменная должна называться `id` либо переименовываться в методе `vlans()`. +Example: -**Пример (Keenetic):** - -Конфигурация: -``` -interface Bridge0/Vlan10 - description "MGMT" -interface Bridge0/Vlan20 - description "SERVERS" +```text +vlan 10 + name MGMT ``` -Шаблон: -``` +```xml -interface {{ ignore }}/Vlan{{ id }} - description {{ description | ORPHRASE | strip('"') }} +vlan {{ vlan_id | _start_ }} + name {{ name | ORPHRASE }} ``` ---- +For compressed vendor syntax such as `vlan batch 101 to 103 110`, parse the raw +range in the template and normalize it in the device class when needed. -## TTP: основные возможности +## Useful TTP Features -### Маркеры строк +### Line markers -| Маркер | Описание | -|-------------|---------------------------------------------------------------| -| `_start_` | Строка с этой переменной считается началом нового совпадения | -| `_end_` | Строка с этой переменной завершает совпадение группы | +| Marker | Description | +| --- | --- | +| `_start_` | Starts a new group match from the current line. | +| `_end_` | Ends the current group match. | -``` -add address={{ ip_address | _start_ }}/{{ mask }} interface={{ name }} +```xml +interface {{ interface | _start_ }} ``` -### Модификаторы переменных +### Variable modifiers -| Модификатор | Описание | -|------------------------|-----------------------------------------------------------| -| `ORPHRASE` | Захватывает одно слово или фразу (до конца строки) | -| `exclude("pattern")` | Пропускает строку, если захваченное значение содержит паттерн | -| `strip('"')` | Удаляет символ из начала и конца захваченного значения | -| `replace("old","new")` | Заменяет подстроку в захваченном значении | -| `re("pattern")` | Принимает значение, только если оно соответствует regex | -| `ignore` | Захватывает, но игнорирует значение (не включает в результат) | -| `ignore('.*')` | Игнорирует всё до конца строки | +| Modifier | Description | +| --- | --- | +| `ORPHRASE` | Captures a word or phrase to the end of the line. | +| `exclude("pattern")` | Skips the match when the captured value contains the pattern. | +| `strip('"')` | Removes a character from both ends of the captured value. | +| `replace("old","new")` | Replaces text inside the captured value. | +| `re("pattern")` | Accepts the value only if it matches the regex. | +| `ignore` | Captures and discards the value. | +| `ignore('.*')` | Discards the rest of the line. | +| `to_cidr` | Converts a dotted decimal netmask to a prefix length. | +| `unrange("-", ",")` | Expands ranges such as `10-12` using a comma separator. | +| `split(",")` | Splits a captured string into a list. | -### Комментарии в шаблоне +### Template comments -Строки, начинающиеся с `##`, — это комментарии TTP и не влияют на парсинг: +Lines beginning with `##` are TTP comments: -``` +```xml ## disabled no comment -add address={{ ip_address | _start_ }}/{{ mask }} interface={{ name }} +add address={{ ip_address | _start_ }}/{{ mask }} interface={{ interface }} ``` ---- +## Default Variables -## Переменные по умолчанию - -Блок `` позволяет задавать значения по умолчанию для группы через атрибут `default`: +The `` block can define default values for a group through the group's +`default` attribute: ```xml default_system = { "model": "", - "serial_number": "" + "serial_number": "", + "version": "" } @@ -253,17 +247,16 @@ default_system = { ``` -Если шаблон не нашёл совпадений для группы, будет возвращён словарь из `default_system`. +If the group does not match anything, TTP returns the default dictionary. ---- +## Full Example -## Практические примеры - -### Полный шаблон для нового устройства (пример: Cisco IOS) +This simplified Cisco IOS-style example shows the expected shape of a complete +template: ```xml - Шаблон для парсинга Cisco IOS running-config +Cisco IOS running-config parser. @@ -283,24 +276,24 @@ System serial number : {{ serial_number }} interface {{ interface | _start_ }} description {{ description | ORPHRASE }} - ip address {{ ip_address }} {{ netmask }} - shutdown {{ shutdown | set("True") }} + ip address {{ ip_address }} {{ mask | to_cidr }} -vlan {{ id | _start_ }} - name {{ description }} +vlan {{ vlan_id | _start_ }} + name {{ name | ORPHRASE }} ``` ---- +## Validation -## Валидация шаблона +`BaseDevice` performs two validation passes: -Фреймворк автоматически выполняет два уровня проверки: +1. Template structure validation checks that the template declares the required + `system` and `interfaces` groups. +2. Parse result validation checks that TTP actually returned the required groups + for the given configuration. -1. **Валидация структуры шаблона** — при создании объекта устройства парсятся XML-теги `` и проверяется наличие обязательных секций (`system`, `interfaces`). - -2. **Валидация результата парсинга** — после запуска TTP проверяется, что обязательные группы действительно присутствуют в результате (т.е. конфигурация содержала соответствующие строки). - -При нарушении любого условия выбрасывается `ValueError` с подробным описанием проблемы. +After that, parsed data is validated by Pydantic models from +`oxi.interfaces.contract`. Invalid structures raise the original Pydantic +validation error. diff --git a/oxi/interfaces/base.py b/oxi/interfaces/base.py index 62610ee..0e3cbe2 100644 --- a/oxi/interfaces/base.py +++ b/oxi/interfaces/base.py @@ -24,10 +24,7 @@ class BaseDevice(ABC): @abstractmethod def template(self) -> str: """ - Expected structure: - Название файла с парсером ttp - Returns: - None + Name of the TTP template file used by this device parser. """ def vlans(self) -> list[dict]: @@ -35,14 +32,14 @@ class BaseDevice(ABC): Parse VLAN configuration from self.raw['vlans']. Expected structure: - [{"id": 10, "description": "MGMT"}, {"id": 15, "name": "SSH"}, ...] + [{"vlan_id": 10, "description": "MGMT"}, {"vlan_id": 15, "name": "SSH"}, ...] Returns: - list[Vlans]: список VLAN из секции vlans, - пустой список если секция отсутствует. + list[Vlans]: VLANs from the vlans section, or an empty list + when the section is absent. Raises: - ValueError: если raw содержит некорректные данные. + ValueError: if raw data cannot be validated by the contract. """ return self.raw.get("vlans", []) @@ -51,10 +48,10 @@ class BaseDevice(ABC): Parse Interface configuration from self.raw['interfaces']. Expected raw structure: - [{"name": "GEthernet1/0/1", "ip_address": "192.168.1.1", "mask": "24", "description": "IPBB interface"}] + [{"interface": "GEthernet1/0/1", "ip_address": "192.168.1.1", "mask": "24", "description": "IPBB interface"}] Raises: - ValueError: если raw содержит некорректные данные. + ValueError: if raw data cannot be validated by the contract. """ return self.raw.get("interfaces", []) @@ -66,7 +63,7 @@ class BaseDevice(ABC): {"model":"RB951Ui-2nD", serial_number: "B88C0B31117B", "version": "7.12.1"} Raises: - ValueError: если raw содержит некорректные данные. + ValueError: if raw data cannot be validated by the contract. """ return self.raw.get("system", None) @@ -97,14 +94,14 @@ class BaseDevice(ABC): return result def _load_template(self): - """Подгрузка темплейтов из папки models/templates""" + """Load the device TTP template from models/templates.""" path = Path(__file__).parent / "models" / "templates" / self.template if not path.exists(): raise FileNotFoundError(f"Template {self.template} not found") return path.read_text(encoding="utf-8") def _validate_template_groups(self) -> None: - """Проверяем только обязательные группы в template.""" + """Validate that the template declares all required groups.""" try: root = ET.fromstring(self._loaded_template) except ET.ParseError: @@ -122,7 +119,7 @@ class BaseDevice(ABC): ) def _run_ttp(self) -> dict: - """Основной парсер""" + """Run the node-not-found check and then parse the config with TTP.""" pattern = """node not {{found}}""" parser = ttp(data=self.config, template=pattern) parser.parse() diff --git a/oxi/interfaces/models/eltex.py b/oxi/interfaces/models/eltex.py index f4a08bf..ee59af0 100644 --- a/oxi/interfaces/models/eltex.py +++ b/oxi/interfaces/models/eltex.py @@ -2,6 +2,30 @@ 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 + + @register_parser("eltex") class Eltex(BaseDevice): template = "eltex.ttp" @@ -17,22 +41,23 @@ class Eltex(BaseDevice): def vlans(self) -> list[dict]: vlans_ttp = self.raw.get("vlans", []) - vlans = [] - named_vlan = set() + vlans: list[dict] = [] + named_vlan: set[str] = set() for item in vlans_ttp: - if item.get("vlan_id"): - named_vlan.add(item.get("vlan_id")) + vlan_id = item.get("vlan_id") + if vlan_id: + named_vlan.add(str(vlan_id)) vlans.append(item) - else: - ids = item.get("vlan_ids", "") - tail = item.get("vlan_tail") - if tail: - ids = f"{ids},{tail}" - for vid in ids: - vid = vid.strip() - if vid in named_vlan: - continue - vlans.append({"vlan_id": vid}) + continue + + ids = item.get("vlan_ids", "") + 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): + if vid in named_vlan: + continue + vlans.append({"vlan_id": vid}) return vlans diff --git a/oxi/interfaces/models/h3c.py b/oxi/interfaces/models/h3c.py index f97b209..db2444f 100644 --- a/oxi/interfaces/models/h3c.py +++ b/oxi/interfaces/models/h3c.py @@ -6,15 +6,17 @@ class H3C(BaseDevice): template = "h3c.ttp" def vlans(self) -> list[dict]: - vlan_list = self.raw["vlans"] - vlans = [] + vlan_list = self.raw.get("vlans", []) + vlans: list[dict] = [] for item in vlan_list: - if item.get("vlans_id"): - vlans.extend([{'vlan_id': vln }for vln in item.get("vlans_id")]) - else: + vlan_ids = item.get("vlans_id") + if not vlan_ids: 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() diff --git a/oxi/interfaces/models/huawei.py b/oxi/interfaces/models/huawei.py index 4d53dbb..ddb90a1 100644 --- a/oxi/interfaces/models/huawei.py +++ b/oxi/interfaces/models/huawei.py @@ -6,8 +6,8 @@ from oxi.interfaces.base import BaseDevice class Huawei(BaseDevice): template = "huawei.ttp" - def vlans(self): - vlan_ids = self.raw["vlans"].get("vlan_ids") + def vlans(self) -> list[dict]: + vlan_ids = self.raw.get("vlans", {}).get("vlan_ids", []) return [{"vlan_id": vlan} for vlan in vlan_ids] diff --git a/oxi/interfaces/models/qtech.py b/oxi/interfaces/models/qtech.py index 605fb9d..cf2247b 100644 --- a/oxi/interfaces/models/qtech.py +++ b/oxi/interfaces/models/qtech.py @@ -1,10 +1,12 @@ -import os from oxi.interfaces import register_parser from oxi.interfaces.base import BaseDevice -def _expand_vlan_range(value: str) -> list[str]: - """Разворачивает строку вида '1,7,14-15,200-205' в список ['1','7','14','15',...].""" +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 @@ -31,12 +33,8 @@ def _expand_vlan_range(value: str) -> list[str]: class Qtech(BaseDevice): template = "qtech.ttp" - def system(self) -> dict: - system = self.raw["system"] - return system - def vlans(self) -> list[dict]: - vlans_ttp = self.raw["vlans"] + vlans_ttp = self.raw.get("vlans", []) vlans: list[dict] = [] named_vlan: set[str] = set() for item in vlans_ttp: @@ -49,7 +47,7 @@ class Qtech(BaseDevice): ids = item.get("vlan_ids") or vlan_id or "" tail = item.get("vlan_tail") if tail: - ids = f"{ids},{tail}" + ids = [*ids, tail] if isinstance(ids, list) else f"{ids},{tail}" for vid in _expand_vlan_range(ids): if vid in named_vlan: continue @@ -58,7 +56,6 @@ class Qtech(BaseDevice): if __name__ == "__main__": - print(os.path.abspath(os.curdir)) with open("./test3.txt") as file: data = file.read() qtech = Qtech(data) diff --git a/oxi/interfaces/models/quasar.py b/oxi/interfaces/models/quasar.py index 3f87156..5d320f8 100644 --- a/oxi/interfaces/models/quasar.py +++ b/oxi/interfaces/models/quasar.py @@ -6,9 +6,9 @@ class Quasar(BaseDevice): template = "quasar.ttp" def interfaces(self) -> list[dict]: - ether_interfaces: dict = self.raw["interfaces"] + ether_interface: dict = self.raw.get("interfaces", {}) interfaces: list[dict] = [] - bulk_interfaces: dict = self.raw["bulkinterfaces"] + bulk_interfaces: dict = self.raw.get("bulkinterfaces", {}) for key, value in bulk_interfaces.items(): interfaces.append( { @@ -18,7 +18,8 @@ class Quasar(BaseDevice): "mask": value.get("mask"), } ) - interfaces.append(ether_interfaces) + if ether_interface: + interfaces.append(ether_interface) return interfaces diff --git a/oxi/interfaces/models/templates/_template.ttp b/oxi/interfaces/models/templates/_template.ttp index 2362b98..c582b1a 100644 --- a/oxi/interfaces/models/templates/_template.ttp +++ b/oxi/interfaces/models/templates/_template.ttp @@ -1,41 +1,20 @@ -Базовый шаблон для нового устройства. Скопируйте этот файл, переименуйте -в <vendor>.ttp и заполните группы под формат конфигурации вашего устройства. +Base template for a new device parser. Copy this file, rename it to +<vendor>.ttp, and fill the groups for the target configuration format. -Обязательные группы: system, interfaces. -Опциональная группа: vlans — добавляйте только если устройство поддерживает VLAN. +Required groups: system, interfaces. +Optional group: vlans. Add it only when VLAN parsing is implemented. ---- Группа system --- -Должна возвращать одиночный словарь с полями: - model (str) — модель устройства - serial_number (str) — серийный номер - version (str) — версия прошивки +system must return one dictionary with: model, serial_number, version. +interfaces must return a list of dictionaries with: interface, ip_address, +mask, description. Use a prefix length for mask; convert dotted decimal masks +with `to_cidr` or in the device class. +vlans must return dictionaries with vlan_id and optional name/description. ---- Группа interfaces --- -Должна возвращать список словарей. Каждый элемент: - interface (str) — имя интерфейса (alias поля name) - ip_address (str|None) — IPv4-адрес - mask (int|None) — длина префикса (напр. 24) - description (str|None) — описание интерфейса +Useful TTP modifiers: ORPHRASE, _start_, strip(), replace(), exclude(), +ignore, ignore('.*'), to_cidr, unrange(), split(). - Если устройство возвращает маску в виде 255.255.255.0, конвертируйте - её в prefix length в методе interfaces() класса устройства. - ---- Группа vlans --- -Должна возвращать список словарей. Каждый элемент: - id (int) — номер VLAN (alias поля vlan_id) - description (str|None) — название VLAN (alias поля name) - ---- Полезные модификаторы TTP --- - {{ field | ORPHRASE }} — одно слово или фраза до конца строки - {{ field | _start_ }} — начало новой записи группы - {{ field | strip('"') }} — убрать кавычки - {{ field | replace("yes","True") }} — замена подстроки - {{ field | exclude("pattern") }} — пропустить строку при совпадении - {{ ignore }} — захватить и выбросить значение - {{ ignore('.*') }} — выбросить всё до конца строки - -Подробнее: docs/templates.md +See docs/templates.md for details. default_system = { diff --git a/oxi/interfaces/models/templates/eltex.ttp b/oxi/interfaces/models/templates/eltex.ttp index 2d8660c..ce66112 100644 --- a/oxi/interfaces/models/templates/eltex.ttp +++ b/oxi/interfaces/models/templates/eltex.ttp @@ -1,41 +1,10 @@ -Базовый шаблон для нового устройства. Скопируйте этот файл, переименуйте -в <vendor>.ttp и заполните группы под формат конфигурации вашего устройства. +Eltex configuration parser. -Обязательные группы: system, interfaces. -Опциональная группа: vlans — добавляйте только если устройство поддерживает VLAN. - ---- Группа system --- -Должна возвращать одиночный словарь с полями: - model (str) — модель устройства - serial_number (str) — серийный номер - version (str) — версия прошивки - ---- Группа interfaces --- -Должна возвращать список словарей. Каждый элемент: - interface (str) — имя интерфейса (alias поля name) - ip_address (str|None) — IPv4-адрес - mask (int|None) — длина префикса (напр. 24) - description (str|None) — описание интерфейса - - Если устройство возвращает маску в виде 255.255.255.0, конвертируйте - её в prefix length в методе interfaces() класса устройства. - ---- Группа vlans --- -Должна возвращать список словарей. Каждый элемент: - id (int) — номер VLAN (alias поля vlan_id) - description (str|None) — название VLAN (alias поля name) - ---- Полезные модификаторы TTP --- - {{ field | ORPHRASE }} — одно слово или фраза до конца строки - {{ field | _start_ }} — начало новой записи группы - {{ field | strip('"') }} — убрать кавычки - {{ field | replace("yes","True") }} — замена подстроки - {{ field | exclude("pattern") }} — пропустить строку при совпадении - {{ ignore }} — захватить и выбросить значение - {{ ignore('.*') }} — выбросить всё до конца строки - -Подробнее: docs/templates.md +The system group reads software version data and the serial group extracts +serial numbers from the unit table. The interfaces group parses interface IP +settings. The vlans group supports named VLAN interfaces and compressed VLAN +lists. default_system = { diff --git a/oxi/interfaces/models/templates/h3c.ttp b/oxi/interfaces/models/templates/h3c.ttp index 8c3abe6..afbe090 100644 --- a/oxi/interfaces/models/templates/h3c.ttp +++ b/oxi/interfaces/models/templates/h3c.ttp @@ -1,41 +1,9 @@ -Базовый шаблон для нового устройства. Скопируйте этот файл, переименуйте -в <vendor>.ttp и заполните группы под формат конфигурации вашего устройства. +H3C configuration parser. -Обязательные группы: system, interfaces. -Опциональная группа: vlans — добавляйте только если устройство поддерживает VLAN. - ---- Группа system --- -Должна возвращать одиночный словарь с полями: - model (str) — модель устройства - serial_number (str) — серийный номер - version (str) — версия прошивки - ---- Группа interfaces --- -Должна возвращать список словарей. Каждый элемент: - interface (str) — имя интерфейса (alias поля name) - ip_address (str|None) — IPv4-адрес - mask (int|None) — длина префикса (напр. 24) - description (str|None) — описание интерфейса - - Если устройство возвращает маску в виде 255.255.255.0, конвертируйте - её в prefix length в методе interfaces() класса устройства. - ---- Группа vlans --- -Должна возвращать список словарей. Каждый элемент: - id (int) — номер VLAN (alias поля vlan_id) - description (str|None) — название VLAN (alias поля name) - ---- Полезные модификаторы TTP --- - {{ field | ORPHRASE }} — одно слово или фраза до конца строки - {{ field | _start_ }} — начало новой записи группы - {{ field | strip('"') }} — убрать кавычки - {{ field | replace("yes","True") }} — замена подстроки - {{ field | exclude("pattern") }} — пропустить строку при совпадении - {{ ignore }} — захватить и выбросить значение - {{ ignore('.*') }} — выбросить всё до конца строки - -Подробнее: docs/templates.md +The system group reads boot image version and board model data. The interfaces +group parses interface IP settings. The vlans groups parse both named VLANs and +range-style VLAN declarations. default_system = { diff --git a/oxi/interfaces/models/templates/huawei.ttp b/oxi/interfaces/models/templates/huawei.ttp index 2585629..4686572 100644 --- a/oxi/interfaces/models/templates/huawei.ttp +++ b/oxi/interfaces/models/templates/huawei.ttp @@ -1,40 +1,9 @@ -Базовый шаблон для нового устройства. Скопируйте этот файл, переименуйте -в <vendor>.ttp и заполните группы под формат конфигурации вашего устройства. +Huawei VRP configuration parser. -Обязательные группы: system, interfaces. -Опциональная группа: vlans — добавляйте только если устройство поддерживает VLAN. - ---- Группа system --- -Должна возвращать одиночный словарь с полями: - model (str) — модель устройства - serial_number (str) — серийный номер - version (str) — версия прошивки - ---- Группа interfaces --- -Должна возвращать список словарей. Каждый элемент: - interface (str) — имя интерфейса (alias поля name) - ip_address (str|None) — IPv4-адрес - mask (int|None) — длина префикса (напр. 24) - description (str|None) — описание интерфейса - - Если устройство возвращает маску в виде 255.255.255.0, конвертируйте - её в prefix length в методе interfaces() класса устройства. - ---- Группа vlans --- -Должна возвращать список словарей. Каждый элемент: - id (int) — номер VLAN (alias поля vlan_id) - description (str|None) — название VLAN (alias поля name) - ---- Полезные модификаторы TTP --- - {{ field | ORPHRASE }} — одно слово или фраза до конца строки - {{ field | _start_ }} — начало новой записи группы - {{ field | strip('"') }} — убрать кавычки - {{ field | replace("yes","True") }} — замена подстроки - {{ field | exclude("pattern") }} — пропустить строку при совпадении - {{ ignore }} — захватить и выбросить значение - {{ ignore('.*') }} — выбросить всё до конца строки -Подробнее: docs/templates.md +The system group reads VRP version and slot ESN data. The interfaces group +parses interface blocks and converts dotted decimal masks to prefix lengths. +The vlans group parses `vlan batch` declarations and emits VLAN IDs. default_system = { diff --git a/oxi/interfaces/models/templates/qtech.ttp b/oxi/interfaces/models/templates/qtech.ttp index f606893..1db628d 100644 --- a/oxi/interfaces/models/templates/qtech.ttp +++ b/oxi/interfaces/models/templates/qtech.ttp @@ -1,4 +1,13 @@ +Qtech switch configuration parser. + +The system group reads the model, serial number, and build number. For Qtech, +system.version intentionally stores the build number from lines like +`Version 2.2.0C Build 96279`. + +The interfaces group parses CLI interface blocks and converts dotted decimal +masks to prefix lengths. The vlans group supports named VLANs, comma-separated +VLAN lists, ranges, and continuation lines. default_system = { diff --git a/oxi/interfaces/models/templates/quasar.ttp b/oxi/interfaces/models/templates/quasar.ttp index 98e20fa..9408d9f 100644 --- a/oxi/interfaces/models/templates/quasar.ttp +++ b/oxi/interfaces/models/templates/quasar.ttp @@ -1,41 +1,10 @@ -Базовый шаблон для нового устройства. Скопируйте этот файл, переименуйте -в <vendor>.ttp и заполните группы под формат конфигурации вашего устройства. +Quasar configuration parser. -Обязательные группы: system, interfaces. -Опциональная группа: vlans — добавляйте только если устройство поддерживает VLAN. - ---- Группа system --- -Должна возвращать одиночный словарь с полями: - model (str) — модель устройства - serial_number (str) — серийный номер - version (str) — версия прошивки - ---- Группа interfaces --- -Должна возвращать список словарей. Каждый элемент: - interface (str) — имя интерфейса (alias поля name) - ip_address (str|None) — IPv4-адрес - mask (int|None) — длина префикса (напр. 24) - description (str|None) — описание интерфейса - - Если устройство возвращает маску в виде 255.255.255.0, конвертируйте - её в prefix length в методе interfaces() класса устройства. - ---- Группа vlans --- -Должна возвращать список словарей. Каждый элемент: - id (int) — номер VLAN (alias поля vlan_id) - description (str|None) — название VLAN (alias поля name) - ---- Полезные модификаторы TTP --- - {{ field | ORPHRASE }} — одно слово или фраза до конца строки - {{ field | _start_ }} — начало новой записи группы - {{ field | strip('"') }} — убрать кавычки - {{ field | replace("yes","True") }} — замена подстроки - {{ field | exclude("pattern") }} — пропустить строку при совпадении - {{ ignore }} — захватить и выбросить значение - {{ ignore('.*') }} — выбросить всё до конца строки - -Подробнее: docs/templates.md +The system group supports Assembly-based and Engine-based firmware blocks. The +interfaces group parses the management Ethernet address, while bulkinterfaces +collects per-port descriptions that the Python model merges into interface +records. default_system = { diff --git a/pyproject.toml b/pyproject.toml index 76e2c37..3e081d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "oxipy" version = "0.1.0" -description = "Oxidized API client" +description = "Python client for Oxidized API with TTP-based config parsing" readme = "README.md" license = { file = "LICENSE" } requires-python = ">=3.10" diff --git a/uv.lock b/uv.lock index 4ecb60b..c3bf3aa 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,6 @@ version = 1 revision = 3 -requires-python = ">=3.11" +requires-python = ">=3.10" [[package]] name = "annotated-types" @@ -26,6 +26,22 @@ version = "3.4.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, + { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, + { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, + { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, + { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, + { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, @@ -143,6 +159,19 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, @@ -221,6 +250,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, From 2a03240414368f4c7d8491742ba4fdf30746be1b Mon Sep 17 00:00:00 2001 From: IluaAir Date: Thu, 28 May 2026 15:55:20 +0300 Subject: [PATCH 3/4] Update setuptools version and modify license information in pyproject.toml - Updated the required setuptools version from 61 to 77 to ensure compatibility with the latest features and improvements. - Changed the license format in `pyproject.toml` to specify "Apache-2.0" directly and added a reference to the license file for clarity. --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3e081d9..83e4615 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=61"] +requires = ["setuptools>=77"] build-backend = "setuptools.build_meta" [project] @@ -7,11 +7,11 @@ name = "oxipy" version = "0.1.0" description = "Python client for Oxidized API with TTP-based config parsing" readme = "README.md" -license = { file = "LICENSE" } +license = "Apache-2.0" +license-files = ["LICENSE"] requires-python = ">=3.10" classifiers = [ "Programming Language :: Python :: 3", - "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", ] dependencies = [ From 494cc9b08be701b3956bfbf26d934e80897a6626 Mon Sep 17 00:00:00 2001 From: IluaAir Date: Fri, 29 May 2026 15:53:24 +0300 Subject: [PATCH 4/4] Update project repository URL and simplify installation instructions in README.md - Added a repository URL section in `pyproject.toml` to link to the GitHub repository. - Updated installation instructions in `README.md` to reflect the change from Gitea to GitHub as the source for package installation, removing references to the private Gitea Package Registry. --- README.md | 46 +++++++--------------------------------------- pyproject.toml | 3 +++ 2 files changed, 10 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index e065633..2ca7943 100644 --- a/README.md +++ b/README.md @@ -22,62 +22,30 @@ configuration sections such as system data, interfaces, and VLANs. ## Installation -The package is distributed through a private Gitea Package Registry and from the -source repository. It is not published to PyPI. +The package is distributed from the source repository. It is not published to +PyPI yet. **Requirements:** Python 3.10+ -### From Gitea Package Registry - -Install the package by pointing `pip` to the private registry: - -```bash -pip install oxipy \ - --index-url https://gitea.imbastark.ru/api/packages/Netbox/pypi/simple/ -``` - -You can also configure the registry permanently in `pip.conf` or `pip.ini`: - -```ini -# ~/.config/pip/pip.conf (Linux/macOS) -# %APPDATA%\pip\pip.ini (Windows) - -[global] -extra-index-url = https://gitea.imbastark.ru/api/packages/Netbox/pypi/simple/ -``` - -After that, install normally: - -```bash -pip install oxipy -``` - -If the registry requires authentication, pass a token in the index URL: - -```bash -pip install oxipy \ - --index-url https://__token__:@gitea.imbastark.ru/api/packages/Netbox/pypi/simple/ -``` - -### From Gitea Source +### From GitHub Source Install directly from the repository: ```bash -pip install git+https://gitea.imbastark.ru/Netbox/oxipy.git +pip install git+https://github.com/sttarsky/oxipy.git ``` Install a specific tag or branch: ```bash -pip install git+https://gitea.imbastark.ru/Netbox/oxipy.git@v0.1.0 -pip install git+https://gitea.imbastark.ru/Netbox/oxipy.git@dev +pip install git+https://github.com/sttarsky/oxipy.git@v0.1.0 +pip install git+https://github.com/sttarsky/oxipy.git@dev ``` For local development: ```bash -git clone https://gitea.imbastark.ru/Netbox/oxipy +git clone https://github.com/sttarsky/oxipy cd oxipy pip install -e . ``` diff --git a/pyproject.toml b/pyproject.toml index 83e4615..066d1ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,9 @@ dependencies = [ "ttp>=0.10.0", ] +[project.urls] +Repository = "https://github.com/sttarsky/oxipy" + [tool.setuptools.packages.find] where = ["."] include = ["oxi*"]