Merge pull request 'documentation' (#1) from documentation into dev
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
298
README.md
298
README.md
@@ -1,86 +1,56 @@
|
||||
# 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 from the source repository. It is not published to
|
||||
PyPI yet.
|
||||
|
||||
> Пакет распространяется через Gitea Package Registry и исходники репозитория.
|
||||
> В PyPI пакет не публикуется.
|
||||
**Requirements:** Python 3.10+
|
||||
|
||||
**Требования:** Python 3.11+
|
||||
### From GitHub Source
|
||||
|
||||
### Из Gitea Package Registry
|
||||
|
||||
Добавьте registry в конфигурацию pip и установите пакет:
|
||||
Install directly from the repository:
|
||||
|
||||
```bash
|
||||
pip install oxipy \
|
||||
--index-url https://gitea.imbastark.ru/api/packages/Netbox/pypi/simple/
|
||||
pip install git+https://github.com/sttarsky/oxipy.git
|
||||
```
|
||||
|
||||
Или пропишите registry постоянно в `pip.conf` / `pip.ini`, чтобы не указывать `--index-url` каждый раз:
|
||||
|
||||
```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/
|
||||
```
|
||||
|
||||
После этого достаточно:
|
||||
Install a specific tag or branch:
|
||||
|
||||
```bash
|
||||
pip install oxipy
|
||||
pip install git+https://github.com/sttarsky/oxipy.git@v0.1.0
|
||||
pip install git+https://github.com/sttarsky/oxipy.git@dev
|
||||
```
|
||||
|
||||
Если registry требует аутентификации, передайте токен:
|
||||
For local development:
|
||||
|
||||
```bash
|
||||
pip install oxipy \
|
||||
--index-url https://__token__:<your_token>@gitea.imbastark.ru/api/packages/Netbox/pypi/simple/
|
||||
```
|
||||
|
||||
### Из репозитория Gitea
|
||||
|
||||
Установка напрямую через pip без клонирования:
|
||||
|
||||
```bash
|
||||
pip install git+https://gitea.imbastark.ru/Netbox/oxipy.git
|
||||
```
|
||||
|
||||
Конкретный тег или ветка:
|
||||
|
||||
```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):
|
||||
|
||||
```bash
|
||||
git clone https://gitea.imbastark.ru/Netbox/oxipy
|
||||
git clone https://github.com/sttarsky/oxipy
|
||||
cd oxipy
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Быстрый старт
|
||||
## Quick Start
|
||||
|
||||
```python
|
||||
from oxi import OxiAPI
|
||||
@@ -93,35 +63,35 @@ print(node.ip)
|
||||
print(node.model)
|
||||
print(node.full_name)
|
||||
|
||||
>>> 192.168.1.1
|
||||
>>> keenetic
|
||||
>>> router/HQ
|
||||
|
||||
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 +102,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,118 +157,94 @@ 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)
|
||||
|
||||
>>> 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. |
|
||||
| `.<attr>` | 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`) |
|
||||
| `.<attr>` | оба варианта | Проксирует обращение к атрибутам вложенной модели |
|
||||
| `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` |
|
||||
@@ -314,15 +253,10 @@ print(interfaces.json())
|
||||
| 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).
|
||||
|
||||
Ключи реестра — это значения поля `model`, возвращаемого API для узла. Регистр не учитывается.
|
||||
|
||||
Добавить поддержку нового устройства можно самостоятельно — подробнее в разделе [Расширение моделей](docs/extending-models.md).
|
||||
|
||||
---
|
||||
|
||||
## Дополнительно
|
||||
|
||||
- [Написание TTP-шаблонов](docs/templates.md)
|
||||
- [Расширение и переопределение моделей устройств](docs/extending-models.md)
|
||||
## Additional Documentation
|
||||
|
||||
- [Writing TTP Templates](docs/templates.md)
|
||||
- [Extending Device Models](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-модели
|
||||
▼
|
||||
|
|
||||
+--> 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
|
||||
<vars>
|
||||
@@ -240,12 +248,12 @@ interface {{ interface | _start_ }}
|
||||
</group>
|
||||
|
||||
<group name="vlans">
|
||||
vlan {{ id | _start_ }}
|
||||
name {{ description }}
|
||||
vlan {{ vlan_id | _start_ }}
|
||||
name {{ name | ORPHRASE }}
|
||||
</group>
|
||||
```
|
||||
|
||||
**Класс устройства** (`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.
|
||||
|
||||
@@ -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
|
||||
<doc>
|
||||
Описание шаблона (опционально)
|
||||
Optional template documentation.
|
||||
</doc>
|
||||
|
||||
<vars>
|
||||
<!-- Переменные по умолчанию для групп -->
|
||||
<!-- Default values for groups. -->
|
||||
</vars>
|
||||
|
||||
<group name="system">
|
||||
<!-- Правила для системной информации -->
|
||||
<!-- Rules for system information. -->
|
||||
</group>
|
||||
|
||||
<group name="interfaces">
|
||||
<!-- Правила для интерфейсов -->
|
||||
<!-- Rules for interfaces. -->
|
||||
</group>
|
||||
|
||||
<group name="vlans">
|
||||
<!-- Правила для VLAN (опционально) -->
|
||||
<!-- Optional rules for VLANs. -->
|
||||
</group>
|
||||
```
|
||||
|
||||
Файл-заготовка находится в `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
|
||||
<group name="system">
|
||||
# version: {{ version }}{{ ignore('.*') }}
|
||||
# model = {{ model }}
|
||||
@@ -88,17 +88,15 @@ oxipy использует библиотеку [TTP (Template Text Parser)](htt
|
||||
</group>
|
||||
```
|
||||
|
||||
**Пример (Keenetic):**
|
||||
Example for Keenetic:
|
||||
|
||||
Конфигурация:
|
||||
```
|
||||
```text
|
||||
! release: 4.1.7.1-1
|
||||
! model: Keenetic Extra
|
||||
! hw_version: F02B4E7A1C90
|
||||
```
|
||||
|
||||
Шаблон:
|
||||
```
|
||||
```xml
|
||||
<group name="system">
|
||||
! release: {{ version }}
|
||||
! model: {{ model | ORPHRASE }}
|
||||
@@ -106,34 +104,34 @@ oxipy использует библиотеку [TTP (Template Text Parser)](htt
|
||||
</group>
|
||||
```
|
||||
|
||||
---
|
||||
## 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
|
||||
<group name="interfaces">
|
||||
/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
|
||||
</group>
|
||||
```
|
||||
|
||||
**Пример (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
|
||||
<group name="interfaces">
|
||||
interface {{ name | _start_ | exclude("Vlan") }}
|
||||
interface {{ interface | _start_ }}
|
||||
description {{ description | ORPHRASE }}
|
||||
ip address {{ ip_address }} {{ netmask }}
|
||||
ip address {{ ip_address }} {{ mask | to_cidr }}
|
||||
</group>
|
||||
```
|
||||
|
||||
Здесь переменная называется `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
|
||||
<group name="vlans">
|
||||
interface {{ ignore }}/Vlan{{ id }}
|
||||
description {{ description | ORPHRASE | strip('"') }}
|
||||
vlan {{ vlan_id | _start_ }}
|
||||
name {{ name | ORPHRASE }}
|
||||
</group>
|
||||
```
|
||||
|
||||
---
|
||||
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
|
||||
|
||||
## Переменные по умолчанию
|
||||
|
||||
Блок `<vars>` позволяет задавать значения по умолчанию для группы через атрибут `default`:
|
||||
The `<vars>` block can define default values for a group through the group's
|
||||
`default` attribute:
|
||||
|
||||
```xml
|
||||
<vars>
|
||||
default_system = {
|
||||
"model": "",
|
||||
"serial_number": ""
|
||||
"serial_number": "",
|
||||
"version": ""
|
||||
}
|
||||
</vars>
|
||||
|
||||
@@ -253,17 +247,16 @@ default_system = {
|
||||
</group>
|
||||
```
|
||||
|
||||
Если шаблон не нашёл совпадений для группы, будет возвращён словарь из `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
|
||||
<doc>
|
||||
Шаблон для парсинга Cisco IOS running-config
|
||||
Cisco IOS running-config parser.
|
||||
</doc>
|
||||
|
||||
<vars>
|
||||
@@ -283,24 +276,24 @@ System serial number : {{ serial_number }}
|
||||
<group name="interfaces">
|
||||
interface {{ interface | _start_ }}
|
||||
description {{ description | ORPHRASE }}
|
||||
ip address {{ ip_address }} {{ netmask }}
|
||||
shutdown {{ shutdown | set("True") }}
|
||||
ip address {{ ip_address }} {{ mask | to_cidr }}
|
||||
</group>
|
||||
|
||||
<group name="vlans">
|
||||
vlan {{ id | _start_ }}
|
||||
name {{ description }}
|
||||
vlan {{ vlan_id | _start_ }}
|
||||
name {{ name | ORPHRASE }}
|
||||
</group>
|
||||
```
|
||||
|
||||
---
|
||||
## 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-теги `<group>` и проверяется наличие обязательных секций (`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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,19 +41,20 @@ 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:
|
||||
continue
|
||||
|
||||
ids = item.get("vlan_ids", "")
|
||||
tail = item.get("vlan_tail")
|
||||
if tail:
|
||||
ids = f"{ids},{tail}"
|
||||
for vid in ids:
|
||||
vid = vid.strip()
|
||||
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})
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
|
||||
@@ -1,31 +1,54 @@
|
||||
import os
|
||||
from oxi.interfaces import register_parser
|
||||
from oxi.interfaces.base import BaseDevice
|
||||
|
||||
|
||||
def _expand_vlan_range(value: str | list[str]) -> list[str]:
|
||||
"""Expand values like '1,7,14-15' into individual VLAN IDs."""
|
||||
if isinstance(value, list):
|
||||
value = ",".join(str(item) for item in value)
|
||||
|
||||
result: list[str] = []
|
||||
if not value:
|
||||
return result
|
||||
for part in value.split(","):
|
||||
part = part.strip()
|
||||
if not part:
|
||||
continue
|
||||
if "-" in part:
|
||||
start_s, end_s = part.split("-", 1)
|
||||
try:
|
||||
start, end = int(start_s), int(end_s)
|
||||
except ValueError:
|
||||
result.append(part)
|
||||
continue
|
||||
if start > end:
|
||||
start, end = end, start
|
||||
result.extend(str(i) for i in range(start, end + 1))
|
||||
else:
|
||||
result.append(part)
|
||||
return result
|
||||
|
||||
|
||||
@register_parser(["QTECH"])
|
||||
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 = []
|
||||
named_vlan = set()
|
||||
vlans_ttp = self.raw.get("vlans", [])
|
||||
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", "")
|
||||
continue
|
||||
|
||||
ids = item.get("vlan_ids") or vlan_id or ""
|
||||
tail = item.get("vlan_tail")
|
||||
if tail:
|
||||
ids = f"{ids},{tail}"
|
||||
for vid in ids.split(","):
|
||||
vid = vid.strip()
|
||||
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})
|
||||
@@ -33,9 +56,13 @@ class Qtech(BaseDevice):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(os.path.abspath(os.curdir))
|
||||
with open("./test3.txt") as file:
|
||||
data = file.read()
|
||||
qtech = Qtech(data)
|
||||
qt = qtech.parse()
|
||||
print(qt)
|
||||
with open("./test3-1.txt") as file:
|
||||
data = file.read()
|
||||
qtech = Qtech(data)
|
||||
qt = qtech.parse()
|
||||
print(qt)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -1,41 +1,20 @@
|
||||
<doc>
|
||||
Базовый шаблон для нового устройства. Скопируйте этот файл, переименуйте
|
||||
в <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.
|
||||
</doc>
|
||||
<vars>
|
||||
default_system = {
|
||||
|
||||
@@ -1,41 +1,10 @@
|
||||
<doc>
|
||||
Базовый шаблон для нового устройства. Скопируйте этот файл, переименуйте
|
||||
в <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.
|
||||
</doc>
|
||||
<vars>
|
||||
default_system = {
|
||||
|
||||
@@ -1,41 +1,9 @@
|
||||
<doc>
|
||||
Базовый шаблон для нового устройства. Скопируйте этот файл, переименуйте
|
||||
в <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.
|
||||
</doc>
|
||||
<vars>
|
||||
default_system = {
|
||||
|
||||
@@ -1,40 +1,9 @@
|
||||
<doc>
|
||||
Базовый шаблон для нового устройства. Скопируйте этот файл, переименуйте
|
||||
в <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.
|
||||
</doc>
|
||||
<vars>
|
||||
default_system = {
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
<doc>
|
||||
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.
|
||||
</doc>
|
||||
<vars>
|
||||
default_system = {
|
||||
|
||||
@@ -1,41 +1,10 @@
|
||||
<doc>
|
||||
Базовый шаблон для нового устройства. Скопируйте этот файл, переименуйте
|
||||
в <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.
|
||||
</doc>
|
||||
<vars>
|
||||
default_system = {
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=61"]
|
||||
requires = ["setuptools>=77"]
|
||||
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" }
|
||||
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 = [
|
||||
@@ -20,6 +20,9 @@ dependencies = [
|
||||
"ttp>=0.10.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://github.com/sttarsky/oxipy"
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["."]
|
||||
include = ["oxi*"]
|
||||
|
||||
39
uv.lock
generated
39
uv.lock
generated
@@ -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" },
|
||||
|
||||
Reference in New Issue
Block a user