Compare commits
50 Commits
08733bd493
...
0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 53b79ce04f | |||
|
|
074f2e9340 | ||
|
|
dd7f117380 | ||
|
|
3c0e70b320 | ||
|
|
c40cae1561 | ||
| 7b09612313 | |||
|
|
8edd1a296c | ||
|
|
686cd6d715 | ||
|
|
acb3a6291c | ||
|
|
9c90279868 | ||
|
|
2ea056aa17 | ||
|
|
a617bd6ecd | ||
|
|
0ef5e7798a | ||
|
|
1bc01c9c1b | ||
|
|
170a2ebf85 | ||
|
|
168111e23c | ||
|
|
d329ddc4ad | ||
|
|
229bef99f6 | ||
|
|
f446ae52e7 | ||
|
|
c4f20d3241 | ||
|
|
bebbe78163 | ||
|
|
a55fc938f0 | ||
| 5bb69dfee3 | |||
|
|
494cc9b08b | ||
|
|
2a03240414 | ||
|
|
41c4cc48e9 | ||
|
|
e8c33b0e64 | ||
|
|
74647bea5b | ||
|
|
5a4cfa18d4 | ||
|
|
5fa56d46af | ||
|
|
9fd0ce1516 | ||
|
|
0b92e342e5 | ||
|
|
1cc225917e | ||
|
|
61892d8f51 | ||
|
|
8cebbf743a | ||
|
|
a107662e99 | ||
|
|
1d0f5ed685 | ||
|
|
5b8380aeee | ||
|
|
65c82fbaf5 | ||
|
|
974fff6038 | ||
|
|
586e52282b | ||
|
|
e3392f6c76 | ||
|
|
de0e09af9d | ||
|
|
ca96d2600a | ||
|
|
56eae15e27 | ||
|
|
db79199319 | ||
| 2e109db121 | |||
| b9dce8e417 | |||
| d185dc6c7c | |||
| 68566a24fb |
40
.gitea/workflows/ci.yml
Normal file
40
.gitea/workflows/ci.yml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, master]
|
||||||
|
pull_request:
|
||||||
|
branches: [main, master]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: rassbery
|
||||||
|
container: catthehacker/ubuntu:act-22.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v6
|
||||||
|
- name: Sync dependencies
|
||||||
|
run: uv sync --group dev
|
||||||
|
- name: Ruff lint
|
||||||
|
run: uv run ruff check --output-format=github .
|
||||||
|
- name: Ruff format check
|
||||||
|
run: uv run ruff format --check .
|
||||||
|
|
||||||
|
test:
|
||||||
|
runs-on: rassbery
|
||||||
|
container: catthehacker/ubuntu:act-22.04
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v6
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
- name: Sync dependencies
|
||||||
|
run: uv sync --group dev
|
||||||
|
- name: Run tests
|
||||||
|
run: uv run pytest -q
|
||||||
38
.github/workflows/ci.yml
vendored
Normal file
38
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, master]
|
||||||
|
pull_request:
|
||||||
|
branches: [main, master]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v6
|
||||||
|
- name: Sync dependencies
|
||||||
|
run: uv sync --group dev
|
||||||
|
- name: Ruff lint
|
||||||
|
run: uv run ruff check --output-format=github .
|
||||||
|
- name: Ruff format check
|
||||||
|
run: uv run ruff format --check .
|
||||||
|
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v6
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
- name: Sync dependencies
|
||||||
|
run: uv sync --group dev
|
||||||
|
- name: Run tests
|
||||||
|
run: uv run pytest -q
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -8,7 +8,9 @@ wheels/
|
|||||||
main.py
|
main.py
|
||||||
# Virtual environments
|
# Virtual environments
|
||||||
.venv
|
.venv
|
||||||
|
.python-version
|
||||||
.idea
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
.vscode
|
||||||
# etc files
|
# etc files
|
||||||
*.txt
|
*.txt
|
||||||
@@ -1 +0,0 @@
|
|||||||
3.13
|
|
||||||
201
LICENSE
Normal file
201
LICENSE
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
309
README.md
309
README.md
@@ -1,86 +1,56 @@
|
|||||||
# oxipy
|
# 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)
|
- [API Reference](#api-reference)
|
||||||
- [OxiAPI](#oxiapi)
|
- [OxiAPI](#oxiapi)
|
||||||
- [NodeView](#nodeview)
|
- [NodeView](#nodeview)
|
||||||
- [NodeConfig](#nodeconfig)
|
- [NodeConfig](#nodeconfig)
|
||||||
- [ModelView](#modelview)
|
- [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 и исходники репозитория.
|
**Requirements:** Python 3.10+
|
||||||
> В PyPI пакет не публикуется.
|
|
||||||
|
|
||||||
**Требования:** Python 3.11+
|
### From GitHub Source
|
||||||
|
|
||||||
### Из Gitea Package Registry
|
Install directly from the repository:
|
||||||
|
|
||||||
Добавьте registry в конфигурацию pip и установите пакет:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install oxipy \
|
pip install git+https://github.com/sttarsky/oxipy.git
|
||||||
--index-url https://gitea.imbastark.ru/api/packages/Netbox/pypi/simple/
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Или пропишите registry постоянно в `pip.conf` / `pip.ini`, чтобы не указывать `--index-url` каждый раз:
|
Install a specific tag or branch:
|
||||||
|
|
||||||
```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/
|
|
||||||
```
|
|
||||||
|
|
||||||
После этого достаточно:
|
|
||||||
|
|
||||||
```bash
|
```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
|
```bash
|
||||||
pip install oxipy \
|
git clone https://github.com/sttarsky/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
|
|
||||||
cd oxipy
|
cd oxipy
|
||||||
pip install -e .
|
pip install -e .
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
## Quick Start
|
||||||
|
|
||||||
## Быстрый старт
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from oxi import OxiAPI
|
from oxi import OxiAPI
|
||||||
@@ -93,35 +63,35 @@ print(node.ip)
|
|||||||
print(node.model)
|
print(node.model)
|
||||||
print(node.full_name)
|
print(node.full_name)
|
||||||
|
|
||||||
>>> 192.168.1.1
|
|
||||||
>>> keenetic
|
|
||||||
>>> router/HQ
|
|
||||||
|
|
||||||
print(node.config.system.model)
|
print(node.config.system.model)
|
||||||
print(node.config.interfaces.json())
|
print(node.config.interfaces.dump_json())
|
||||||
print(node.config.vlans.json())
|
print(node.config.vlans.dump_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"}
|
|
||||||
]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
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
|
## API Reference
|
||||||
|
|
||||||
### OxiAPI
|
### OxiAPI
|
||||||
|
|
||||||
Точка входа. Управляет HTTP-сессией и предоставляет доступ к узлам.
|
`OxiAPI` is the entry point. It manages the HTTP session and provides access to
|
||||||
|
Oxidized nodes.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
OxiAPI(
|
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`. |
|
||||||
|
|
||||||
| Параметр | Тип | Описание |
|
Example:
|
||||||
| ---------- | ------ | --------------------------------------------------------- |
|
|
||||||
| `url` | `str` | Базовый URL Oxi API, например `https://oxi.example.com` |
|
|
||||||
| `username` | `str` | Имя пользователя для базовой аутентификации (опционально) |
|
|
||||||
| `password` | `str` | Пароль для базовой аутентификации (опционально) |
|
|
||||||
| `verify` | `bool` | Проверять SSL-сертификат. `True` по умолчанию |
|
|
||||||
|
|
||||||
|
|
||||||
**Пример:**
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Без аутентификации
|
# Without authentication
|
||||||
api = OxiAPI(url="https://oxi.example.com")
|
api = OxiAPI(url="https://oxi.example.com")
|
||||||
|
|
||||||
# С базовой аутентификацией
|
# With HTTP basic authentication
|
||||||
api = OxiAPI(
|
api = OxiAPI(
|
||||||
url="https://oxi.example.com",
|
url="https://oxi.example.com",
|
||||||
username="admin",
|
username="admin",
|
||||||
password="secret",
|
password="secret",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Использование как контекстного менеджера (автоматически закрывает сессию)
|
# As a context manager. The HTTP session is closed automatically.
|
||||||
with OxiAPI(url="https://oxi.example.com") as api:
|
with OxiAPI(url="https://oxi.example.com") as api:
|
||||||
node = api.node("HQ")
|
node = api.node("HQ")
|
||||||
print(node.ip)
|
print(node.ip)
|
||||||
|
|
||||||
>>> 192.168.1.1
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `api.node(name)`
|
#### `api.node(name)`
|
||||||
|
|
||||||
Возвращает `[NodeView](#nodeview)` для указанного узла.
|
Returns a `NodeView` for the requested Oxidized node.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
node = api.node("HQ")
|
node = api.node("HQ")
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### NodeView
|
### 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. |
|
||||||
|
|
||||||
| Свойство | Тип | Описание |
|
Example:
|
||||||
| ----------- | ------------ | ---------------------------------------------------- |
|
|
||||||
| `ip` | `str` | IP-адрес узла |
|
|
||||||
| `full_name` | `str` | Полное имя узла в Oxi |
|
|
||||||
| `group` | `str` | Группа, к которой принадлежит узел |
|
|
||||||
| `model` | `str` | Модель устройства (используется для парсинга) |
|
|
||||||
| `config` | `NodeConfig` | Конфигурация узла (загружается при первом обращении) |
|
|
||||||
|
|
||||||
|
|
||||||
**Пример:**
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
node = api.node("HQ")
|
node = api.node("HQ")
|
||||||
@@ -194,132 +157,106 @@ node = api.node("HQ")
|
|||||||
print(node.ip)
|
print(node.ip)
|
||||||
print(node.group)
|
print(node.group)
|
||||||
print(node.model)
|
print(node.model)
|
||||||
|
|
||||||
>>> 192.168.1.1
|
|
||||||
>>> branch-office
|
|
||||||
>>> keenetic
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### NodeConfig
|
### 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. |
|
||||||
|
|
||||||
| Свойство | Возвращает | Описание |
|
Example:
|
||||||
| ------------ | ----------------------------- | ---------------------------------- |
|
|
||||||
| `system` | `ModelView[System]` | Системная информация об устройстве |
|
|
||||||
| `interfaces` | `ModelView[list[Interfaces]]` | Список интерфейсов |
|
|
||||||
| `vlans` | `ModelView[list[Vlans]]` | Список VLAN (если есть) |
|
|
||||||
| `text` | `str` | Сырой текст конфигурации |
|
|
||||||
|
|
||||||
|
|
||||||
**Пример:**
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
cfg = node.config
|
cfg = node.config
|
||||||
|
|
||||||
# Системная информация
|
|
||||||
print(cfg.system.model)
|
print(cfg.system.model)
|
||||||
print(cfg.system.serial_number)
|
print(cfg.system.serial_number)
|
||||||
print(cfg.system.version)
|
print(cfg.system.version)
|
||||||
|
|
||||||
>>> Mikrotik RB951Ui-2nD
|
|
||||||
>>> B88C0B31117B
|
|
||||||
>>> 7.16.1
|
|
||||||
|
|
||||||
# Итерация по интерфейсам
|
|
||||||
for iface in cfg.interfaces:
|
for iface in cfg.interfaces:
|
||||||
print(iface.name, iface.ip_address, iface.mask)
|
print(iface.name, iface.ip_address, iface.mask)
|
||||||
|
|
||||||
# Индексация
|
|
||||||
first_iface = cfg.interfaces[0]
|
first_iface = cfg.interfaces[0]
|
||||||
print(first_iface.name)
|
print(first_iface.name)
|
||||||
|
|
||||||
# Количество интерфейсов
|
|
||||||
print(len(cfg.interfaces))
|
print(len(cfg.interfaces))
|
||||||
|
|
||||||
# JSON-дамп любой секции
|
print(cfg.interfaces.dump_json())
|
||||||
print(cfg.interfaces.json())
|
print(cfg.vlans.dump_json())
|
||||||
print(cfg.vlans.json())
|
print(cfg.system.dump_json())
|
||||||
print(cfg.system.json())
|
|
||||||
|
|
||||||
# Сырая конфигурация текстом
|
|
||||||
print(cfg.text)
|
print(cfg.text)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
`NodeConfig` also provides `dump()` and `dump_json()` methods for the whole
|
||||||
|
parsed device object.
|
||||||
|
|
||||||
### ModelView
|
### 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. |
|
||||||
|
|
||||||
| Метод / свойство | Применимо к | Описание |
|
`__iter__`, `__len__`, and `__getitem__` are available only for list-backed
|
||||||
| ---------------- | ------------ | ------------------------------------------------- |
|
sections such as `interfaces` and `vlans`. Calling them on `system` raises
|
||||||
| `.json()` | оба варианта | Возвращает JSON-строку (с `by_alias=True`) |
|
`TypeError`.
|
||||||
| `.<attr>` | оба варианта | Проксирует обращение к атрибутам вложенной модели |
|
|
||||||
| `iter(view)` | список | Итерация по элементам списка моделей |
|
|
||||||
| `len(view)` | список | Количество элементов в списке |
|
|
||||||
| `view[i]` | список | Получение элемента по индексу или срез |
|
|
||||||
|
|
||||||
|
Examples:
|
||||||
> `__iter__`, `__len__` и `__getitem__` доступны только для `interfaces` и `vlans` (они оборачивают список). Вызов этих методов на `system` (одиночная модель) вызовет `TypeError`.
|
|
||||||
|
|
||||||
**Примеры:**
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Одиночная модель — system
|
system = node.config.system
|
||||||
view = node.config.system
|
print(system.dump_json())
|
||||||
print(view.json())
|
print(system.model)
|
||||||
>>> '{"model":"RB951Ui-2nD","serial_number":"B88C0B31117B","version":"7.12.1"}'
|
print(system.serial_number)
|
||||||
print(view.model) # 'RB951Ui-2nD'
|
|
||||||
print(view.serial_number) # 'B88C0B31117B'
|
|
||||||
|
|
||||||
>>> RB951Ui-2nD
|
|
||||||
>>> B88C0B31117B
|
|
||||||
# Список — interfaces
|
|
||||||
interfaces = node.config.interfaces
|
interfaces = node.config.interfaces
|
||||||
|
|
||||||
# Итерация
|
|
||||||
for iface in interfaces:
|
for iface in interfaces:
|
||||||
print(iface.name, iface.ip_address)
|
print(iface.name, iface.ip_address)
|
||||||
|
|
||||||
# Длина
|
print(len(interfaces))
|
||||||
print(len(interfaces)) # 5
|
print(interfaces[0])
|
||||||
|
print(interfaces[:3])
|
||||||
# Индексация и срезы
|
print(interfaces.dump())
|
||||||
first = interfaces[0]
|
|
||||||
top3 = interfaces[:3]
|
|
||||||
|
|
||||||
# JSON всего списка
|
|
||||||
print(interfaces.json())
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
## 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` |
|
||||||
|
|
||||||
| Устройство | Ключи реестра |
|
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).
|
||||||
| Keenetic | `ndms`, `keenetic`, `keeneticos` |
|
|
||||||
| MikroTik | `routeros`, `ros`, `mikrotik` |
|
|
||||||
| Qtech | `qtech` |
|
|
||||||
| Huawei | `huawei`, `vrp` |
|
|
||||||
|
|
||||||
|
## Additional Documentation
|
||||||
|
|
||||||
Ключи реестра — это значения поля `model`, возвращаемого API для узла. Регистр не учитывается.
|
- [Writing TTP Templates](docs/templates.md)
|
||||||
|
- [Extending Device Models](docs/extending-models.md)
|
||||||
Добавить поддержку нового устройства можно самостоятельно — подробнее в разделе [Расширение моделей](docs/extending-models.md).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Дополнительно
|
|
||||||
|
|
||||||
- [Написание TTP-шаблонов](docs/templates.md)
|
|
||||||
- [Расширение и переопределение моделей устройств](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.
|
||||||
|
|
||||||
- [Архитектура: путь данных](#архитектура-путь-данных)
|
## Contents
|
||||||
- [Регистрация нового устройства](#регистрация-нового-устройства)
|
|
||||||
- [Переопределение методов (monkey patching)](#переопределение-методов-monkey-patching)
|
- [Data Flow](#data-flow)
|
||||||
|
- [Registering a Device](#registering-a-device)
|
||||||
|
- [Method Overrides](#method-overrides)
|
||||||
- [interfaces()](#interfaces)
|
- [interfaces()](#interfaces)
|
||||||
- [vlans()](#vlans)
|
- [vlans()](#vlans)
|
||||||
- [system()](#system)
|
- [system()](#system)
|
||||||
- [Полный пример: новое устройство](#полный-пример-новое-устройство)
|
- [Complete Example](#complete-example)
|
||||||
- [Контракт: ожидаемые структуры](#контракт-ожидаемые-структуры)
|
- [Expected Contract](#expected-contract)
|
||||||
|
|
||||||
---
|
## Data Flow
|
||||||
|
|
||||||
## Архитектура: путь данных
|
```text
|
||||||
|
configuration text
|
||||||
```
|
|
|
||||||
текст конфигурации
|
v
|
||||||
│
|
TTP template (.ttp)
|
||||||
▼
|
|
|
||||||
TTP-шаблон (.ttp)
|
v
|
||||||
│ парсит в сырой словарь
|
|
||||||
▼
|
|
||||||
self.raw: dict
|
self.raw: dict
|
||||||
│
|
|
|
||||||
├──► system() → dict
|
+--> system() -> dict
|
||||||
├──► interfaces() → list[dict]
|
+--> interfaces() -> list[dict]
|
||||||
└──► vlans() → list[dict]
|
+--> vlans() -> list[dict]
|
||||||
│
|
|
|
||||||
▼
|
v
|
||||||
_validate_contract()
|
Pydantic validation
|
||||||
│ создаёт Pydantic-модели
|
|
|
||||||
▼
|
v
|
||||||
Device(system, interfaces, vlans)
|
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
|
```python
|
||||||
# BaseDevice (упрощённо)
|
|
||||||
def interfaces(self) -> list[dict]:
|
def interfaces(self) -> list[dict]:
|
||||||
return self.raw.get("interfaces", [])
|
return self.raw.get("interfaces", [])
|
||||||
|
|
||||||
@@ -51,18 +54,16 @@ def system(self) -> dict:
|
|||||||
return self.raw.get("system", None)
|
return self.raw.get("system", None)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
## Registering a Device
|
||||||
|
|
||||||
## Регистрация нового устройства
|
To add support for a new vendor:
|
||||||
|
|
||||||
Чтобы добавить поддержку нового вендора:
|
1. Create a Python file in `oxi/interfaces/models/`, for example `cisco.py`.
|
||||||
|
2. Create a template in `oxi/interfaces/models/templates/`, for example
|
||||||
1. Создайте файл в `oxi/interfaces/models/`, например `cisco.py`.
|
`cisco.ttp`.
|
||||||
2. Создайте шаблон `oxi/interfaces/models/templates/cisco.ttp`.
|
3. Subclass `BaseDevice` and register it with `@register_parser`.
|
||||||
3. Унаследуйте класс от `BaseDevice` и зарегистрируйте его декоратором `@register_parser`.
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# oxi/interfaces/models/cisco.py
|
|
||||||
from oxi.interfaces import register_parser
|
from oxi.interfaces import register_parser
|
||||||
from oxi.interfaces.base import BaseDevice
|
from oxi.interfaces.base import BaseDevice
|
||||||
|
|
||||||
@@ -72,26 +73,26 @@ class CiscoIOS(BaseDevice):
|
|||||||
template = "cisco.ttp"
|
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.
|
||||||
|
|
||||||
---
|
## Method Overrides
|
||||||
|
|
||||||
## Переопределение методов (monkey patching)
|
|
||||||
|
|
||||||
### interfaces()
|
### interfaces()
|
||||||
|
|
||||||
Используйте переопределение, когда нужно:
|
Override `interfaces()` when you need to:
|
||||||
|
|
||||||
- Преобразовать формат IP-адреса (например, `netmask` → `prefix_length`).
|
- Convert dotted decimal netmasks to prefix lengths.
|
||||||
- Декодировать escape-последовательности в описаниях.
|
- Decode escaped descriptions.
|
||||||
- Переименовать ключи, не совпадающие с контрактом.
|
- Rename keys that do not match the contract.
|
||||||
- Фильтровать служебные интерфейсы.
|
- Filter service-only interfaces.
|
||||||
|
|
||||||
**Пример: конвертация маски подсети в префикс**
|
Example: convert a netmask to a prefix length.
|
||||||
|
|
||||||
TTP возвращает `netmask` как `255.255.255.0`, а контракт `Interfaces` ожидает `mask` как целое число (prefix length):
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from ipaddress import ip_interface
|
from ipaddress import ip_interface
|
||||||
@@ -114,19 +115,17 @@ class MyVendor(BaseDevice):
|
|||||||
return result
|
return result
|
||||||
```
|
```
|
||||||
|
|
||||||
**Пример: фильтрация служебных интерфейсов**
|
Example: filter management interfaces.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def interfaces(self) -> list[dict]:
|
def interfaces(self) -> list[dict]:
|
||||||
return [
|
return [
|
||||||
item for item in self.raw.get("interfaces", [])
|
item for item in self.raw.get("interfaces", [])
|
||||||
if not item.get("name", "").startswith("lo")
|
if not item.get("interface", "").startswith("Mgmt")
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
**Пример: декодирование Unicode escape-последовательностей**
|
Example: decode escaped UTF-8 descriptions.
|
||||||
|
|
||||||
Некоторые устройства (например, Keenetic) хранят кириллические описания как `\xd0\xb8\xd0\xbc\xd1\x8f`:
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def _decode_utf(self, text: str) -> str:
|
def _decode_utf(self, text: str) -> str:
|
||||||
@@ -140,6 +139,7 @@ def _decode_utf(self, text: str) -> str:
|
|||||||
)
|
)
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
def interfaces(self) -> list[dict]:
|
def interfaces(self) -> list[dict]:
|
||||||
interfaces = self.raw.get("interfaces", [])
|
interfaces = self.raw.get("interfaces", [])
|
||||||
for item in interfaces:
|
for item in interfaces:
|
||||||
@@ -148,75 +148,83 @@ def interfaces(self) -> list[dict]:
|
|||||||
return interfaces
|
return interfaces
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### vlans()
|
### 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
|
```python
|
||||||
def vlans(self) -> list[dict]:
|
def vlans(self) -> list[dict]:
|
||||||
result = []
|
result = []
|
||||||
for item in self.raw.get("vlans", []):
|
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)
|
result.append(item)
|
||||||
return result
|
return result
|
||||||
```
|
```
|
||||||
|
|
||||||
**Пример: объединение данных из нескольких секций**
|
Example: merge data from another raw group.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def vlans(self) -> list[dict]:
|
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", []):
|
for extra in self.raw.get("vlan_details", []):
|
||||||
vlan_id = extra.get("id")
|
vlan_id = extra.get("vlan_id")
|
||||||
if vlan_id in vlans:
|
if vlan_id in vlans:
|
||||||
vlans[vlan_id].update(extra)
|
vlans[vlan_id].update(extra)
|
||||||
return list(vlans.values())
|
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()
|
### 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
|
```python
|
||||||
def system(self) -> dict:
|
def system(self) -> dict:
|
||||||
raw_system = self.raw.get("system", {})
|
raw_system = self.raw.get("system", {})
|
||||||
# Устройство возвращает серийный номер в двух частях
|
|
||||||
part1 = raw_system.get("serial_part1", "")
|
part1 = raw_system.get("serial_part1", "")
|
||||||
part2 = raw_system.get("serial_part2", "")
|
part2 = raw_system.get("serial_part2", "")
|
||||||
raw_system["serial_number"] = f"{part1}-{part2}"
|
raw_system["serial_number"] = f"{part1}-{part2}"
|
||||||
return raw_system
|
return raw_system
|
||||||
```
|
```
|
||||||
|
|
||||||
**Пример: нормализация строки версии**
|
Example: normalize a version string.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def system(self) -> dict:
|
def system(self) -> dict:
|
||||||
raw_system = self.raw.get("system", {})
|
raw_system = self.raw.get("system", {})
|
||||||
# Убираем лишнее из "7.12.1 (stable)" → "7.12.1"
|
|
||||||
version = raw_system.get("version", "")
|
version = raw_system.get("version", "")
|
||||||
raw_system["version"] = version.split()[0] if version else version
|
raw_system["version"] = version.split()[0] if version else version
|
||||||
return raw_system
|
return raw_system
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
## Complete Example
|
||||||
|
|
||||||
## Полный пример: новое устройство
|
Assume a Cisco IOS-like device where:
|
||||||
|
|
||||||
Допустим, нужно добавить поддержку Cisco IOS, где:
|
- IP address and netmask are separated by a space.
|
||||||
- IP-адрес и маска разделены пробелом в конфигурации (`ip address 10.0.0.1 255.255.255.0`).
|
- 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
|
```xml
|
||||||
<vars>
|
<vars>
|
||||||
@@ -240,12 +248,12 @@ interface {{ interface | _start_ }}
|
|||||||
</group>
|
</group>
|
||||||
|
|
||||||
<group name="vlans">
|
<group name="vlans">
|
||||||
vlan {{ id | _start_ }}
|
vlan {{ vlan_id | _start_ }}
|
||||||
name {{ description }}
|
name {{ name | ORPHRASE }}
|
||||||
</group>
|
</group>
|
||||||
```
|
```
|
||||||
|
|
||||||
**Класс устройства** (`oxi/interfaces/models/cisco.py`):
|
Device model: `oxi/interfaces/models/cisco.py`
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from ipaddress import ip_interface
|
from ipaddress import ip_interface
|
||||||
@@ -260,12 +268,10 @@ class CiscoIOS(BaseDevice):
|
|||||||
def interfaces(self) -> list[dict]:
|
def interfaces(self) -> list[dict]:
|
||||||
result = []
|
result = []
|
||||||
for item in self.raw.get("interfaces", []):
|
for item in self.raw.get("interfaces", []):
|
||||||
# Конвертируем маску подсети в длину префикса
|
|
||||||
if item.get("ip_address") and item.get("netmask"):
|
if item.get("ip_address") and item.get("netmask"):
|
||||||
iface = ip_interface(f"{item['ip_address']}/{item['netmask']}")
|
iface = ip_interface(f"{item['ip_address']}/{item['netmask']}")
|
||||||
item["mask"] = iface.network.prefixlen
|
item["mask"] = iface.network.prefixlen
|
||||||
item.pop("netmask", None)
|
item.pop("netmask", None)
|
||||||
# Фильтруем интерфейсы управления
|
|
||||||
if item.get("interface", "").startswith("Mgmt"):
|
if item.get("interface", "").startswith("Mgmt"):
|
||||||
continue
|
continue
|
||||||
result.append(item)
|
result.append(item)
|
||||||
@@ -273,53 +279,48 @@ class CiscoIOS(BaseDevice):
|
|||||||
|
|
||||||
def system(self) -> dict:
|
def system(self) -> dict:
|
||||||
raw_system = self.raw.get("system", {})
|
raw_system = self.raw.get("system", {})
|
||||||
# Нормализуем версию: "15.2(4)M3" → оставляем как есть
|
|
||||||
# Убираем лишние пробелы в модели
|
|
||||||
if raw_system.get("model"):
|
if raw_system.get("model"):
|
||||||
raw_system["model"] = raw_system["model"].strip()
|
raw_system["model"] = raw_system["model"].strip()
|
||||||
return raw_system
|
return raw_system
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
## Expected Contract
|
||||||
|
|
||||||
## Контракт: ожидаемые структуры
|
Methods must return structures accepted by `oxi.interfaces.contract`.
|
||||||
|
|
||||||
Методы должны возвращать данные в следующем формате. Контракт жёстко проверяется Pydantic.
|
### `system() -> dict`
|
||||||
|
|
||||||
### `system()` → `dict`
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
{
|
{
|
||||||
"model": "RB951Ui-2nD", # str, обязательно
|
"model": "RB951Ui-2nD",
|
||||||
"serial_number": "B88C0B31117B", # str, обязательно
|
"serial_number": "B88C0B31117B",
|
||||||
"version": "7.12.1", # str, обязательно
|
"version": "7.12.1",
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### `interfaces()` → `list[dict]`
|
### `interfaces() -> list[dict]`
|
||||||
|
|
||||||
```python
|
```python
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"interface": "ether1", # str, обязательно (alias для поля name)
|
"interface": "ether1",
|
||||||
"ip_address": "192.168.1.1", # str | None
|
"ip_address": "192.168.1.1",
|
||||||
"mask": 24, # int | None (длина префикса)
|
"mask": 24,
|
||||||
"description": "LAN", # str | None
|
"description": "LAN",
|
||||||
},
|
},
|
||||||
...
|
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
### `vlans()` → `list[dict]`
|
### `vlans() -> list[dict]`
|
||||||
|
|
||||||
```python
|
```python
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"id": 10, # int, обязательно (alias для поля vlan_id)
|
"vlan_id": 10,
|
||||||
"description": "MGMT", # str | None (alias для поля name)
|
"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
|
||||||
|
|
||||||
- [Структура шаблона](#структура-шаблона)
|
- [Template Structure](#template-structure)
|
||||||
- [Обязательные группы](#обязательные-группы)
|
- [Required Groups](#required-groups)
|
||||||
- [Секция system](#секция-system)
|
- [The system Group](#the-system-group)
|
||||||
- [Секция interfaces](#секция-interfaces)
|
- [The interfaces Group](#the-interfaces-group)
|
||||||
- [Секция vlans](#секция-vlans)
|
- [The vlans Group](#the-vlans-group)
|
||||||
- [TTP: основные возможности](#ttp-основные-возможности)
|
- [Useful TTP Features](#useful-ttp-features)
|
||||||
- [Переменные по умолчанию](#переменные-по-умолчанию)
|
- [Default Variables](#default-variables)
|
||||||
- [Практические примеры](#практические-примеры)
|
- [Full Example](#full-example)
|
||||||
- [Валидация шаблона](#валидация-шаблона)
|
- [Validation](#validation)
|
||||||
|
|
||||||
---
|
## Template Structure
|
||||||
|
|
||||||
## Структура шаблона
|
Each template is a `.ttp` file with a small set of conventional blocks:
|
||||||
|
|
||||||
Каждый шаблон — это `.ttp`-файл, состоящий из следующих блоков:
|
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<doc>
|
<doc>
|
||||||
Описание шаблона (опционально)
|
Optional template documentation.
|
||||||
</doc>
|
</doc>
|
||||||
|
|
||||||
<vars>
|
<vars>
|
||||||
<!-- Переменные по умолчанию для групп -->
|
<!-- Default values for groups. -->
|
||||||
</vars>
|
</vars>
|
||||||
|
|
||||||
<group name="system">
|
<group name="system">
|
||||||
<!-- Правила для системной информации -->
|
<!-- Rules for system information. -->
|
||||||
</group>
|
</group>
|
||||||
|
|
||||||
<group name="interfaces">
|
<group name="interfaces">
|
||||||
<!-- Правила для интерфейсов -->
|
<!-- Rules for interfaces. -->
|
||||||
</group>
|
</group>
|
||||||
|
|
||||||
<group name="vlans">
|
<group name="vlans">
|
||||||
<!-- Правила для VLAN (опционально) -->
|
<!-- Optional rules for VLANs. -->
|
||||||
</group>
|
</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. |
|
||||||
|
|
||||||
| Группа | Обязательна | Описание |
|
If a required group is missing from the template or from the TTP result,
|
||||||
|--------------|-------------|-------------------------------|
|
`BaseDevice` raises `ValueError`.
|
||||||
| `system` | Да | Системная информация |
|
|
||||||
| `interfaces` | Да | Конфигурация интерфейсов |
|
|
||||||
| `vlans` | Нет | Конфигурация VLAN |
|
|
||||||
|
|
||||||
Если обязательная группа отсутствует в шаблоне или 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. |
|
||||||
|
|
||||||
| Поле | Тип | Обязательное | Описание |
|
Example for MikroTik:
|
||||||
|-----------------|------|--------------|---------------------|
|
|
||||||
| `model` | str | Да | Модель устройства |
|
|
||||||
| `serial_number` | str | Да | Серийный номер |
|
|
||||||
| `version` | str | Да | Версия прошивки |
|
|
||||||
|
|
||||||
**Пример (MikroTik):**
|
```text
|
||||||
|
|
||||||
Конфигурация:
|
|
||||||
```
|
|
||||||
# version: 7.12.1 (stable)
|
# version: 7.12.1 (stable)
|
||||||
# model = RB951Ui-2nD
|
# model = RB951Ui-2nD
|
||||||
# serial number = B88C0B31117B
|
# serial number = B88C0B31117B
|
||||||
```
|
```
|
||||||
|
|
||||||
Шаблон:
|
```xml
|
||||||
```
|
|
||||||
<group name="system">
|
<group name="system">
|
||||||
# version: {{ version }}{{ ignore('.*') }}
|
# version: {{ version }}{{ ignore('.*') }}
|
||||||
# model = {{ model }}
|
# model = {{ model }}
|
||||||
@@ -88,17 +88,15 @@ oxipy использует библиотеку [TTP (Template Text Parser)](htt
|
|||||||
</group>
|
</group>
|
||||||
```
|
```
|
||||||
|
|
||||||
**Пример (Keenetic):**
|
Example for Keenetic:
|
||||||
|
|
||||||
Конфигурация:
|
```text
|
||||||
```
|
|
||||||
! release: 4.1.7.1-1
|
! release: 4.1.7.1-1
|
||||||
! model: Keenetic Extra
|
! model: Keenetic Extra
|
||||||
! hw_version: F02B4E7A1C90
|
! hw_version: F02B4E7A1C90
|
||||||
```
|
```
|
||||||
|
|
||||||
Шаблон:
|
```xml
|
||||||
```
|
|
||||||
<group name="system">
|
<group name="system">
|
||||||
! release: {{ version }}
|
! release: {{ version }}
|
||||||
! model: {{ model | ORPHRASE }}
|
! model: {{ model | ORPHRASE }}
|
||||||
@@ -106,34 +104,34 @@ oxipy использует библиотеку [TTP (Template Text Parser)](htt
|
|||||||
</group>
|
</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 | Тип | Обязательное |
|
The Pydantic field `name` has the alias `interface`, so templates should usually
|
||||||
|---------------|-----------------|------------------|--------------|
|
emit `interface`. You can also emit `name` because the models allow population
|
||||||
| `name` | `interface` | str | Да |
|
by field name, or you can normalize keys in the device class by overriding
|
||||||
| `ip_address` | `ip_address` | IPv4Address | Нет |
|
`interfaces()`.
|
||||||
| `mask` | `mask` | int (prefix len) | Нет |
|
|
||||||
| `description` | `description` | str | Нет |
|
|
||||||
|
|
||||||
> **Важно:** поле `name` в Pydantic-модели имеет алиас `interface`, поэтому в шаблоне переменную нужно называть именно `interface` **или** переопределить метод `interfaces()` в классе модели (см. [Расширение моделей](extending-models.md)).
|
Example for MikroTik:
|
||||||
|
|
||||||
**Пример (MikroTik):**
|
```text
|
||||||
|
|
||||||
Конфигурация:
|
|
||||||
```
|
|
||||||
/ip address
|
/ip address
|
||||||
add address=192.168.1.1/24 interface=ether1 network=192.168.1.0
|
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
|
add address=10.0.0.1/30 comment="WAN link" interface=ether2 network=10.0.0.0
|
||||||
```
|
```
|
||||||
|
|
||||||
Шаблон:
|
```xml
|
||||||
```
|
|
||||||
<group name="interfaces">
|
<group name="interfaces">
|
||||||
/ip address
|
/ip address
|
||||||
add address={{ ip_address | _start_ }}/{{ mask }} interface={{ interface }} network={{ network }}
|
add address={{ ip_address | _start_ }}/{{ mask }} interface={{ interface }} network={{ network }}
|
||||||
@@ -141,108 +139,104 @@ add address={{ ip_address | _start_ }}/{{ mask }} comment={{ description | ORPHR
|
|||||||
</group>
|
</group>
|
||||||
```
|
```
|
||||||
|
|
||||||
**Пример (Keenetic):**
|
Example for CLI-style devices:
|
||||||
|
|
||||||
Конфигурация:
|
```text
|
||||||
```
|
interface Vlanif120
|
||||||
interface GigabitEthernet0/0
|
description SSH
|
||||||
description "WAN"
|
ip address 10.26.196.254 255.255.255.0
|
||||||
ip address 10.0.0.2 255.255.255.252
|
|
||||||
interface GigabitEthernet0/1
|
|
||||||
ip address 192.168.1.1 255.255.255.0
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Шаблон:
|
```xml
|
||||||
```
|
|
||||||
<group name="interfaces">
|
<group name="interfaces">
|
||||||
interface {{ name | _start_ | exclude("Vlan") }}
|
interface {{ interface | _start_ }}
|
||||||
description {{ description | ORPHRASE }}
|
description {{ description | ORPHRASE }}
|
||||||
ip address {{ ip_address }} {{ netmask }}
|
ip address {{ ip_address }} {{ mask | to_cidr }}
|
||||||
</group>
|
</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 | Тип | Обязательное |
|
`name` has the alias `description`, so either key is accepted. Existing parsers
|
||||||
|-----------|-----------------|------|--------------|
|
use both forms depending on the vendor format.
|
||||||
| `vlan_id` | `id` | int | Да |
|
|
||||||
| `name` | `description` | str | Нет |
|
|
||||||
|
|
||||||
> `vlan_id` имеет алиас `id`, поэтому в шаблоне переменная должна называться `id` либо переименовываться в методе `vlans()`.
|
Example:
|
||||||
|
|
||||||
**Пример (Keenetic):**
|
```text
|
||||||
|
vlan 10
|
||||||
Конфигурация:
|
name MGMT
|
||||||
```
|
|
||||||
interface Bridge0/Vlan10
|
|
||||||
description "MGMT"
|
|
||||||
interface Bridge0/Vlan20
|
|
||||||
description "SERVERS"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Шаблон:
|
```xml
|
||||||
```
|
|
||||||
<group name="vlans">
|
<group name="vlans">
|
||||||
interface {{ ignore }}/Vlan{{ id }}
|
vlan {{ vlan_id | _start_ }}
|
||||||
description {{ description | ORPHRASE | strip('"') }}
|
name {{ name | ORPHRASE }}
|
||||||
</group>
|
</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
|
||||||
|
|
||||||
| Маркер | Описание |
|
| Marker | Description |
|
||||||
|-------------|---------------------------------------------------------------|
|
| --- | --- |
|
||||||
| `_start_` | Строка с этой переменной считается началом нового совпадения |
|
| `_start_` | Starts a new group match from the current line. |
|
||||||
| `_end_` | Строка с этой переменной завершает совпадение группы |
|
| `_end_` | Ends the current group match. |
|
||||||
|
|
||||||
```
|
```xml
|
||||||
add address={{ ip_address | _start_ }}/{{ mask }} interface={{ name }}
|
interface {{ interface | _start_ }}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Модификаторы переменных
|
### Variable modifiers
|
||||||
|
|
||||||
| Модификатор | Описание |
|
| Modifier | Description |
|
||||||
|------------------------|-----------------------------------------------------------|
|
| --- | --- |
|
||||||
| `ORPHRASE` | Захватывает одно слово или фразу (до конца строки) |
|
| `ORPHRASE` | Captures a word or phrase to the end of the line. |
|
||||||
| `exclude("pattern")` | Пропускает строку, если захваченное значение содержит паттерн |
|
| `exclude("pattern")` | Skips the match when the captured value contains the pattern. |
|
||||||
| `strip('"')` | Удаляет символ из начала и конца захваченного значения |
|
| `strip('"')` | Removes a character from both ends of the captured value. |
|
||||||
| `replace("old","new")` | Заменяет подстроку в захваченном значении |
|
| `replace("old","new")` | Replaces text inside the captured value. |
|
||||||
| `re("pattern")` | Принимает значение, только если оно соответствует regex |
|
| `re("pattern")` | Accepts the value only if it matches the regex. |
|
||||||
| `ignore` | Захватывает, но игнорирует значение (не включает в результат) |
|
| `ignore` | Captures and discards the value. |
|
||||||
| `ignore('.*')` | Игнорирует всё до конца строки |
|
| `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
|
## disabled no comment
|
||||||
add address={{ ip_address | _start_ }}/{{ mask }} interface={{ name }}
|
add address={{ ip_address | _start_ }}/{{ mask }} interface={{ interface }}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
## Default Variables
|
||||||
|
|
||||||
## Переменные по умолчанию
|
The `<vars>` block can define default values for a group through the group's
|
||||||
|
`default` attribute:
|
||||||
Блок `<vars>` позволяет задавать значения по умолчанию для группы через атрибут `default`:
|
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<vars>
|
<vars>
|
||||||
default_system = {
|
default_system = {
|
||||||
"model": "",
|
"model": "",
|
||||||
"serial_number": ""
|
"serial_number": "",
|
||||||
|
"version": ""
|
||||||
}
|
}
|
||||||
</vars>
|
</vars>
|
||||||
|
|
||||||
@@ -253,17 +247,16 @@ default_system = {
|
|||||||
</group>
|
</group>
|
||||||
```
|
```
|
||||||
|
|
||||||
Если шаблон не нашёл совпадений для группы, будет возвращён словарь из `default_system`.
|
If the group does not match anything, TTP returns the default dictionary.
|
||||||
|
|
||||||
---
|
## Full Example
|
||||||
|
|
||||||
## Практические примеры
|
This simplified Cisco IOS-style example shows the expected shape of a complete
|
||||||
|
template:
|
||||||
### Полный шаблон для нового устройства (пример: Cisco IOS)
|
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<doc>
|
<doc>
|
||||||
Шаблон для парсинга Cisco IOS running-config
|
Cisco IOS running-config parser.
|
||||||
</doc>
|
</doc>
|
||||||
|
|
||||||
<vars>
|
<vars>
|
||||||
@@ -283,24 +276,24 @@ System serial number : {{ serial_number }}
|
|||||||
<group name="interfaces">
|
<group name="interfaces">
|
||||||
interface {{ interface | _start_ }}
|
interface {{ interface | _start_ }}
|
||||||
description {{ description | ORPHRASE }}
|
description {{ description | ORPHRASE }}
|
||||||
ip address {{ ip_address }} {{ netmask }}
|
ip address {{ ip_address }} {{ mask | to_cidr }}
|
||||||
shutdown {{ shutdown | set("True") }}
|
|
||||||
</group>
|
</group>
|
||||||
|
|
||||||
<group name="vlans">
|
<group name="vlans">
|
||||||
vlan {{ id | _start_ }}
|
vlan {{ vlan_id | _start_ }}
|
||||||
name {{ description }}
|
name {{ name | ORPHRASE }}
|
||||||
</group>
|
</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`).
|
After that, parsed data is validated by Pydantic models from
|
||||||
|
`oxi.interfaces.contract`. Invalid structures raise the original Pydantic
|
||||||
2. **Валидация результата парсинга** — после запуска TTP проверяется, что обязательные группы действительно присутствуют в результате (т.е. конфигурация содержала соответствующие строки).
|
validation error.
|
||||||
|
|
||||||
При нарушении любого условия выбрасывается `ValueError` с подробным описанием проблемы.
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
from .core import OxiAPI
|
from .core import OxiAPI
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"OxiAPI",
|
"OxiAPI",
|
||||||
]
|
]
|
||||||
|
|||||||
20
oxi/adapter.py
Normal file
20
oxi/adapter.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
from requests.adapters import HTTPAdapter
|
||||||
|
from urllib3.util import Retry
|
||||||
|
|
||||||
|
|
||||||
|
class OxiAdapter(HTTPAdapter):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
timeout: int | None = None,
|
||||||
|
max_retries: int = 3,
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
self.timeout = timeout
|
||||||
|
retry = Retry(total=max_retries, backoff_factor=0.3)
|
||||||
|
super().__init__(*args, max_retries=retry, **kwargs)
|
||||||
|
|
||||||
|
def send(self, request, **kwargs):
|
||||||
|
if kwargs.get("timeout") is None:
|
||||||
|
kwargs["timeout"] = self.timeout
|
||||||
|
return super().send(request, **kwargs)
|
||||||
21
oxi/conf.py
21
oxi/conf.py
@@ -1,6 +1,7 @@
|
|||||||
from functools import cached_property
|
|
||||||
import json
|
import json
|
||||||
from typing import TYPE_CHECKING, Generic, Iterator, Type, TypeVar
|
from collections.abc import Iterator
|
||||||
|
from functools import cached_property
|
||||||
|
from typing import TYPE_CHECKING, Generic, TypeVar
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
@@ -16,7 +17,7 @@ class ModelView(Generic[TModel]):
|
|||||||
def __init__(self, model: TModel | list[TModel]):
|
def __init__(self, model: TModel | list[TModel]):
|
||||||
self._model = model
|
self._model = model
|
||||||
|
|
||||||
def json(self) -> str:
|
def dump_json(self) -> str:
|
||||||
if isinstance(self._model, list):
|
if isinstance(self._model, list):
|
||||||
return json.dumps(
|
return json.dumps(
|
||||||
[item.model_dump(by_alias=True) for item in self._model],
|
[item.model_dump(by_alias=True) for item in self._model],
|
||||||
@@ -24,6 +25,11 @@ class ModelView(Generic[TModel]):
|
|||||||
)
|
)
|
||||||
return self._model.model_dump_json(by_alias=True)
|
return self._model.model_dump_json(by_alias=True)
|
||||||
|
|
||||||
|
def dump(self) -> dict | list:
|
||||||
|
if isinstance(self._model, list):
|
||||||
|
return [item.model_dump(by_alias=True) for item in self._model]
|
||||||
|
return self._model.model_dump(by_alias=True)
|
||||||
|
|
||||||
def __iter__(self) -> Iterator[TModel]:
|
def __iter__(self) -> Iterator[TModel]:
|
||||||
if isinstance(self._model, list):
|
if isinstance(self._model, list):
|
||||||
return iter(self._model)
|
return iter(self._model)
|
||||||
@@ -55,7 +61,7 @@ class NodeConfig:
|
|||||||
self._device: type[BaseDevice] = device_registry.get(self._model.lower())
|
self._device: type[BaseDevice] = device_registry.get(self._model.lower())
|
||||||
if self._device is None:
|
if self._device is None:
|
||||||
raise ValueError(f"Device model '{self._model}' not found in registry")
|
raise ValueError(f"Device model '{self._model}' not found in registry")
|
||||||
self._parsed_data = self._device(self.text).parse()
|
self._parsed_data = self._device(self.text, name=self._full_name).parse()
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def _response(self):
|
def _response(self):
|
||||||
@@ -67,8 +73,11 @@ class NodeConfig:
|
|||||||
def text(self):
|
def text(self):
|
||||||
return self._response.text
|
return self._response.text
|
||||||
|
|
||||||
def json(self):
|
def dump_json(self):
|
||||||
return self._parsed_data.model_dump_json()
|
return self._parsed_data.model_dump_json(by_alias=True)
|
||||||
|
|
||||||
|
def dump(self):
|
||||||
|
return self._parsed_data.model_dump(by_alias=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.text
|
return self.text
|
||||||
|
|||||||
39
oxi/core.py
39
oxi/core.py
@@ -1,5 +1,8 @@
|
|||||||
from typing import Optional
|
from requests import HTTPError, Session
|
||||||
from requests import Session
|
|
||||||
|
from oxi.adapter import OxiAdapter
|
||||||
|
from oxi.exception import OxiAPIError
|
||||||
|
|
||||||
from .node import Node
|
from .node import Node
|
||||||
|
|
||||||
|
|
||||||
@@ -7,17 +10,29 @@ class OxiAPI:
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
url: str,
|
url: str,
|
||||||
username: Optional[str] = None,
|
username: str | None = None,
|
||||||
password: Optional[str] = None,
|
password: str | None = None,
|
||||||
verify: bool = True,
|
verify: bool = True,
|
||||||
):
|
):
|
||||||
self.base_url = url.rstrip("/")
|
self.base_url = url.rstrip("/")
|
||||||
self._session = Session()
|
self._session = self.__create_session(username, password, verify)
|
||||||
self._session.verify = verify
|
|
||||||
if username and password:
|
|
||||||
self._session.auth = (username, password)
|
|
||||||
self.node = Node(self._session, self.base_url)
|
self.node = Node(self._session, self.base_url)
|
||||||
|
|
||||||
|
def __create_session(
|
||||||
|
self,
|
||||||
|
username: str | None = None,
|
||||||
|
password: str | None = None,
|
||||||
|
verify: bool = True,
|
||||||
|
) -> Session:
|
||||||
|
session = Session()
|
||||||
|
adapter = OxiAdapter(timeout=10, max_retries=3)
|
||||||
|
session.mount("https://", adapter)
|
||||||
|
session.mount("http://", adapter)
|
||||||
|
session.verify = verify
|
||||||
|
if username and password:
|
||||||
|
session.auth = (username, password)
|
||||||
|
return session
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@@ -26,3 +41,11 @@ class OxiAPI:
|
|||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
return self._session.close()
|
return self._session.close()
|
||||||
|
|
||||||
|
def reload(self):
|
||||||
|
try:
|
||||||
|
reload_response = self._session.get(f"{self.base_url}/reload")
|
||||||
|
reload_response.raise_for_status()
|
||||||
|
except HTTPError as e:
|
||||||
|
raise OxiAPIError.from_http_error(e, context="Reload Oxidized") from e
|
||||||
|
return reload_response.status_code
|
||||||
|
|||||||
65
oxi/exception.py
Normal file
65
oxi/exception.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from requests import HTTPError
|
||||||
|
|
||||||
|
_STATUS_MESSAGES: dict[int, str] = {
|
||||||
|
401: "Unauthorized",
|
||||||
|
403: "Forbidden",
|
||||||
|
500: "Internal Server Error",
|
||||||
|
502: "Bad Gateway",
|
||||||
|
503: "Service Unavailable",
|
||||||
|
504: "Gateway Timeout",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _looks_like_node_not_found_html(e: "HTTPError") -> bool:
|
||||||
|
resp = getattr(e, "response", None)
|
||||||
|
if resp is None:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
content_type = (resp.headers or {}).get("Content-Type", "")
|
||||||
|
except Exception:
|
||||||
|
content_type = ""
|
||||||
|
if "text/html" not in (content_type or "").lower():
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
body = (resp.text or "")[:20_000]
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
return (
|
||||||
|
"Oxidized::NodeNotFound" in body
|
||||||
|
or "NodeNotFound" in body
|
||||||
|
or "<title>Oxidized::NodeNotFound" in body
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class OxiAPIError(Exception):
|
||||||
|
def __init__(self, message: str, status_code: int | None = None):
|
||||||
|
super().__init__(message)
|
||||||
|
self.status_code = status_code
|
||||||
|
self.message = message
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if self.status_code is not None:
|
||||||
|
return f"OxiAPIError: {self.args[0]} (HTTP {self.status_code})"
|
||||||
|
return f"OxiAPIError: {self.args[0]}"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_http_error(cls, e: "HTTPError", context: str = "") -> "OxiAPIError":
|
||||||
|
resp = getattr(e, "response", None)
|
||||||
|
status = resp.status_code if resp is not None else None
|
||||||
|
|
||||||
|
if status == 500 and _looks_like_node_not_found_html(e):
|
||||||
|
status = 404
|
||||||
|
|
||||||
|
if status == 404:
|
||||||
|
message = f"{context} not found" if context else "Not found"
|
||||||
|
else:
|
||||||
|
base = (
|
||||||
|
(_STATUS_MESSAGES.get(status) if status is not None else None)
|
||||||
|
or (resp.reason if resp is not None else None)
|
||||||
|
or (f"HTTP {status}" if status is not None else "Request failed")
|
||||||
|
)
|
||||||
|
message = f"{context}: {base}" if context else base
|
||||||
|
return cls(message, status)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from typing import Callable, Type
|
from collections.abc import Callable
|
||||||
|
|
||||||
from .base import BaseDevice
|
from .base import BaseDevice
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@ device_registry = {}
|
|||||||
|
|
||||||
def register_parser(
|
def register_parser(
|
||||||
name: list[str] | str,
|
name: list[str] | str,
|
||||||
) -> Callable[[Type[BaseDevice]], Type[BaseDevice]]:
|
) -> Callable[[type[BaseDevice]], type[BaseDevice]]:
|
||||||
def wrapper(cls):
|
def wrapper(cls):
|
||||||
name_list = []
|
name_list = []
|
||||||
if isinstance(name, str):
|
if isinstance(name, str):
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
|
import xml.etree.ElementTree as ET
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from ttp import ttp
|
from ttp import ttp
|
||||||
from oxi.interfaces.contract import Device
|
|
||||||
import xml.etree.ElementTree as ET
|
from oxi.exception import OxiAPIError
|
||||||
from oxi.interfaces.contract import Interfaces, System, Vlans
|
from oxi.interfaces.contract import Device, Interfaces, System, Vlans
|
||||||
|
|
||||||
|
|
||||||
class BaseDevice(ABC):
|
class BaseDevice(ABC):
|
||||||
_REQUIRED_SECTIONS: frozenset[str] = frozenset({"system", "interfaces"})
|
_REQUIRED_SECTIONS: frozenset[str] = frozenset({"system", "interfaces"})
|
||||||
_OPTIONAL_SECTIONS: frozenset[str] = frozenset({"vlans"})
|
_OPTIONAL_SECTIONS: frozenset[str] = frozenset({"vlans"})
|
||||||
|
|
||||||
def __init__(self, config: str):
|
def __init__(self, config: str, name: str | None = None):
|
||||||
self.config: str = config
|
self.config: str = config
|
||||||
|
self.name = name
|
||||||
|
|
||||||
self._loaded_template = self._load_template()
|
self._loaded_template = self._load_template()
|
||||||
self._declared_sections = None
|
self._declared_sections = None
|
||||||
@@ -22,10 +25,7 @@ class BaseDevice(ABC):
|
|||||||
@abstractmethod
|
@abstractmethod
|
||||||
def template(self) -> str:
|
def template(self) -> str:
|
||||||
"""
|
"""
|
||||||
Expected structure:
|
Name of the TTP template file used by this device parser.
|
||||||
Название файла с парсером ttp
|
|
||||||
Returns:
|
|
||||||
None
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def vlans(self) -> list[dict]:
|
def vlans(self) -> list[dict]:
|
||||||
@@ -33,15 +33,15 @@ class BaseDevice(ABC):
|
|||||||
Parse VLAN configuration from self.raw['vlans'].
|
Parse VLAN configuration from self.raw['vlans'].
|
||||||
|
|
||||||
Expected structure:
|
Expected structure:
|
||||||
[{"id": 10, "description": "MGMT"}, {"id": 15, "name": "SSH"}, ...]
|
[{"vlan_id": 10, "description": "MGMT"}, {"vlan_id": 15, "name": "SSH"}, ...]
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list[Vlans]: список VLAN из секции vlans,
|
list[Vlans]: VLANs from the vlans section, or an empty list
|
||||||
пустой список если секция отсутствует.
|
when the section is absent.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: если raw содержит некорректные данные.
|
ValueError: if raw data cannot be validated by the contract.
|
||||||
"""
|
""" # noqa: E501
|
||||||
return self.raw.get("vlans", [])
|
return self.raw.get("vlans", [])
|
||||||
|
|
||||||
def interfaces(self) -> list[dict]:
|
def interfaces(self) -> list[dict]:
|
||||||
@@ -49,11 +49,11 @@ class BaseDevice(ABC):
|
|||||||
Parse Interface configuration from self.raw['interfaces'].
|
Parse Interface configuration from self.raw['interfaces'].
|
||||||
|
|
||||||
Expected raw structure:
|
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:
|
Raises:
|
||||||
ValueError: если raw содержит некорректные данные.
|
ValueError: if raw data cannot be validated by the contract.
|
||||||
"""
|
""" # noqa: E501
|
||||||
return self.raw.get("interfaces", [])
|
return self.raw.get("interfaces", [])
|
||||||
|
|
||||||
def system(self) -> dict:
|
def system(self) -> dict:
|
||||||
@@ -64,13 +64,29 @@ class BaseDevice(ABC):
|
|||||||
{"model":"RB951Ui-2nD", serial_number: "B88C0B31117B", "version": "7.12.1"}
|
{"model":"RB951Ui-2nD", serial_number: "B88C0B31117B", "version": "7.12.1"}
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: если raw содержит некорректные данные.
|
ValueError: if raw data cannot be validated by the contract.
|
||||||
"""
|
"""
|
||||||
return self.raw.get("system", None)
|
return self.raw.get("system", None)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _as_list(data) -> list:
|
||||||
|
"""Normalize a TTP group result to a list.
|
||||||
|
|
||||||
|
TTP returns a single dict when a group matches exactly one entry and a
|
||||||
|
list when it matches several. Callers always expect a list.
|
||||||
|
"""
|
||||||
|
if data is None:
|
||||||
|
return []
|
||||||
|
if isinstance(data, dict):
|
||||||
|
return [data]
|
||||||
|
return data
|
||||||
|
|
||||||
def _validate_contract(self) -> dict:
|
def _validate_contract(self) -> dict:
|
||||||
|
if self.raw is None:
|
||||||
|
msg = f"Node {self.name} not found" if self.name else "Node not found"
|
||||||
|
raise OxiAPIError(msg, status_code=404)
|
||||||
system_data = self.system()
|
system_data = self.system()
|
||||||
interfaces_data = self.interfaces() or []
|
interfaces_data = self._as_list(self.interfaces())
|
||||||
result = {
|
result = {
|
||||||
"system": System(**system_data),
|
"system": System(**system_data),
|
||||||
"interfaces": [Interfaces(**item) for item in interfaces_data],
|
"interfaces": [Interfaces(**item) for item in interfaces_data],
|
||||||
@@ -80,22 +96,22 @@ class BaseDevice(ABC):
|
|||||||
if "vlans" in self._declared_sections:
|
if "vlans" in self._declared_sections:
|
||||||
if "vlans" not in self.raw:
|
if "vlans" not in self.raw:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"{self.__class__.__name__}: template '{self.template}' declares optional group "
|
f"{self.__class__.__name__}: template '{self.template}' "
|
||||||
f"'vlans', but TTP did not return it."
|
f"declares optional group 'vlans', but TTP did not return it."
|
||||||
)
|
)
|
||||||
vlans_data = self.vlans() or []
|
vlans_data = self._as_list(self.vlans())
|
||||||
result["vlans"] = [Vlans(**item) for item in vlans_data]
|
result["vlans"] = [Vlans(**item) for item in vlans_data]
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _load_template(self):
|
def _load_template(self):
|
||||||
"""Подгрузка темплейтов из папки models/templates"""
|
"""Load the device TTP template from models/templates."""
|
||||||
path = Path(__file__).parent / "models" / "templates" / self.template
|
path = Path(__file__).parent / "models" / "templates" / self.template
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
raise FileNotFoundError(f"Template {self.template} not found")
|
raise FileNotFoundError(f"Template {self.template} not found")
|
||||||
return path.read_text(encoding="utf-8")
|
return path.read_text(encoding="utf-8")
|
||||||
|
|
||||||
def _validate_template_groups(self) -> None:
|
def _validate_template_groups(self) -> None:
|
||||||
"""Проверяем только обязательные группы в template."""
|
"""Validate that the template declares all required groups."""
|
||||||
try:
|
try:
|
||||||
root = ET.fromstring(self._loaded_template)
|
root = ET.fromstring(self._loaded_template)
|
||||||
except ET.ParseError:
|
except ET.ParseError:
|
||||||
@@ -113,7 +129,14 @@ class BaseDevice(ABC):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _run_ttp(self) -> dict:
|
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()
|
||||||
|
res = parser.result()
|
||||||
|
if res[0][0]:
|
||||||
|
# raise OxiAPIError(f"Node {self.name} not found", status_code=404)
|
||||||
|
return None
|
||||||
p = ttp(data=self.config, template=self._loaded_template)
|
p = ttp(data=self.config, template=self._loaded_template)
|
||||||
p.parse()
|
p.parse()
|
||||||
raw: dict = p.result()[0][0]
|
raw: dict = p.result()[0][0]
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from ipaddress import IPv4Address
|
from ipaddress import IPv4Address
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
|
||||||
|
|
||||||
@@ -8,7 +9,7 @@ class Base(BaseModel):
|
|||||||
|
|
||||||
class System(BaseModel):
|
class System(BaseModel):
|
||||||
"""
|
"""
|
||||||
Requred
|
Required
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model: str
|
model: str
|
||||||
@@ -18,7 +19,7 @@ class System(BaseModel):
|
|||||||
|
|
||||||
class Interfaces(Base):
|
class Interfaces(Base):
|
||||||
"""
|
"""
|
||||||
Requred
|
Required
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name: str = Field(alias="interface")
|
name: str = Field(alias="interface")
|
||||||
@@ -32,7 +33,7 @@ class Vlans(Base):
|
|||||||
Optional
|
Optional
|
||||||
"""
|
"""
|
||||||
|
|
||||||
vlan_id: int = Field(alias="id")
|
vlan_id: int
|
||||||
name: str | None = Field(default=None, alias="description")
|
name: str | None = Field(default=None, alias="description")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ import pkgutil
|
|||||||
|
|
||||||
package = __package__
|
package = __package__
|
||||||
|
|
||||||
for loader, module_name, is_pkg in pkgutil.iter_modules(__path__):
|
for _, module_name, _ in pkgutil.iter_modules(__path__):
|
||||||
importlib.import_module(f"{package}.{module_name}")
|
importlib.import_module(f"{package}.{module_name}")
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from oxi.interfaces import register_parser
|
from oxi.interfaces import register_parser
|
||||||
from oxi.interfaces.base import BaseDevice
|
from oxi.interfaces.base import BaseDevice
|
||||||
|
from oxi.interfaces.utils import expand_vlan_range
|
||||||
|
|
||||||
|
|
||||||
@register_parser("eltex")
|
@register_parser("eltex")
|
||||||
@@ -17,27 +18,21 @@ class Eltex(BaseDevice):
|
|||||||
|
|
||||||
def vlans(self) -> list[dict]:
|
def vlans(self) -> list[dict]:
|
||||||
vlans_ttp = self.raw.get("vlans", [])
|
vlans_ttp = self.raw.get("vlans", [])
|
||||||
vlans = []
|
vlans: list[dict] = []
|
||||||
named_vlan = set()
|
named_vlan: set[str] = set()
|
||||||
for item in vlans_ttp:
|
for item in vlans_ttp:
|
||||||
if item.get("vlan_id"):
|
vlan_id = item.get("vlan_id")
|
||||||
named_vlan.add(item.get("vlan_id"))
|
if vlan_id:
|
||||||
|
named_vlan.add(str(vlan_id))
|
||||||
vlans.append(item)
|
vlans.append(item)
|
||||||
else:
|
continue
|
||||||
ids = item.get("vlan_ids", "")
|
|
||||||
tail = item.get("vlan_tail")
|
ids = item.get("vlan_ids", "")
|
||||||
if tail:
|
tail = item.get("vlan_tail")
|
||||||
ids = f"{ids},{tail}"
|
if tail:
|
||||||
for vid in ids:
|
ids = [*ids, tail] if isinstance(ids, list) else f"{ids},{tail}"
|
||||||
vid = vid.strip()
|
for vid in expand_vlan_range(ids):
|
||||||
if vid in named_vlan:
|
if vid in named_vlan:
|
||||||
continue
|
continue
|
||||||
vlans.append({"vlan_id": vid})
|
vlans.append({"vlan_id": vid})
|
||||||
return vlans
|
return vlans
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
with open("./test6.txt") as file:
|
|
||||||
data = file.read()
|
|
||||||
eltex = Eltex(data)
|
|
||||||
print(eltex.parse())
|
|
||||||
|
|||||||
@@ -6,17 +6,12 @@ class H3C(BaseDevice):
|
|||||||
template = "h3c.ttp"
|
template = "h3c.ttp"
|
||||||
|
|
||||||
def vlans(self) -> list[dict]:
|
def vlans(self) -> list[dict]:
|
||||||
vlan_list = self.raw["vlans"]
|
vlan_list = self.raw.get("vlans", [])
|
||||||
vlans = []
|
vlans: list[dict] = []
|
||||||
for item in vlan_list:
|
for item in vlan_list:
|
||||||
if item.get("vlans_id"):
|
vlan_ids = item.get("vlans_id")
|
||||||
vlans.extend([{'vlan_id': vln }for vln in item.get("vlans_id")])
|
if not vlan_ids:
|
||||||
else:
|
|
||||||
vlans.append(item)
|
vlans.append(item)
|
||||||
|
continue
|
||||||
|
vlans.extend({"vlan_id": vlan_id} for vlan_id in vlan_ids)
|
||||||
return vlans
|
return vlans
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
with open("./test5.txt") as file:
|
|
||||||
data = file.read()
|
|
||||||
h3c = H3C(data)
|
|
||||||
print(h3c.parse())
|
|
||||||
@@ -6,13 +6,6 @@ from oxi.interfaces.base import BaseDevice
|
|||||||
class Huawei(BaseDevice):
|
class Huawei(BaseDevice):
|
||||||
template = "huawei.ttp"
|
template = "huawei.ttp"
|
||||||
|
|
||||||
def vlans(self):
|
def vlans(self) -> list[dict]:
|
||||||
vlan_ids = self.raw["vlans"].get("vlan_ids")
|
vlan_ids = self.raw.get("vlans", {}).get("vlan_ids", [])
|
||||||
return [{"vlan_id": vlan} for vlan in vlan_ids]
|
return [{"vlan_id": vlan} for vlan in vlan_ids]
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
with open("./test4.txt") as file:
|
|
||||||
data = file.read()
|
|
||||||
huawei = Huawei(data)
|
|
||||||
print(huawei.parse())
|
|
||||||
|
|||||||
@@ -1,24 +1,14 @@
|
|||||||
from ipaddress import ip_interface
|
from ipaddress import ip_interface
|
||||||
|
|
||||||
from oxi.interfaces import register_parser
|
from oxi.interfaces import register_parser
|
||||||
from oxi.interfaces.base import BaseDevice
|
from oxi.interfaces.base import BaseDevice
|
||||||
|
from oxi.interfaces.utils import decode_utf
|
||||||
|
|
||||||
|
|
||||||
@register_parser(["NDMS", "keenetic", "KeeneticOS"])
|
@register_parser(["NDMS", "keenetic", "KeeneticOS"])
|
||||||
class Keenetic(BaseDevice):
|
class Keenetic(BaseDevice):
|
||||||
template = "keenetic.ttp"
|
template = "keenetic.ttp"
|
||||||
|
|
||||||
def _decode_utf(self, text: str):
|
|
||||||
if "\\x" in text:
|
|
||||||
desc = text.strip('"')
|
|
||||||
decoded = (
|
|
||||||
desc.encode("utf-8")
|
|
||||||
.decode("unicode_escape")
|
|
||||||
.encode("latin1")
|
|
||||||
.decode("utf-8")
|
|
||||||
)
|
|
||||||
return decoded
|
|
||||||
return text
|
|
||||||
|
|
||||||
def interfaces(self):
|
def interfaces(self):
|
||||||
interfaces: list[dict] = self.raw["interfaces"]
|
interfaces: list[dict] = self.raw["interfaces"]
|
||||||
for item in interfaces:
|
for item in interfaces:
|
||||||
@@ -29,7 +19,7 @@ class Keenetic(BaseDevice):
|
|||||||
item["mask"] = ipaddress.network.prefixlen
|
item["mask"] = ipaddress.network.prefixlen
|
||||||
item.pop("netmask", "Key not found")
|
item.pop("netmask", "Key not found")
|
||||||
if item.get("description"):
|
if item.get("description"):
|
||||||
decoded = self._decode_utf(item.get("description", ""))
|
decoded = decode_utf(item.get("description", ""))
|
||||||
item["description"] = decoded
|
item["description"] = decoded
|
||||||
return interfaces
|
return interfaces
|
||||||
|
|
||||||
@@ -37,13 +27,6 @@ class Keenetic(BaseDevice):
|
|||||||
vlans = self.raw["vlans"]
|
vlans = self.raw["vlans"]
|
||||||
for item in vlans:
|
for item in vlans:
|
||||||
if item.get("description"):
|
if item.get("description"):
|
||||||
decoded = self._decode_utf(item.get("description", ""))
|
decoded = decode_utf(item.get("description", ""))
|
||||||
item["description"] = decoded
|
item["description"] = decoded
|
||||||
return vlans
|
return vlans
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
with open("./test2.txt") as file:
|
|
||||||
data = file.read()
|
|
||||||
mikr = Keenetic(data)
|
|
||||||
print(mikr.parse().model_dump_json())
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import os
|
|
||||||
from oxi.interfaces import register_parser
|
from oxi.interfaces import register_parser
|
||||||
from oxi.interfaces.base import BaseDevice
|
from oxi.interfaces.base import BaseDevice
|
||||||
|
|
||||||
@@ -6,11 +5,3 @@ from oxi.interfaces.base import BaseDevice
|
|||||||
@register_parser(["routeros", "ros", "mikrotik"])
|
@register_parser(["routeros", "ros", "mikrotik"])
|
||||||
class Mikrotik(BaseDevice):
|
class Mikrotik(BaseDevice):
|
||||||
template = "mikrotik.ttp"
|
template = "mikrotik.ttp"
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
print(os.path.abspath(os.curdir))
|
|
||||||
with open("./test.txt") as file:
|
|
||||||
data = file.read()
|
|
||||||
mikr = Mikrotik(data)
|
|
||||||
print(mikr.parse().json())
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import os
|
|
||||||
from oxi.interfaces import register_parser
|
from oxi.interfaces import register_parser
|
||||||
from oxi.interfaces.base import BaseDevice
|
from oxi.interfaces.base import BaseDevice
|
||||||
|
from oxi.interfaces.utils import expand_vlan_range
|
||||||
|
|
||||||
|
|
||||||
@register_parser(["QTECH"])
|
@register_parser(["QTECH"])
|
||||||
@@ -8,30 +8,22 @@ class Qtech(BaseDevice):
|
|||||||
template = "qtech.ttp"
|
template = "qtech.ttp"
|
||||||
|
|
||||||
def vlans(self) -> list[dict]:
|
def vlans(self) -> list[dict]:
|
||||||
vlans_ttp = self.raw["vlans"]
|
vlans_ttp = self.raw.get("vlans", [])
|
||||||
vlans = []
|
vlans: list[dict] = []
|
||||||
named_vlan = set()
|
named_vlan: set[str] = set()
|
||||||
for item in vlans_ttp:
|
for item in vlans_ttp:
|
||||||
if item.get("vlan_id"):
|
vlan_id = item.get("vlan_id")
|
||||||
named_vlan.add(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)
|
vlans.append(item)
|
||||||
else:
|
continue
|
||||||
ids = item.get("vlan_ids", "")
|
|
||||||
tail = item.get("vlan_tail")
|
ids = item.get("vlan_ids") or vlan_id or ""
|
||||||
if tail:
|
tail = item.get("vlan_tail")
|
||||||
ids = f"{ids},{tail}"
|
if tail:
|
||||||
for vid in ids.split(","):
|
ids = [*ids, tail] if isinstance(ids, list) else f"{ids},{tail}"
|
||||||
vid = vid.strip()
|
for vid in expand_vlan_range(ids):
|
||||||
if vid in named_vlan:
|
if vid in named_vlan:
|
||||||
continue
|
continue
|
||||||
vlans.append({"vlan_id": vid})
|
vlans.append({"vlan_id": vid})
|
||||||
return vlans
|
return vlans
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|||||||
23
oxi/interfaces/models/quasar.py
Normal file
23
oxi/interfaces/models/quasar.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
from oxi.interfaces import BaseDevice, register_parser
|
||||||
|
|
||||||
|
|
||||||
|
@register_parser(["quasar", "qos"])
|
||||||
|
class Quasar(BaseDevice):
|
||||||
|
template = "quasar.ttp"
|
||||||
|
|
||||||
|
def interfaces(self) -> list[dict]:
|
||||||
|
ether_interface: dict = self.raw.get("interfaces", {})
|
||||||
|
interfaces: list[dict] = []
|
||||||
|
bulk_interfaces: dict = self.raw.get("bulkinterfaces", {})
|
||||||
|
for key, value in bulk_interfaces.items():
|
||||||
|
interfaces.append(
|
||||||
|
{
|
||||||
|
"interface": key,
|
||||||
|
"description": value.get("description"),
|
||||||
|
"ip_address": value.get("ip_address"),
|
||||||
|
"mask": value.get("mask"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if ether_interface:
|
||||||
|
interfaces.append(ether_interface)
|
||||||
|
return interfaces
|
||||||
@@ -1,41 +1,20 @@
|
|||||||
<doc>
|
<doc>
|
||||||
Базовый шаблон для нового устройства. Скопируйте этот файл, переименуйте
|
Base template for a new device parser. Copy this file, rename it to
|
||||||
в <vendor>.ttp и заполните группы под формат конфигурации вашего устройства.
|
<vendor>.ttp, and fill the groups for the target configuration format.
|
||||||
|
|
||||||
Обязательные группы: system, interfaces.
|
Required groups: system, interfaces.
|
||||||
Опциональная группа: vlans — добавляйте только если устройство поддерживает VLAN.
|
Optional group: vlans. Add it only when VLAN parsing is implemented.
|
||||||
|
|
||||||
--- Группа system ---
|
system must return one dictionary with: model, serial_number, version.
|
||||||
Должна возвращать одиночный словарь с полями:
|
interfaces must return a list of dictionaries with: interface, ip_address,
|
||||||
model (str) — модель устройства
|
mask, description. Use a prefix length for mask; convert dotted decimal masks
|
||||||
serial_number (str) — серийный номер
|
with `to_cidr` or in the device class.
|
||||||
version (str) — версия прошивки
|
vlans must return dictionaries with vlan_id and optional name/description.
|
||||||
|
|
||||||
--- Группа interfaces ---
|
Useful TTP modifiers: ORPHRASE, _start_, strip(), replace(), exclude(),
|
||||||
Должна возвращать список словарей. Каждый элемент:
|
ignore, ignore('.*'), to_cidr, unrange(), split().
|
||||||
interface (str) — имя интерфейса (alias поля name)
|
|
||||||
ip_address (str|None) — IPv4-адрес
|
|
||||||
mask (int|None) — длина префикса (напр. 24)
|
|
||||||
description (str|None) — описание интерфейса
|
|
||||||
|
|
||||||
Если устройство возвращает маску в виде 255.255.255.0, конвертируйте
|
See docs/templates.md for details.
|
||||||
её в 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
|
|
||||||
</doc>
|
</doc>
|
||||||
<vars>
|
<vars>
|
||||||
default_system = {
|
default_system = {
|
||||||
|
|||||||
@@ -1,41 +1,10 @@
|
|||||||
<doc>
|
<doc>
|
||||||
Базовый шаблон для нового устройства. Скопируйте этот файл, переименуйте
|
Eltex configuration parser.
|
||||||
в <vendor>.ttp и заполните группы под формат конфигурации вашего устройства.
|
|
||||||
|
|
||||||
Обязательные группы: system, interfaces.
|
The system group reads software version data and the serial group extracts
|
||||||
Опциональная группа: vlans — добавляйте только если устройство поддерживает VLAN.
|
serial numbers from the unit table. The interfaces group parses interface IP
|
||||||
|
settings. The vlans group supports named VLAN interfaces and compressed VLAN
|
||||||
--- Группа system ---
|
lists.
|
||||||
Должна возвращать одиночный словарь с полями:
|
|
||||||
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
|
|
||||||
</doc>
|
</doc>
|
||||||
<vars>
|
<vars>
|
||||||
default_system = {
|
default_system = {
|
||||||
@@ -57,11 +26,13 @@ Active-image: {{ ignore }} {{ _start_ }}
|
|||||||
<group name="interfaces">
|
<group name="interfaces">
|
||||||
interface {{ interface | ORPHRASE }}
|
interface {{ interface | ORPHRASE }}
|
||||||
ip address {{ ip_address }} {{ mask | to_cidr }}
|
ip address {{ ip_address }} {{ mask | to_cidr }}
|
||||||
|
name {{ description | ORPHRASE}}
|
||||||
|
description {{ description | ORPHRASE }}
|
||||||
</group>
|
</group>
|
||||||
|
|
||||||
<group name="vlans">
|
<group name="vlans">
|
||||||
interface vlan {{ vlan_id | _start_ }}
|
interface vlan {{ vlan_id | _start_ }}
|
||||||
name {{ name }}
|
name {{ name | ORPHRASE }}
|
||||||
|
|
||||||
vlan {{ _db_ | _start_ }}
|
vlan {{ _db_ | _start_ }}
|
||||||
vlan {{ vlan_ids | joinmatches(',') | unrange("-", ",") | split(",")}}
|
vlan {{ vlan_ids | joinmatches(',') | unrange("-", ",") | split(",")}}
|
||||||
|
|||||||
@@ -1,41 +1,9 @@
|
|||||||
<doc>
|
<doc>
|
||||||
Базовый шаблон для нового устройства. Скопируйте этот файл, переименуйте
|
H3C configuration parser.
|
||||||
в <vendor>.ttp и заполните группы под формат конфигурации вашего устройства.
|
|
||||||
|
|
||||||
Обязательные группы: system, interfaces.
|
The system group reads boot image version and board model data. The interfaces
|
||||||
Опциональная группа: vlans — добавляйте только если устройство поддерживает VLAN.
|
group parses interface IP settings. The vlans groups parse both named VLANs and
|
||||||
|
range-style VLAN declarations.
|
||||||
--- Группа 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
|
|
||||||
</doc>
|
</doc>
|
||||||
<vars>
|
<vars>
|
||||||
default_system = {
|
default_system = {
|
||||||
@@ -53,6 +21,7 @@ default_system = {
|
|||||||
|
|
||||||
<group name="interfaces">
|
<group name="interfaces">
|
||||||
interface {{ interface }}
|
interface {{ interface }}
|
||||||
|
description {{ description | ORPHRASE }}
|
||||||
ip address {{ ip_address }} {{ mask | to_cidr }}
|
ip address {{ ip_address }} {{ mask | to_cidr }}
|
||||||
</group>
|
</group>
|
||||||
|
|
||||||
|
|||||||
@@ -1,40 +1,9 @@
|
|||||||
<doc>
|
<doc>
|
||||||
Базовый шаблон для нового устройства. Скопируйте этот файл, переименуйте
|
Huawei VRP configuration parser.
|
||||||
в <vendor>.ttp и заполните группы под формат конфигурации вашего устройства.
|
|
||||||
|
|
||||||
Обязательные группы: system, interfaces.
|
The system group reads VRP version and slot ESN data. The interfaces group
|
||||||
Опциональная группа: vlans — добавляйте только если устройство поддерживает VLAN.
|
parses interface blocks and converts dotted decimal masks to prefix lengths.
|
||||||
|
The vlans group parses `vlan batch` declarations and emits VLAN IDs.
|
||||||
--- Группа 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
|
|
||||||
</doc>
|
</doc>
|
||||||
<vars>
|
<vars>
|
||||||
default_system = {
|
default_system = {
|
||||||
@@ -45,12 +14,13 @@ default_system = {
|
|||||||
</vars>
|
</vars>
|
||||||
|
|
||||||
<group name="system" default="default_system">
|
<group name="system" default="default_system">
|
||||||
# VRP (R) software, Version {{ version }} ({{ model }} {{ serial_number }})
|
# VRP (R) software, Version {{ version }} ({{ model }} {{ _line_ }}
|
||||||
|
# ESN of slot {{ slot_number }}: {{ serial_number }}
|
||||||
</group>
|
</group>
|
||||||
|
|
||||||
<group name="interfaces">
|
<group name="interfaces">
|
||||||
interface {{ interface }}
|
interface {{ interface }}
|
||||||
description {{ description }}
|
description {{ description | ORPHRASE }}
|
||||||
ip address {{ ip_address }} {{ mask | to_cidr }}
|
ip address {{ ip_address }} {{ mask | to_cidr }}
|
||||||
</group>
|
</group>
|
||||||
<group name="vlans">
|
<group name="vlans">
|
||||||
|
|||||||
@@ -42,6 +42,6 @@ add comment={{ comment | _start_ | ORPHRASE | exclude("disabled=") | strip('"')}
|
|||||||
## disabled with comment with/without quotes
|
## disabled with comment with/without quotes
|
||||||
add comment={{ comment | _start_ | ORPHRASE | exclude("disabled=") | strip('"')}} disabled={{ disabled | replace("yes","True") | strip('"') }} interface={{ interface }} name={{ name | ORPHRASE | strip('"') }} vlan-id={{ vlan_id }}
|
add comment={{ comment | _start_ | ORPHRASE | exclude("disabled=") | strip('"')}} disabled={{ disabled | replace("yes","True") | strip('"') }} interface={{ interface }} name={{ name | ORPHRASE | strip('"') }} vlan-id={{ vlan_id }}
|
||||||
## disabled no comment
|
## disabled no comment
|
||||||
add interface={{ interface | _start_ }} name={{ name | ORPHRASE | strip('"') }} vlan-id={{ vlan_id }} disabled={{ disabled | replace("yes","True") | strip('"') }}
|
add disabled={{ disabled | _start_ | replace("yes","True") | strip('"') }} interface={{ interface }} name={{ name | ORPHRASE | strip('"') }} vlan-id={{ vlan_id }}
|
||||||
</group>
|
</group>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
<doc>
|
<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>
|
</doc>
|
||||||
<vars>
|
<vars>
|
||||||
default_system = {
|
default_system = {
|
||||||
@@ -9,18 +18,23 @@ default_system = {
|
|||||||
</vars>
|
</vars>
|
||||||
|
|
||||||
<group name="system" default="default_system">
|
<group name="system" default="default_system">
|
||||||
! {{ model | ORPHRASE }} Series Software, Version {{ ignore }} Build {{ version | strip(",") }}{{ ignore('.*') }}
|
! {{ model | ORPHRASE | _start_ }} Series Software, Version {{ ignore }} Build {{ version | strip(",") }}{{ ignore('.*') }}
|
||||||
! Serial num:{{ serial_number | strip(",") }}{{ ignore('.*') }}
|
! Serial num:{{ serial_number | strip(",") }}{{ ignore('.*') }}
|
||||||
|
|
||||||
|
! System description : {{ description | PHRASE | _start_ }}({{ model }}) By {{ vendor }}
|
||||||
|
! System description : {{ description | PHRASE | _start_ }}({{ model }})
|
||||||
|
! System software version : {{ description | PHRASE }}, Release({{ version }})
|
||||||
|
! System serial number : {{ serial_number }}
|
||||||
</group>
|
</group>
|
||||||
|
|
||||||
<group name="interfaces">
|
<group name="interfaces">
|
||||||
interface {{ interface }}
|
interface {{ interface | ORPHRASE }}
|
||||||
description {{ description | ORPHRASE }}
|
description {{ description | ORPHRASE }}
|
||||||
ip address {{ ip_address }} {{ mask | to_cidr }}
|
ip address {{ ip_address }} {{ mask | to_cidr }}
|
||||||
</group>
|
</group>
|
||||||
|
|
||||||
<group name="vlans">
|
<group name="vlans">
|
||||||
vlan {{ vlan_ids | contains(",", "-") | unrange("-", ",") | _start_ }}
|
vlan {{ vlan_ids | contains(",", "-") | unrange("-", ",") }}
|
||||||
,{{ vlan_tail | unrange("-", ",") }}
|
,{{ vlan_tail | unrange("-", ",") }}
|
||||||
vlan {{ vlan_id | _start_ }}
|
vlan {{ vlan_id | _start_ }}
|
||||||
name {{ name | ORPHRASE }}
|
name {{ name | ORPHRASE }}
|
||||||
|
|||||||
35
oxi/interfaces/models/templates/quasar.ttp
Normal file
35
oxi/interfaces/models/templates/quasar.ttp
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<doc>
|
||||||
|
Quasar configuration parser.
|
||||||
|
|
||||||
|
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 = {
|
||||||
|
"model": "",
|
||||||
|
"serial_number": "",
|
||||||
|
"version": ""
|
||||||
|
}
|
||||||
|
</vars>
|
||||||
|
|
||||||
|
<group name="system" default="default_system">
|
||||||
|
# Component Version {{ _start_ }}
|
||||||
|
# Assembly {{ version }}
|
||||||
|
# Product Name {{ model | ORPHRASE }}
|
||||||
|
# Product Serial Number {{ serial_number }}
|
||||||
|
|
||||||
|
# Subsystem Version {{ _start_ }}
|
||||||
|
# Engine {{ version }}
|
||||||
|
# Product Name {{ model | ORPHRASE }}
|
||||||
|
# Product Serial Number {{ serial_number }}
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group name="interfaces">
|
||||||
|
{{ ignore("\\s*") }}config {{ interface }} ipv4 address {{ ip_address }}/{{ mask }} gateway {{ gateway }} {{ ignore }}
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group name="bulkinterfaces.{{ interface }}">
|
||||||
|
{{ ignore("\\s*") }}config interface {{ interface | _start_ }} description {{ description | ORPHRASE | strip('"')}}
|
||||||
|
</group>
|
||||||
39
oxi/interfaces/utils.py
Normal file
39
oxi/interfaces/utils.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def decode_utf(text: str):
|
||||||
|
"""Decode escaped UTF-8 descriptions."""
|
||||||
|
if "\\x" in text:
|
||||||
|
desc = text.strip('"')
|
||||||
|
decoded = (
|
||||||
|
desc.encode("utf-8")
|
||||||
|
.decode("unicode_escape")
|
||||||
|
.encode("latin1")
|
||||||
|
.decode("utf-8")
|
||||||
|
)
|
||||||
|
return decoded
|
||||||
|
return text
|
||||||
20
oxi/node.py
20
oxi/node.py
@@ -1,7 +1,10 @@
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from .view import NodeView
|
from requests import HTTPError
|
||||||
|
|
||||||
|
from oxi.exception import OxiAPIError
|
||||||
|
|
||||||
|
from .view import NodeView
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from requests import Session
|
from requests import Session
|
||||||
@@ -11,15 +14,16 @@ class Node:
|
|||||||
def __init__(self, session: "Session", base_url: str):
|
def __init__(self, session: "Session", base_url: str):
|
||||||
self._session = session
|
self._session = session
|
||||||
self._base_url = base_url
|
self._base_url = base_url
|
||||||
self._data = None
|
|
||||||
|
|
||||||
def __call__(self, name: str) -> NodeView:
|
def __call__(self, name: str) -> NodeView:
|
||||||
url = f"{self._base_url}/node/show/{name}"
|
try:
|
||||||
if not url.endswith(".json"):
|
url = f"{self._base_url}/node/show/{name}"
|
||||||
url += ".json"
|
if not url.endswith(".json"):
|
||||||
response = self._session.get(url)
|
url += ".json"
|
||||||
if response.status_code == 500:
|
response = self._session.get(url)
|
||||||
raise ValueError(f"page {url} not found")
|
response.raise_for_status()
|
||||||
|
except HTTPError as e:
|
||||||
|
raise OxiAPIError.from_http_error(e, context=f"Node {name}") from e
|
||||||
return NodeView(
|
return NodeView(
|
||||||
session=self._session, base_url=self._base_url, data=response.json()
|
session=self._session, base_url=self._base_url, data=response.json()
|
||||||
)
|
)
|
||||||
|
|||||||
36
oxi/view.py
36
oxi/view.py
@@ -3,7 +3,6 @@ from typing import TYPE_CHECKING
|
|||||||
|
|
||||||
from .conf import NodeConfig
|
from .conf import NodeConfig
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from requests import Session
|
from requests import Session
|
||||||
|
|
||||||
@@ -14,22 +13,47 @@ class NodeView:
|
|||||||
self._base_url = base_url
|
self._base_url = base_url
|
||||||
self._data = data
|
self._data = data
|
||||||
|
|
||||||
|
def _updater(self) -> int:
|
||||||
|
response = self._session.get(f"{self._base_url}/node/next/{self.full_name}")
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.status_code
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ip(self):
|
def name(self) -> str:
|
||||||
|
return self._data.get("name")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ip(self) -> str:
|
||||||
return self._data.get("ip")
|
return self._data.get("ip")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def full_name(self):
|
def full_name(self) -> str:
|
||||||
return self._data.get("full_name")
|
return self._data.get("full_name")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def group(self):
|
def group(self) -> str:
|
||||||
return self._data.get("group")
|
return self._data.get("group")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def model(self):
|
def model(self) -> str:
|
||||||
return self._data.get("model")
|
return self._data.get("model")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_status(self) -> str:
|
||||||
|
last = self._data.get("last") or {}
|
||||||
|
return last.get("status")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_check(self) -> str:
|
||||||
|
last = self._data.get("last") or {}
|
||||||
|
return last.get("start")
|
||||||
|
|
||||||
|
def refresh(self) -> str:
|
||||||
|
result = self._updater()
|
||||||
|
if result != 200:
|
||||||
|
raise ValueError(f"Failed to refresh node {self.full_name}")
|
||||||
|
return "OK"
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def config(self):
|
def config(self) -> NodeConfig:
|
||||||
return NodeConfig(self._session, self.full_name, self.model, self._base_url)
|
return NodeConfig(self._session, self.full_name, self.model, self._base_url)
|
||||||
|
|||||||
@@ -1,19 +1,38 @@
|
|||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools>=61"]
|
requires = ["setuptools>=77"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "oxipy"
|
name = "oxipy"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "Oxi API client"
|
description = "Python client for Oxidized API with TTP-based config parsing"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
license = "Apache-2.0"
|
||||||
|
license-files = ["LICENSE"]
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
classifiers = [
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pydantic>=2.12.5",
|
"pydantic>=2.12.5",
|
||||||
"requests>=2.32.5",
|
"requests>=2.32.5",
|
||||||
"ttp>=0.10.0",
|
"ttp>=0.10.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"pytest>=9.0.3",
|
||||||
|
"responses>=0.26.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Repository = "https://github.com/sttarsky/oxipy"
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
pythonpath = ["."]
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
where = ["."]
|
where = ["."]
|
||||||
include = ["oxi*"]
|
include = ["oxi*"]
|
||||||
@@ -23,3 +42,17 @@ include-package-data = true
|
|||||||
|
|
||||||
[tool.setuptools.package-data]
|
[tool.setuptools.package-data]
|
||||||
"oxi" = ["**/*.ttp"]
|
"oxi" = ["**/*.ttp"]
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = [
|
||||||
|
"pytest>=9.0.3",
|
||||||
|
"responses>=0.26.1",
|
||||||
|
"ruff>=0.15.17",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
target-version = "py310"
|
||||||
|
line-length = 88
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = ["E", "F", "I", "UP", "B"]
|
||||||
|
|||||||
8
tests/conftest.py
Normal file
8
tests/conftest.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
FIXTURES = Path(__file__).parent / "fixtures"
|
||||||
|
|
||||||
|
|
||||||
|
def load(device: str, name: str = "config.conf") -> str:
|
||||||
|
"""Read a device config fixture from tests/fixtures/<device>/<name>."""
|
||||||
|
return (FIXTURES / device / name).read_text(encoding="utf-8")
|
||||||
55
tests/fixtures/eltex/config.conf
vendored
Normal file
55
tests/fixtures/eltex/config.conf
vendored
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
!
|
||||||
|
Active-image: flash://system/images/mes3300-669-3R3.ros
|
||||||
|
! Version: 6.6.9.3
|
||||||
|
! Commit: 3a5c2e39
|
||||||
|
! Build: 3 (master)
|
||||||
|
! MD5 Digest: 7bc289cc18be560954bd5cb0afd9b2d5
|
||||||
|
! Date: 22-Sep-2025
|
||||||
|
! Time: 12:38:20
|
||||||
|
! Inactive-image: flash://system/images/_image1.bin
|
||||||
|
! Version: 6.6.2
|
||||||
|
! Commit: 3ebc7503
|
||||||
|
! Build: 5 (master)
|
||||||
|
! MD5 Digest: a3f15a788c97c71e07e90d84c0ff3b12
|
||||||
|
! Date: 20-Nov-2023
|
||||||
|
! Time: 16:39:20
|
||||||
|
!
|
||||||
|
! Unit MAC address Hardware version Serial number
|
||||||
|
! ---- ----------------- ---------------- -------------
|
||||||
|
! 1 90:54:b7:6b:9d:40 01.01.01 ESG7007778
|
||||||
|
! 2 90:54:b7:6b:bb:80 01.01.01 ESG7007777
|
||||||
|
!
|
||||||
|
!
|
||||||
|
!
|
||||||
|
interface TenGigabitEthernet1/0/2
|
||||||
|
shutdown
|
||||||
|
description FREE
|
||||||
|
storm-control broadcast pps 3000
|
||||||
|
storm-control multicast pps 3000
|
||||||
|
exit
|
||||||
|
!
|
||||||
|
interface TenGigabitEthernet1/0/11
|
||||||
|
shutdown
|
||||||
|
description FREE
|
||||||
|
storm-control broadcast pps 3000
|
||||||
|
storm-control multicast pps 3000
|
||||||
|
exit
|
||||||
|
!
|
||||||
|
interface vlan 1700
|
||||||
|
name sw-test_HW
|
||||||
|
ip address 13.36.8.1 255.255.255.0
|
||||||
|
exit
|
||||||
|
!
|
||||||
|
vlan database
|
||||||
|
vlan 114-115,120,130,414,610,999-1000,1701-1703,1705,1801,2001,2011
|
||||||
|
vlan 2021-2022,3157-3158,3333-3334
|
||||||
|
exit
|
||||||
|
!
|
||||||
|
interface vlan 666
|
||||||
|
name test
|
||||||
|
exit
|
||||||
|
!
|
||||||
|
interface vlan 777
|
||||||
|
name test2
|
||||||
|
exit
|
||||||
|
!
|
||||||
137
tests/fixtures/eltex/config.expected.json
vendored
Normal file
137
tests/fixtures/eltex/config.expected.json
vendored
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
{
|
||||||
|
"system": {
|
||||||
|
"model": "",
|
||||||
|
"serial_number": "ESG7007778",
|
||||||
|
"version": "6.6.9.3"
|
||||||
|
},
|
||||||
|
"interfaces": [
|
||||||
|
{
|
||||||
|
"interface": "TenGigabitEthernet1/0/2",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": "FREE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "TenGigabitEthernet1/0/11",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": "FREE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "vlan 1700",
|
||||||
|
"ip_address": "13.36.8.1",
|
||||||
|
"mask": 24,
|
||||||
|
"description": "sw-test_HW"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "vlan 666",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": "test"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "vlan 777",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": "test2"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"vlans": [
|
||||||
|
{
|
||||||
|
"vlan_id": 1700,
|
||||||
|
"description": "sw-test_HW"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 114,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 115,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 120,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 130,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 414,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 610,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 999,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 1000,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 1701,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 1702,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 1703,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 1705,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 1801,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 2001,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 2011,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 2021,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 2022,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 3157,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 3158,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 3333,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 3334,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 666,
|
||||||
|
"description": "test"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 777,
|
||||||
|
"description": "test2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
1
tests/fixtures/eltex/not_found.conf
vendored
Normal file
1
tests/fixtures/eltex/not_found.conf
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
node not found
|
||||||
169
tests/fixtures/h3c/config.conf
vendored
Normal file
169
tests/fixtures/h3c/config.conf
vendored
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
# H3C Comware Software, Version 7.1.070, Release 6616P01
|
||||||
|
# Copyright (c) 2004-2021 New H3C Technologies Co., Ltd. All rights reserved.
|
||||||
|
# Last reboot reason : User reboot
|
||||||
|
#
|
||||||
|
# Boot image: flash:/s9820-cmw710-boot-r6616p01.bin
|
||||||
|
# Boot image version: 7.1.070, Release 6616P01
|
||||||
|
# Compiled May 06 2021 11:00:00
|
||||||
|
# System image: flash:/s9820-cmw710-system-r6616p01.bin
|
||||||
|
# System image version: 7.1.070, Release 6616P01
|
||||||
|
# Compiled May 06 2021 11:00:00
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# MPU(M) Slot 1:
|
||||||
|
# H3C S9820-64H MPU(M) with 1 C2538 Processor(s)
|
||||||
|
# BOARD TYPE: S9820-64H
|
||||||
|
# DRAM: 8192M bytes
|
||||||
|
# FLASH: 3630M bytes
|
||||||
|
# NVRAM: 0K bytes
|
||||||
|
# PCB 1 Version: VER.A
|
||||||
|
# PCB 2 Version: VER.B
|
||||||
|
# PCB 3 Version: VER.A
|
||||||
|
# PCB 4 Version: VER.A
|
||||||
|
# Basic BootWare Version: 105
|
||||||
|
# Extended BootWare Version: 108
|
||||||
|
# CPLD 1 Version: 002
|
||||||
|
# CPLD 2 Version: 027
|
||||||
|
# CPLD 3 Version: 002
|
||||||
|
# CPLD 4 Version: 002
|
||||||
|
# FPGA 1 Version: 001
|
||||||
|
# Release Version: H3C S9820-64H-6616P01
|
||||||
|
# Patch Version: None
|
||||||
|
# Reboot Cause: UserReboot
|
||||||
|
# [SubSlot 0] 64QSFP28
|
||||||
|
#
|
||||||
|
# MPU(S) Slot 2:
|
||||||
|
# H3C S9820-64H MPU(S) with 1 C2538 Processor(s)
|
||||||
|
# BOARD TYPE: S9820-64H
|
||||||
|
# DRAM: 8192M bytes
|
||||||
|
# FLASH: 3630M bytes
|
||||||
|
# NVRAM: 0K bytes
|
||||||
|
# PCB 1 Version: VER.A
|
||||||
|
# PCB 2 Version: VER.B
|
||||||
|
# PCB 3 Version: VER.A
|
||||||
|
# PCB 4 Version: VER.A
|
||||||
|
# Basic BootWare Version: 105
|
||||||
|
# Extended BootWare Version: 108
|
||||||
|
# CPLD 1 Version: 002
|
||||||
|
# CPLD 2 Version: 027
|
||||||
|
# CPLD 3 Version: 002
|
||||||
|
# CPLD 4 Version: 002
|
||||||
|
# FPGA 1 Version: 001
|
||||||
|
# Release Version: H3C S9820-64H-6616P01
|
||||||
|
# Patch Version: None
|
||||||
|
# Reboot Cause: IRFMergeReboot
|
||||||
|
# [SubSlot 0] 64QSFP28
|
||||||
|
# Slot Type State Subslot Soft Ver Patch Ver
|
||||||
|
# 1 S9820-64H Master 0 S9820-64H-6616P01 None
|
||||||
|
# 2 S9820-64H Standby 0 S9820-64H-6616P01 None
|
||||||
|
#
|
||||||
|
vlan 1
|
||||||
|
#
|
||||||
|
vlan 12
|
||||||
|
name BGP to OSPF1
|
||||||
|
#
|
||||||
|
vlan 13
|
||||||
|
name BGP to OSPF2
|
||||||
|
#
|
||||||
|
vlan 15
|
||||||
|
name HW_TEST_1
|
||||||
|
description HW_TEST_1
|
||||||
|
#
|
||||||
|
vlan 222
|
||||||
|
name MGMT
|
||||||
|
description MGMT
|
||||||
|
#
|
||||||
|
vlan 222
|
||||||
|
#
|
||||||
|
vlan 1112 to 1116
|
||||||
|
#
|
||||||
|
vlan 1122
|
||||||
|
name DATA
|
||||||
|
#
|
||||||
|
vlan 1123
|
||||||
|
#
|
||||||
|
vlan 1200
|
||||||
|
#
|
||||||
|
vlan 1512
|
||||||
|
#
|
||||||
|
vlan 1513
|
||||||
|
description cluster HW_TEST_1
|
||||||
|
#
|
||||||
|
vlan 2000
|
||||||
|
description cluster HW_TEST_2
|
||||||
|
#
|
||||||
|
vlan 3377
|
||||||
|
name VRF3377
|
||||||
|
#
|
||||||
|
irf-port 1/2
|
||||||
|
port group interface HundredGigE1/0/63
|
||||||
|
port group interface HundredGigE1/0/64
|
||||||
|
#
|
||||||
|
irf-port 2/1
|
||||||
|
port group interface HundredGigE2/0/63
|
||||||
|
port group interface HundredGigE2/0/64
|
||||||
|
#
|
||||||
|
interface Bridge-Aggregation1
|
||||||
|
description HW_TEST_1
|
||||||
|
port link-type trunk
|
||||||
|
undo port trunk permit vlan 1
|
||||||
|
port trunk permit vlan 221 1112 to 1116 1512 2000
|
||||||
|
link-aggregation mode dynamic
|
||||||
|
#
|
||||||
|
interface Bridge-Aggregation2
|
||||||
|
description HW_TEST_2
|
||||||
|
port link-type trunk
|
||||||
|
undo port trunk permit vlan 1
|
||||||
|
port trunk permit vlan 221 1112 to 1116 1512
|
||||||
|
link-aggregation mode dynamic
|
||||||
|
#
|
||||||
|
interface Bridge-Aggregation3
|
||||||
|
description HW_TEST_3
|
||||||
|
port link-type trunk
|
||||||
|
undo port trunk permit vlan 1
|
||||||
|
port trunk permit vlan 221 1112 to 1116 1512 2000
|
||||||
|
link-aggregation mode dynamic
|
||||||
|
#
|
||||||
|
interface Bridge-Aggregation4
|
||||||
|
description HW_TEST_4
|
||||||
|
port link-type trunk
|
||||||
|
undo port trunk permit vlan 1
|
||||||
|
port trunk permit vlan 221 1112 to 1116 1512
|
||||||
|
link-aggregation mode dynamic
|
||||||
|
#
|
||||||
|
interface NULL0
|
||||||
|
#
|
||||||
|
interface Vlan-interface1
|
||||||
|
dhcp client identifier ascii 0098a92d5735b0-VLAN0001
|
||||||
|
#
|
||||||
|
interface Vlan-interface12
|
||||||
|
description BGP to OSPF1
|
||||||
|
mtu 9008
|
||||||
|
ip address 15.12.16.246 255.255.255.252
|
||||||
|
#
|
||||||
|
interface Vlan-interface3000
|
||||||
|
description L3 to HW_TEST_3
|
||||||
|
ip binding vpn-instance HW_TEST_3
|
||||||
|
ip address 192.168.19.254 255.255.255.128
|
||||||
|
#
|
||||||
|
interface HundredGigE1/0/3
|
||||||
|
port link-mode bridge
|
||||||
|
description HW_TEST_1
|
||||||
|
port link-type trunk
|
||||||
|
undo port trunk permit vlan 1
|
||||||
|
port trunk permit vlan 221 1112 to 1116 1512
|
||||||
|
storm-constrain broadcast pps 1100 1000
|
||||||
|
storm-constrain multicast pps 1100 1000
|
||||||
|
storm-constrain control shutdown
|
||||||
|
port link-aggregation group 2
|
||||||
|
#
|
||||||
|
interface HundredGigE1/0/63
|
||||||
|
description HW_TEST_2
|
||||||
|
#
|
||||||
|
interface M-GigabitEthernet0/0/0
|
||||||
|
ip address 192.168.10.101 255.255.255.0
|
||||||
|
dhcp client identifier hex 0298a92d5735b0
|
||||||
|
#
|
||||||
|
interface M-GigabitEthernet0/0/1
|
||||||
|
dhcp client identifier hex 0298a92d5735b0
|
||||||
|
#
|
||||||
155
tests/fixtures/h3c/config.expected.json
vendored
Normal file
155
tests/fixtures/h3c/config.expected.json
vendored
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
{
|
||||||
|
"system": {
|
||||||
|
"model": "S9820-64H",
|
||||||
|
"serial_number": "",
|
||||||
|
"version": "7.1.070"
|
||||||
|
},
|
||||||
|
"interfaces": [
|
||||||
|
{
|
||||||
|
"interface": "Bridge-Aggregation1",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": "HW_TEST_1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "Bridge-Aggregation2",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": "HW_TEST_2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "Bridge-Aggregation3",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": "HW_TEST_3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "Bridge-Aggregation4",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": "HW_TEST_4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "NULL0",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "Vlan-interface1",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "Vlan-interface12",
|
||||||
|
"ip_address": "15.12.16.246",
|
||||||
|
"mask": 30,
|
||||||
|
"description": "BGP to OSPF1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "Vlan-interface3000",
|
||||||
|
"ip_address": "192.168.19.254",
|
||||||
|
"mask": 25,
|
||||||
|
"description": "L3 to HW_TEST_3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "HundredGigE1/0/3",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": "HW_TEST_1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "HundredGigE1/0/63",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": "HW_TEST_2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "M-GigabitEthernet0/0/0",
|
||||||
|
"ip_address": "192.168.10.101",
|
||||||
|
"mask": 24,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "M-GigabitEthernet0/0/1",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"vlans": [
|
||||||
|
{
|
||||||
|
"vlan_id": 1,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 12,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 13,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 15,
|
||||||
|
"description": "HW_TEST_1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 222,
|
||||||
|
"description": "MGMT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 222,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 1122,
|
||||||
|
"description": "DATA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 1123,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 1200,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 1512,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 1513,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 2000,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 3377,
|
||||||
|
"description": "HW_TEST_1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 1112,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 1113,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 1114,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 1115,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 1116,
|
||||||
|
"description": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
38
tests/fixtures/huawei/config.conf
vendored
Normal file
38
tests/fixtures/huawei/config.conf
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Huawei Versatile Routing Platform Software
|
||||||
|
# VRP (R) software, Version 5.170 (S5731 V200R019C00SPC500)
|
||||||
|
# Copyright (C) 2000-2019 HUAWEI TECH Co., Ltd.
|
||||||
|
#
|
||||||
|
# DDR Memory Size : 4096 M bytes
|
||||||
|
# FLASH Total Memory Size : 1024 M bytes
|
||||||
|
# FLASH Available Memory Size : 739 M bytes
|
||||||
|
# Pcb Version : VER.B
|
||||||
|
# BootROM Version : 0000.04e4
|
||||||
|
# BootLoad Version : 0213.0000
|
||||||
|
# CPLD Version : 0104
|
||||||
|
# Software Version : VRP (R) Software, Version 5.170 (V200R019C00SPC500)
|
||||||
|
# FLASH Version : 0000.0000
|
||||||
|
# PWR1 information
|
||||||
|
# Pcb Version : PWR VER.D
|
||||||
|
# PWR2 information
|
||||||
|
# Pcb Version : PWR VER.D
|
||||||
|
# FAN1 information
|
||||||
|
# Pcb Version : NA
|
||||||
|
# FAN2 information
|
||||||
|
# Pcb Version : NA
|
||||||
|
# ESN of slot 1: 102266666666
|
||||||
|
# ESN of slot 2: 102288888888
|
||||||
|
interface GigabitEthernet0/0/33
|
||||||
|
port link-type access
|
||||||
|
port default vlan 101
|
||||||
|
loopback-detect enable
|
||||||
|
stp disable
|
||||||
|
storm-control broadcast min-rate 1500 max-rate 2500
|
||||||
|
storm-control multicast min-rate 1000 max-rate 2000
|
||||||
|
storm-control action error-down
|
||||||
|
#
|
||||||
|
interface Vlanif120
|
||||||
|
description SSH
|
||||||
|
ip address 10.26.196.254 255.255.255.0
|
||||||
|
#
|
||||||
|
vlan batch 13 26 101 to 103 110 120 130 201 to 204 209 to 212 350 360
|
||||||
|
#
|
||||||
95
tests/fixtures/huawei/config.expected.json
vendored
Normal file
95
tests/fixtures/huawei/config.expected.json
vendored
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
{
|
||||||
|
"system": {
|
||||||
|
"model": "S5731",
|
||||||
|
"serial_number": "102266666666",
|
||||||
|
"version": "5.170"
|
||||||
|
},
|
||||||
|
"interfaces": [
|
||||||
|
{
|
||||||
|
"interface": "GigabitEthernet0/0/33",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "Vlanif120",
|
||||||
|
"ip_address": "10.26.196.254",
|
||||||
|
"mask": 24,
|
||||||
|
"description": "SSH"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"vlans": [
|
||||||
|
{
|
||||||
|
"vlan_id": 13,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 26,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 101,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 102,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 103,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 110,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 120,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 130,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 201,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 202,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 203,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 204,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 209,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 210,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 211,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 212,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 350,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 360,
|
||||||
|
"description": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
341
tests/fixtures/keenetic/config.conf
vendored
Normal file
341
tests/fixtures/keenetic/config.conf
vendored
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
!
|
||||||
|
! release: 4.03.C.6.2-7
|
||||||
|
! sandbox: stable
|
||||||
|
! title: 4.3.6.2
|
||||||
|
! arch: mips
|
||||||
|
!
|
||||||
|
! ndm:
|
||||||
|
! exact: 0-a3057529fd
|
||||||
|
! cdate: 29 Sep 2025
|
||||||
|
!
|
||||||
|
! bsp:
|
||||||
|
! exact: 0-03b50470c4
|
||||||
|
! cdate: 30 Sep 2025
|
||||||
|
!
|
||||||
|
! ndw:
|
||||||
|
! features: dual_image,led_control,wifi_button,wifi5ghz,
|
||||||
|
! vht2ghz,mimo2ghz,mimo5ghz,atf2ghz,atf5ghz,wifi6,wifi_ft,
|
||||||
|
! wpa3,hwnat
|
||||||
|
! components: base,cloudcontrol,corewireless,ddns,dhcpd,
|
||||||
|
! dns-filter,dns-https,dns-tls,dot1x,easyconfig,igmp,ip6,
|
||||||
|
! lang-en,lang-ru,miniupnpd,mws,nathelper-ftp,nathelper-
|
||||||
|
! h323,nathelper-pptp,nathelper-rtsp,nathelper-sip,ndmp,
|
||||||
|
! ndns,openvpn,pingcheck,ppe,pppoe,pptp,ssh,trafficcontrol,
|
||||||
|
! wireguard
|
||||||
|
!
|
||||||
|
! ndw3:
|
||||||
|
! version: 1.101.18.1
|
||||||
|
!
|
||||||
|
! ndw4:
|
||||||
|
! version: 4.3.C.6.2
|
||||||
|
!
|
||||||
|
! manufacturer: Keenetic Ltd.
|
||||||
|
! vendor: Keenetic
|
||||||
|
! series: KN
|
||||||
|
! model: Sprinter (KN-3710)
|
||||||
|
! hw_version: 7777777
|
||||||
|
! hw_type: router
|
||||||
|
! hw_id: KN-3710
|
||||||
|
! device: Sprinter
|
||||||
|
! region: EA
|
||||||
|
! description: Keenetic Sprinter (KN-3710)
|
||||||
|
! $$$ Agent: http/rci
|
||||||
|
! $$$ Last change: Fri, 3 Oct 2025 18:37:40 GMT
|
||||||
|
! $$$ Model: Keenetic Sprinter
|
||||||
|
! $$$ Username: admin
|
||||||
|
! $$$ Version: 2.06.1
|
||||||
|
system
|
||||||
|
set net.ipv4.ip_forward 1
|
||||||
|
set net.ipv4.neigh.default.gc_thresh1 256
|
||||||
|
set net.ipv4.neigh.default.gc_thresh2 1024
|
||||||
|
set net.ipv4.neigh.default.gc_thresh3 2048
|
||||||
|
set net.ipv4.tcp_fin_timeout 30
|
||||||
|
set net.ipv4.tcp_keepalive_time 120
|
||||||
|
set net.ipv6.conf.all.forwarding 1
|
||||||
|
set net.ipv6.neigh.default.gc_thresh1 256
|
||||||
|
set net.ipv6.neigh.default.gc_thresh2 1024
|
||||||
|
set net.ipv6.neigh.default.gc_thresh3 2048
|
||||||
|
set net.netfilter.nf_conntrack_tcp_timeout_established 1200
|
||||||
|
set vm.overcommit_memory 0
|
||||||
|
set vm.vfs_cache_pressure 1000
|
||||||
|
clock timezone Europe/Berlin
|
||||||
|
domainname WORKGROUP
|
||||||
|
hostname test_HW
|
||||||
|
caption default
|
||||||
|
description "Keenetic Sprinter (KN-3710)"
|
||||||
|
ndss dump-report disable
|
||||||
|
!
|
||||||
|
dyndns profile _WEBADMIN
|
||||||
|
!
|
||||||
|
interface GigabitEthernet0
|
||||||
|
up
|
||||||
|
!
|
||||||
|
interface GigabitEthernet0/1
|
||||||
|
rename 1
|
||||||
|
switchport mode access
|
||||||
|
switchport access vlan 1
|
||||||
|
up
|
||||||
|
!
|
||||||
|
interface GigabitEthernet0/2
|
||||||
|
rename 2
|
||||||
|
switchport mode access
|
||||||
|
switchport access vlan 1
|
||||||
|
up
|
||||||
|
!
|
||||||
|
interface GigabitEthernet0/3
|
||||||
|
rename 3
|
||||||
|
switchport mode access
|
||||||
|
switchport access vlan 1
|
||||||
|
up
|
||||||
|
!
|
||||||
|
interface GigabitEthernet0/Vlan1
|
||||||
|
description "Home VLAN"
|
||||||
|
ip dhcp client dns-routes
|
||||||
|
ip name-servers
|
||||||
|
up
|
||||||
|
!
|
||||||
|
interface GigabitEthernet0/Vlan2
|
||||||
|
rename ISP
|
||||||
|
description "\xd0\x9f\xd0\xbe\xd0\xb4\xd0\xba\xd0\xbb\xd1\x8e\xd1\x87\xd0\xb5\xd0\xbd\xd0\xb8\xd0\xb5 Ethernet"
|
||||||
|
dyndns nobind
|
||||||
|
mac address factory wan
|
||||||
|
security-level public
|
||||||
|
ip address dhcp
|
||||||
|
ip dhcp client hostname test_HW
|
||||||
|
ip dhcp client dns-routes
|
||||||
|
ip mtu 1500
|
||||||
|
ip access-group _WEBADMIN_ISP in
|
||||||
|
ip global 57342
|
||||||
|
ip no name-servers
|
||||||
|
igmp upstream
|
||||||
|
ipv6 address auto
|
||||||
|
ipv6 prefix auto
|
||||||
|
ipv6 no name-servers auto
|
||||||
|
up
|
||||||
|
!
|
||||||
|
interface GigabitEthernet0/0
|
||||||
|
rename 0
|
||||||
|
role inet for ISP
|
||||||
|
switchport mode access
|
||||||
|
switchport access vlan 2
|
||||||
|
up
|
||||||
|
!
|
||||||
|
interface GigabitEthernet0/Vlan3
|
||||||
|
dyndns nobind
|
||||||
|
ip dhcp client dns-routes
|
||||||
|
ip name-servers
|
||||||
|
up
|
||||||
|
!
|
||||||
|
interface WifiMaster0
|
||||||
|
country-code RU
|
||||||
|
compatibility BGN+AX
|
||||||
|
rekey-interval 86400
|
||||||
|
up
|
||||||
|
!
|
||||||
|
interface WifiMaster0/AccessPoint0
|
||||||
|
mac access-list type none
|
||||||
|
authentication wpa-psk ns3 7777ggggddddsss
|
||||||
|
encryption enable
|
||||||
|
encryption wpa2
|
||||||
|
ip dhcp client dns-routes
|
||||||
|
ssid test_HW_2.4G
|
||||||
|
up
|
||||||
|
!
|
||||||
|
interface WifiMaster0/AccessPoint1
|
||||||
|
mac access-list type none
|
||||||
|
security-level private
|
||||||
|
encryption no enable
|
||||||
|
ip dhcp client dns-routes
|
||||||
|
down
|
||||||
|
!
|
||||||
|
interface WifiMaster0/AccessPoint2
|
||||||
|
mac access-list type none
|
||||||
|
security-level private
|
||||||
|
encryption no enable
|
||||||
|
ip dhcp client dns-routes
|
||||||
|
down
|
||||||
|
!
|
||||||
|
interface WifiMaster0/WifiStation0
|
||||||
|
security-level public
|
||||||
|
encryption no enable
|
||||||
|
ip dhcp client dns-routes
|
||||||
|
standby enable
|
||||||
|
standby timeout 600
|
||||||
|
down
|
||||||
|
!
|
||||||
|
interface WifiMaster1
|
||||||
|
country-code RU
|
||||||
|
compatibility AN+AC+AX
|
||||||
|
channel width 40-above/80
|
||||||
|
rekey-interval 86400
|
||||||
|
up
|
||||||
|
!
|
||||||
|
interface WifiMaster1/AccessPoint0
|
||||||
|
mac access-list type none
|
||||||
|
authentication wpa-psk ns3 7777ggggddddsss
|
||||||
|
encryption enable
|
||||||
|
encryption wpa2
|
||||||
|
ip dhcp client dns-routes
|
||||||
|
ssid test_HW_5G
|
||||||
|
up
|
||||||
|
!
|
||||||
|
interface WifiMaster1/AccessPoint1
|
||||||
|
mac access-list type none
|
||||||
|
security-level private
|
||||||
|
encryption no enable
|
||||||
|
ip dhcp client dns-routes
|
||||||
|
down
|
||||||
|
!
|
||||||
|
interface WifiMaster1/AccessPoint2
|
||||||
|
mac access-list type none
|
||||||
|
security-level private
|
||||||
|
encryption no enable
|
||||||
|
ip dhcp client dns-routes
|
||||||
|
down
|
||||||
|
!
|
||||||
|
interface WifiMaster1/WifiStation0
|
||||||
|
security-level public
|
||||||
|
encryption no enable
|
||||||
|
ip dhcp client dns-routes
|
||||||
|
standby enable
|
||||||
|
standby timeout 600
|
||||||
|
down
|
||||||
|
!
|
||||||
|
interface Bridge0
|
||||||
|
rename Home
|
||||||
|
description "Home network"
|
||||||
|
dyndns nobind
|
||||||
|
include GigabitEthernet0/Vlan1
|
||||||
|
include WifiMaster0/AccessPoint0
|
||||||
|
include WifiMaster1/AccessPoint0
|
||||||
|
mac access-list type none
|
||||||
|
security-level private
|
||||||
|
ip address 17.36.1.1 255.255.255.0
|
||||||
|
ip dhcp client dns-routes
|
||||||
|
ip access-group _WEBADMIN_Home in
|
||||||
|
ip name-servers
|
||||||
|
band-steering
|
||||||
|
up
|
||||||
|
!
|
||||||
|
interface Bridge1
|
||||||
|
rename Guest
|
||||||
|
description "Guest network"
|
||||||
|
traffic-shape rate 5120
|
||||||
|
dyndns nobind
|
||||||
|
include GigabitEthernet0/Vlan3
|
||||||
|
mac access-list type none
|
||||||
|
peer-isolation
|
||||||
|
security-level protected
|
||||||
|
ip address 10.1.30.1 255.255.255.0
|
||||||
|
ip dhcp client dns-routes
|
||||||
|
ip name-servers
|
||||||
|
down
|
||||||
|
!
|
||||||
|
interface Bridge2
|
||||||
|
rename Test
|
||||||
|
mac access-list type none
|
||||||
|
security-level public
|
||||||
|
ip dhcp client dns-routes
|
||||||
|
up
|
||||||
|
!
|
||||||
|
interface OpenVPN0
|
||||||
|
description test_HW-udp
|
||||||
|
role misc
|
||||||
|
security-level public
|
||||||
|
ip dhcp client dns-routes
|
||||||
|
ip tcp adjust-mss pmtu
|
||||||
|
ip name-servers
|
||||||
|
ipv6 name-servers auto
|
||||||
|
openvpn accept-routes
|
||||||
|
openvpn connect
|
||||||
|
up
|
||||||
|
!
|
||||||
|
interface OpenVPN2
|
||||||
|
description test_HW-tcp
|
||||||
|
role misc
|
||||||
|
dyndns nobind
|
||||||
|
security-level public
|
||||||
|
ip dhcp client dns-routes
|
||||||
|
ip tcp adjust-mss pmtu
|
||||||
|
openvpn accept-routes
|
||||||
|
openvpn connect
|
||||||
|
down
|
||||||
|
!
|
||||||
|
interface Wireguard0
|
||||||
|
description test_HW
|
||||||
|
dyndns nobind
|
||||||
|
security-level public
|
||||||
|
ip address 10.3.100.1 255.255.255.0
|
||||||
|
ip mtu 1324
|
||||||
|
ip tcp adjust-mss pmtu
|
||||||
|
wireguard listen-port 65513
|
||||||
|
wireguard peer 7777ggggddddsss= !test_HW
|
||||||
|
allow-ips 0.0.0.0 0.0.0.0
|
||||||
|
connect
|
||||||
|
!
|
||||||
|
up
|
||||||
|
!
|
||||||
|
interface Wireguard1
|
||||||
|
description test_HW
|
||||||
|
dyndns nobind
|
||||||
|
security-level private
|
||||||
|
ip address 10.1.100.1 255.255.255.0
|
||||||
|
ip mtu 1324
|
||||||
|
ip access-group _WEBADMIN_Wireguard1 in
|
||||||
|
ip tcp adjust-mss pmtu
|
||||||
|
wireguard listen-port 65511
|
||||||
|
wireguard peer 7777ggggddddsss= !test_HW
|
||||||
|
allow-ips 10.1.100.0 255.255.255.0
|
||||||
|
allow-ips 17.36.3.0 255.255.255.0
|
||||||
|
allow-ips 17.36.1.0 255.255.255.0
|
||||||
|
allow-ips 0.0.0.0 0.0.0.0
|
||||||
|
connect
|
||||||
|
!
|
||||||
|
up
|
||||||
|
!
|
||||||
|
interface Wireguard2
|
||||||
|
description test_HW
|
||||||
|
dyndns nobind
|
||||||
|
security-level private
|
||||||
|
ip address 10.2.100.1 255.255.255.0
|
||||||
|
ip access-group _WEBADMIN_Wireguard2 in
|
||||||
|
ip tcp adjust-mss pmtu
|
||||||
|
wireguard listen-port 65512
|
||||||
|
wireguard peer 7777ggggddddsss= !test_HW
|
||||||
|
allow-ips 0.0.0.0 0.0.0.0
|
||||||
|
connect
|
||||||
|
!
|
||||||
|
up
|
||||||
|
!
|
||||||
|
ip ssh
|
||||||
|
port 22
|
||||||
|
security-level public
|
||||||
|
lockout-policy 5 15 3
|
||||||
|
!
|
||||||
|
ip hotspot
|
||||||
|
policy Home permit
|
||||||
|
host 7777ggggddddsss permit
|
||||||
|
host 7777ggggddddsss priority 4
|
||||||
|
!
|
||||||
|
ipv6 subnet Default
|
||||||
|
bind Home
|
||||||
|
mode slaac
|
||||||
|
prefix length 64
|
||||||
|
number 0
|
||||||
|
!
|
||||||
|
ppe software
|
||||||
|
ppe hardware
|
||||||
|
upnp lan Home
|
||||||
|
service dhcp
|
||||||
|
service dns-proxy
|
||||||
|
service http
|
||||||
|
service telnet
|
||||||
|
service ssh
|
||||||
|
service ntp
|
||||||
|
service upnp
|
||||||
|
!
|
||||||
|
easyconfig disable
|
||||||
|
components
|
||||||
|
auto-update disable
|
||||||
|
auto-update channel stable
|
||||||
|
!
|
||||||
161
tests/fixtures/keenetic/config.expected.json
vendored
Normal file
161
tests/fixtures/keenetic/config.expected.json
vendored
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
{
|
||||||
|
"system": {
|
||||||
|
"model": "Sprinter (KN-3710)",
|
||||||
|
"serial_number": "7777777",
|
||||||
|
"version": "4.03.C.6.2-7"
|
||||||
|
},
|
||||||
|
"interfaces": [
|
||||||
|
{
|
||||||
|
"interface": "GigabitEthernet0",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "GigabitEthernet0/1",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "GigabitEthernet0/2",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "GigabitEthernet0/3",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "GigabitEthernet0/0",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "WifiMaster0",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "WifiMaster0/AccessPoint0",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "WifiMaster0/AccessPoint1",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "WifiMaster0/AccessPoint2",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "WifiMaster0/WifiStation0",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "WifiMaster1",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "WifiMaster1/AccessPoint0",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "WifiMaster1/AccessPoint1",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "WifiMaster1/AccessPoint2",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "WifiMaster1/WifiStation0",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "Bridge0",
|
||||||
|
"ip_address": "17.36.1.1",
|
||||||
|
"mask": 24,
|
||||||
|
"description": "Home network"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "Bridge1",
|
||||||
|
"ip_address": "10.1.30.1",
|
||||||
|
"mask": 24,
|
||||||
|
"description": "Guest network"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "Bridge2",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "OpenVPN0",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": "test_HW-udp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "OpenVPN2",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": "test_HW-tcp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "Wireguard0",
|
||||||
|
"ip_address": "10.3.100.1",
|
||||||
|
"mask": 24,
|
||||||
|
"description": "test_HW"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "Wireguard1",
|
||||||
|
"ip_address": "10.1.100.1",
|
||||||
|
"mask": 24,
|
||||||
|
"description": "test_HW"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "Wireguard2",
|
||||||
|
"ip_address": "10.2.100.1",
|
||||||
|
"mask": 24,
|
||||||
|
"description": "test_HW"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"vlans": [
|
||||||
|
{
|
||||||
|
"vlan_id": 1,
|
||||||
|
"description": "Home VLAN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 2,
|
||||||
|
"description": "Подключение Ethernet"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 3,
|
||||||
|
"description": "Home network"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
123
tests/fixtures/mikrotik/config.conf
vendored
Normal file
123
tests/fixtures/mikrotik/config.conf
vendored
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
# 2026-02-18 22:32:59 by RouterOS 7.19.3
|
||||||
|
# software id = 0V5S-56MC
|
||||||
|
# version: 7.12
|
||||||
|
# model = C52iG-5HaxD2HaxD
|
||||||
|
# serial number = HE108BBGW0B
|
||||||
|
/interface bridge
|
||||||
|
add name=bridge.LAN
|
||||||
|
/interface ethernet
|
||||||
|
set [ find default-name=ether1 ] mac-address=C4:AD:32:B2:A1:9A poe-out=off
|
||||||
|
set [ find default-name=ether4 ] comment=test
|
||||||
|
/interface vlan
|
||||||
|
add comment="super test vlan" interface=ether4 name="test vlan" vlan-id=255
|
||||||
|
add disabled=yes interface=ether5 name="test test vlan" vlan-id=254
|
||||||
|
/interface list
|
||||||
|
add name=LAN
|
||||||
|
add name=WAN
|
||||||
|
/interface wifi channel
|
||||||
|
add band=2ghz-ax disabled=no name=ch-2ghz width=20/40mhz
|
||||||
|
add band=5ghz-ax disabled=no name=ch-5ghz width=20/40mhz
|
||||||
|
/interface wifi security
|
||||||
|
add authentication-types=wpa2-psk name=common-auth wps=disable
|
||||||
|
/interface wifi configuration
|
||||||
|
add name=common-auth security=common-auth ssid=test_HW
|
||||||
|
/interface wifi
|
||||||
|
set [ find default-name=wifi1 ] channel=ch-5ghz configuration=common-auth configuration.mode=ap disabled=no
|
||||||
|
set [ find default-name=wifi2 ] channel=ch-2ghz configuration=common-auth configuration.mode=ap disabled=no
|
||||||
|
/ip pool
|
||||||
|
add name=dhcp_pool0 ranges=172.16.3.2-172.16.3.254
|
||||||
|
add name=dhcp_pool1 ranges=192.168.3.2-192.168.3.254
|
||||||
|
/ip dhcp-server
|
||||||
|
add address-pool=dhcp_pool0 interface=bridge.LAN lease-time=10h name=dhcp1
|
||||||
|
/ppp profile
|
||||||
|
add name=new_antizapret on-down="/ip dns cache flush\r\
|
||||||
|
\n" on-up="/ip dns cache flush\r\
|
||||||
|
\n"
|
||||||
|
add name=robovps on-down="/ip dns cache flush" on-up="/ip dns cache flush" use-ipv6=no
|
||||||
|
/routing table
|
||||||
|
add disabled=no fib name=test_HW_table
|
||||||
|
/interface bridge port
|
||||||
|
add bridge=bridge.LAN interface=ether2 learn=yes
|
||||||
|
add bridge=bridge.LAN interface=ether3
|
||||||
|
add bridge=bridge.LAN interface=ether4
|
||||||
|
add bridge=bridge.LAN interface=ether5
|
||||||
|
add bridge=bridge.LAN interface=wifi1
|
||||||
|
add bridge=bridge.LAN interface=wifi2
|
||||||
|
/ipv6 settings
|
||||||
|
set disable-ipv6=yes forward=no
|
||||||
|
/interface detect-internet
|
||||||
|
set detect-interface-list=all
|
||||||
|
/interface list member
|
||||||
|
add interface=bridge.LAN list=LAN
|
||||||
|
add interface=ether1 list=WAN
|
||||||
|
/interface ovpn-server server
|
||||||
|
add mac-address=FE:25:E0:B8:66:01 name=ovpn-server1
|
||||||
|
/ip address
|
||||||
|
add address=172.16.3.1/24 interface=bridge.LAN network=172.16.3.0
|
||||||
|
add address=10.38.3.245/24 interface=ether1 network=10.38.3.0
|
||||||
|
add address=10.1.100.2/24 interface=wireguard2 network=10.1.100.0
|
||||||
|
add address=100.10.10.1/24 disabled=yes interface=ether4 network=100.10.10.0
|
||||||
|
/ip dhcp-server lease
|
||||||
|
add address=172.16.3.20 client-id=1:d8:3a:dd:22:28:1d mac-address=D8:3A:DD:21:28:1D server=dhcp1
|
||||||
|
add address=172.16.3.4 client-id=1:2c:cd:29:1a:ea:6d comment=test_HW mac-address=1E:CD:29:8A:EA:6D server=dhcp1
|
||||||
|
/ip dhcp-server network
|
||||||
|
add address=172.16.3.0/24 dns-server=172.16.3.1 gateway=172.16.3.1
|
||||||
|
/ip dns
|
||||||
|
set allow-remote-requests=yes servers=217.10.44.35
|
||||||
|
/ip dns static
|
||||||
|
add address=172.16.3.20 regexp=".*\\.home\$" type=A
|
||||||
|
add address=172.16.3.20 regexp=".*\\.home.uk\$" type=A
|
||||||
|
add address=172.16.3.20 disabled=yes regexp=".*\\.home.uk\$" type=A
|
||||||
|
/ip firewall address-list
|
||||||
|
add address=172.16.3.0/24 list=test_HW
|
||||||
|
add address=172.16.2.0/24 list=test_HW
|
||||||
|
add address=172.16.1.0/24 list=test_HW
|
||||||
|
add address=255.255.255.255 list=test_HW
|
||||||
|
/ip firewall filter
|
||||||
|
add action=drop chain=forward out-interface=ether1 src-address=172.16.3.11
|
||||||
|
add action=accept chain=input src-address=255.255.255.255
|
||||||
|
add action=accept chain=forward in-interface=wireguard2
|
||||||
|
add action=accept chain=forward connection-state=established,related in-interface=ether1
|
||||||
|
add action=accept chain=input connection-state=established,related in-interface=ether1
|
||||||
|
add action=accept chain=input in-interface=bridge.LAN
|
||||||
|
add action=drop chain=input in-interface=ether1
|
||||||
|
add action=drop chain=forward in-interface=ether1
|
||||||
|
/ip firewall mangle
|
||||||
|
add action=passthrough chain=prerouting connection-mark=test_HW src-address=172.16.3.20
|
||||||
|
/ip firewall nat
|
||||||
|
add action=masquerade chain=srcnat out-interface=ether1
|
||||||
|
add action=masquerade chain=srcnat comment=test_HW out-interface=test_HW
|
||||||
|
/ip firewall service-port
|
||||||
|
set ftp disabled=yes
|
||||||
|
set tftp disabled=yes
|
||||||
|
set h323 disabled=yes
|
||||||
|
/ip route
|
||||||
|
add disabled=no distance=1 dst-address=172.16.1.0/24 gateway=10.1.100.1
|
||||||
|
add disabled=no distance=1 dst-address=172.16.2.0/24 gateway=10.1.100.1
|
||||||
|
add disabled=no distance=1 dst-address=255.255.255.255/32 gateway=10.38.3.1
|
||||||
|
add disabled=no distance=3 dst-address=0.0.0.0/0 gateway=10.38.3.1 routing-table=main scope=30 suppress-hw-offload=no target-scope=10
|
||||||
|
add disabled=yes distance=2 dst-address=0.0.0.0/0 gateway=10.38.3.1 routing-table=main scope=30 suppress-hw-offload=no target-scope=10
|
||||||
|
add comment=test_HW disabled=no distance=1 dst-address=185.255.255.255/32 gateway=10.38.3.1 routing-table=main scope=30 suppress-hw-offload=no target-scope=10
|
||||||
|
add comment=test_HW disabled=no distance=1 dst-address=192.168.255.255/32 gateway=10.38.3.1 routing-table=main scope=30 suppress-hw-offload=no target-scope=10
|
||||||
|
/ip service
|
||||||
|
set telnet disabled=yes
|
||||||
|
/system clock
|
||||||
|
set time-zone-name=Europe/Berlin
|
||||||
|
/system identity
|
||||||
|
set name=test_HW
|
||||||
|
/system logging
|
||||||
|
set 0 topics=info,!wireless
|
||||||
|
/system note
|
||||||
|
set show-at-login=no
|
||||||
|
/system ntp client
|
||||||
|
set enabled=yes
|
||||||
|
/system ntp client servers
|
||||||
|
add address=pool.ntp.org
|
||||||
|
/tool mac-server
|
||||||
|
set allowed-interface-list=LAN
|
||||||
|
/tool mac-server mac-winbox
|
||||||
|
set allowed-interface-list=LAN
|
||||||
|
/tool mac-server ping
|
||||||
|
set enabled=no
|
||||||
|
/user aaa
|
||||||
|
set default-group=full use-radius=yes
|
||||||
43
tests/fixtures/mikrotik/config.expected.json
vendored
Normal file
43
tests/fixtures/mikrotik/config.expected.json
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"system": {
|
||||||
|
"model": "C52iG-5HaxD2HaxD",
|
||||||
|
"serial_number": "HE108BBGW0B",
|
||||||
|
"version": "7.12"
|
||||||
|
},
|
||||||
|
"interfaces": [
|
||||||
|
{
|
||||||
|
"interface": "bridge.LAN",
|
||||||
|
"ip_address": "172.16.3.1",
|
||||||
|
"mask": 24,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "ether1",
|
||||||
|
"ip_address": "10.38.3.245",
|
||||||
|
"mask": 24,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "wireguard2",
|
||||||
|
"ip_address": "10.1.100.2",
|
||||||
|
"mask": 24,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "ether4",
|
||||||
|
"ip_address": "100.10.10.1",
|
||||||
|
"mask": 24,
|
||||||
|
"description": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"vlans": [
|
||||||
|
{
|
||||||
|
"vlan_id": 255,
|
||||||
|
"description": "test vlan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 254,
|
||||||
|
"description": "test test vlan"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
42
tests/fixtures/qtech/config_1.conf
vendored
Normal file
42
tests/fixtures/qtech/config_1.conf
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
! QTECH LLC Internetwork Operating System Software
|
||||||
|
! QSW-8330-40T-DC Series Software, Version 2.2.0C Build 96279, RELEASE SOFTWARE
|
||||||
|
! ROM: System Bootstrap, Version 0.4.7,hardware version:A
|
||||||
|
! Serial num:6060606060606060, ID num:555555555555
|
||||||
|
! System image file is "Switch.bin"
|
||||||
|
! QTECH LLC QSW-8330-40T-DC RISC
|
||||||
|
! 524288K bytes of memory,16384K bytes of flash
|
||||||
|
! Base ethernet MAC Address: 08:c6:b3:08:cf:ff
|
||||||
|
! snmp info:
|
||||||
|
! vend_ID:27514 product_ID:404 system_ID:1.3.6.1.4.1.27514
|
||||||
|
interface GigaEthernet1/0/9
|
||||||
|
shutdown
|
||||||
|
description FREE
|
||||||
|
switchport pvid 102
|
||||||
|
storm-control broadcast threshold 15
|
||||||
|
storm-control broadcast action shutdown
|
||||||
|
storm-control broadcast auto_resume 60s
|
||||||
|
storm-control multicast threshold 10
|
||||||
|
storm-control multicast action shutdown
|
||||||
|
storm-control multicast auto_resume 60s
|
||||||
|
qos policy IPP3 ingress
|
||||||
|
!
|
||||||
|
interface VLAN1
|
||||||
|
ip address 192.168.0.1 255.255.0.0
|
||||||
|
ip mtu 1500
|
||||||
|
no ip directed-broadcast
|
||||||
|
!
|
||||||
|
interface VLAN1002
|
||||||
|
description test-1002
|
||||||
|
ip address 13.36.8.1 255.255.255.0
|
||||||
|
ip mtu 1500
|
||||||
|
no ip directed-broadcast
|
||||||
|
!
|
||||||
|
vlan 772
|
||||||
|
name test
|
||||||
|
!
|
||||||
|
vlan 888
|
||||||
|
name test_super
|
||||||
|
!
|
||||||
|
vlan 1,7,14-15,44,101-102,115,117-124,130-136,139,167,200-205,772
|
||||||
|
,1607
|
||||||
|
vlan 888,2016,2085-2088
|
||||||
185
tests/fixtures/qtech/config_1.expected.json
vendored
Normal file
185
tests/fixtures/qtech/config_1.expected.json
vendored
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
{
|
||||||
|
"system": {
|
||||||
|
"model": "QSW-8330-40T-DC",
|
||||||
|
"serial_number": "6060606060606060",
|
||||||
|
"version": "96279"
|
||||||
|
},
|
||||||
|
"interfaces": [
|
||||||
|
{
|
||||||
|
"interface": "GigaEthernet1/0/9",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": "FREE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "VLAN1",
|
||||||
|
"ip_address": "192.168.0.1",
|
||||||
|
"mask": 16,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "VLAN1002",
|
||||||
|
"ip_address": "13.36.8.1",
|
||||||
|
"mask": 24,
|
||||||
|
"description": "test-1002"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"vlans": [
|
||||||
|
{
|
||||||
|
"vlan_id": 772,
|
||||||
|
"description": "test"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 888,
|
||||||
|
"description": "test_super"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 1,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 7,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 14,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 15,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 44,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 101,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 102,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 115,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 117,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 118,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 119,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 120,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 121,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 122,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 123,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 124,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 130,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 131,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 132,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 133,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 134,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 135,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 136,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 139,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 167,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 200,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 201,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 202,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 203,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 204,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 205,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 1607,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 2016,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 2085,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 2086,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 2087,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 2088,
|
||||||
|
"description": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
36
tests/fixtures/qtech/config_2.conf
vendored
Normal file
36
tests/fixtures/qtech/config_2.conf
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
! QTECH LLC Internetwork Operating System Software
|
||||||
|
! QSW-8330-40T-DC Series Software, Version 2.2.0C Build 96279, RELEASE SOFTWARE
|
||||||
|
! ROM: System Bootstrap, Version 0.4.7,hardware version:A
|
||||||
|
! Serial num:6060606060606060, ID num:555555555555
|
||||||
|
! System image file is "Switch.bin"
|
||||||
|
! QTECH LLC QSW-8330-40T-DC RISC
|
||||||
|
! 524288K bytes of memory,16384K bytes of flash
|
||||||
|
! Base ethernet MAC Address: 08:c6:b3:08:cf:f7
|
||||||
|
! snmp info:
|
||||||
|
! vend_ID:27514 product_ID:404 system_ID:1.3.6.1.4.1.27514
|
||||||
|
interface GigaEthernet1/0/9
|
||||||
|
shutdown
|
||||||
|
description FREE
|
||||||
|
switchport pvid 102
|
||||||
|
storm-control broadcast threshold 15
|
||||||
|
storm-control broadcast action shutdown
|
||||||
|
storm-control broadcast auto_resume 60s
|
||||||
|
storm-control multicast threshold 10
|
||||||
|
storm-control multicast action shutdown
|
||||||
|
storm-control multicast auto_resume 60s
|
||||||
|
qos policy IPP3 ingress
|
||||||
|
!
|
||||||
|
interface VLAN1
|
||||||
|
ip address 192.168.0.1 255.255.0.0
|
||||||
|
ip mtu 1500
|
||||||
|
no ip directed-broadcast
|
||||||
|
!
|
||||||
|
interface VLAN1002
|
||||||
|
description test-1002
|
||||||
|
ip address 13.36.8.1 255.255.255.0
|
||||||
|
ip mtu 1500
|
||||||
|
no ip directed-broadcast
|
||||||
|
!
|
||||||
|
vlan 1,7,14-15,44,101-102,115,117-124,130-136,139,167,200-205,772
|
||||||
|
,1607
|
||||||
|
vlan 888,2016,2085-2088
|
||||||
185
tests/fixtures/qtech/config_2.expected.json
vendored
Normal file
185
tests/fixtures/qtech/config_2.expected.json
vendored
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
{
|
||||||
|
"system": {
|
||||||
|
"model": "QSW-8330-40T-DC",
|
||||||
|
"serial_number": "6060606060606060",
|
||||||
|
"version": "96279"
|
||||||
|
},
|
||||||
|
"interfaces": [
|
||||||
|
{
|
||||||
|
"interface": "GigaEthernet1/0/9",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": "FREE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "VLAN1",
|
||||||
|
"ip_address": "192.168.0.1",
|
||||||
|
"mask": 16,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "VLAN1002",
|
||||||
|
"ip_address": "13.36.8.1",
|
||||||
|
"mask": 24,
|
||||||
|
"description": "test-1002"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"vlans": [
|
||||||
|
{
|
||||||
|
"vlan_id": 1,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 7,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 14,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 15,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 44,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 101,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 102,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 115,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 117,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 118,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 119,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 120,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 121,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 122,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 123,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 124,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 130,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 131,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 132,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 133,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 134,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 135,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 136,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 139,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 167,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 200,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 201,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 202,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 203,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 204,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 205,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 772,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 1607,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 888,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 2016,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 2085,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 2086,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 2087,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vlan_id": 2088,
|
||||||
|
"description": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
41
tests/fixtures/quasar/config_1.conf
vendored
Normal file
41
tests/fixtures/quasar/config_1.conf
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Copyright © 2021-2022, TechArgos LLC
|
||||||
|
# ----------- -----------------
|
||||||
|
# Subsystem Version
|
||||||
|
# ----------- -----------------
|
||||||
|
# Engine 0.2.17.2022-10-21
|
||||||
|
# DPlane 0.2.18.2022-05-16
|
||||||
|
# BfMonitor 1.1.2.2022-08-25
|
||||||
|
# CLI.core 1.0.3.2022-09-30
|
||||||
|
# CLI.engine 1.2.2.2022-10-12
|
||||||
|
# RConsole 0.3.4.2022-01-12
|
||||||
|
# RcAppParams 0.3.1.2022-01-12
|
||||||
|
# SNMP 0.0.11.2022-04-26
|
||||||
|
# Zabbix 0.2.13.2022-01-26
|
||||||
|
# WebUI 1.1.3.2022-10-19
|
||||||
|
# BF.core 9.3.1.2021-01-30
|
||||||
|
#
|
||||||
|
# ------------------------- ---------------
|
||||||
|
# Platform EEPROM field Value
|
||||||
|
# ------------------------- ---------------
|
||||||
|
# Product Name Quasar-T-Q-0002
|
||||||
|
# Product Number HB4NC011234M
|
||||||
|
# Local MAC N/A
|
||||||
|
# Product Serial Number WEE1C1CC0004A
|
||||||
|
# Product Version 0.0
|
||||||
|
# System Manufacturing Date 2026-06-07
|
||||||
|
config ethernet ipv4 address 25.25.1.221/24 gateway 25.25.1.254 enable
|
||||||
|
|
||||||
|
config interface 7/4 fec none mode force-up enable
|
||||||
|
config interface 8/1 fec none mode force-up enable
|
||||||
|
config interface 8/2 fec none mode force-up enable
|
||||||
|
config interface 8/3 fec none mode force-up enable
|
||||||
|
config interface 8/4 fec none mode force-up enable
|
||||||
|
config interface 9/1 fec none mode force-up enable
|
||||||
|
config interface 9/2 fec none mode force-up enable
|
||||||
|
config interface 7/4 description "IN DWDM / OUT TEST_HW_08_N0_p1"
|
||||||
|
config interface 8/1 description "IN DWDM / OUT TEST_HW_09_N1_p0"
|
||||||
|
config interface 8/2 description "IN DWDM / OUT TEST_HW_09_N0_p0"
|
||||||
|
config interface 8/3 description "IN DWDM / OUT TEST_HW_10_N1_p0"
|
||||||
|
config interface 8/4 description "IN DWDM / OUT TEST_HW_10_N0_p0"
|
||||||
|
config interface 9/1 description "IN DWDM / OUT TEST_HW_11_N0_p0"
|
||||||
|
config interface 9/2 description "IN DWDM / OUT TEST_HW_11_N0_p1"
|
||||||
58
tests/fixtures/quasar/config_1.expected.json
vendored
Normal file
58
tests/fixtures/quasar/config_1.expected.json
vendored
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"system": {
|
||||||
|
"model": "Quasar-T-Q-0002",
|
||||||
|
"serial_number": "WEE1C1CC0004A",
|
||||||
|
"version": "0.2.17.2022-10-21"
|
||||||
|
},
|
||||||
|
"interfaces": [
|
||||||
|
{
|
||||||
|
"interface": "7/4",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": "IN DWDM / OUT TEST_HW_08_N0_p1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "8/1",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": "IN DWDM / OUT TEST_HW_09_N1_p0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "8/2",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": "IN DWDM / OUT TEST_HW_09_N0_p0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "8/3",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": "IN DWDM / OUT TEST_HW_10_N1_p0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "8/4",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": "IN DWDM / OUT TEST_HW_10_N0_p0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "9/1",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": "IN DWDM / OUT TEST_HW_11_N0_p0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "9/2",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": "IN DWDM / OUT TEST_HW_11_N0_p1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "ethernet",
|
||||||
|
"ip_address": "25.25.1.221",
|
||||||
|
"mask": 24,
|
||||||
|
"description": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"vlans": []
|
||||||
|
}
|
||||||
43
tests/fixtures/quasar/config_2.conf
vendored
Normal file
43
tests/fixtures/quasar/config_2.conf
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Copyright © 2021-2024, TechArgos LLC
|
||||||
|
# ----------- ------------------
|
||||||
|
# Component Version
|
||||||
|
# ----------- ------------------
|
||||||
|
# Assembly 0.2.23_9.9.1_GA
|
||||||
|
# Engine 2.23.15.2024-08-22
|
||||||
|
# DPlane 0.2.20.2023-10-17
|
||||||
|
# BfMonitor 1.4.0.2024-08-27
|
||||||
|
# CLI.core 1.0.3.2022-09-30
|
||||||
|
# CLI.engine 1.3.4.2024-08-22
|
||||||
|
# RConsole 1.0.1.2022-12-23
|
||||||
|
# RcAppParams 1.0.1.2022-12-23
|
||||||
|
# SNMP 0.3.0.2024-07-08
|
||||||
|
# Zabbix 0.2.13.2022-01-26
|
||||||
|
# WebUI 1.3.34.2024-08-30
|
||||||
|
# NMS.agent 1.2.12.2024-08-21
|
||||||
|
# SysLog 0.7.12.2024-02-02
|
||||||
|
# NTPd 0.1.5.2024-07-15
|
||||||
|
# AAA 0.0.8.2023-11-16
|
||||||
|
#
|
||||||
|
# ------------------------- --------------------
|
||||||
|
# Platform EEPROM field Value
|
||||||
|
# ------------------------- --------------------
|
||||||
|
# Product Name D5232C-T
|
||||||
|
# Part Number TA-PB-D5232C-T-AC-PI
|
||||||
|
# Local MAC N/A
|
||||||
|
# Product Serial Number WHF1C87123456A
|
||||||
|
# Product Version 0.0
|
||||||
|
# System Manufacturing Date 2026-06-07
|
||||||
|
config ethernet ipv4 address 25.25.18.19/24 gateway 25.25.18.254 enable
|
||||||
|
config interface 1/1 description "TEST_HW_1_1"
|
||||||
|
config interface 1/3 description "TEST_HW_1_3"
|
||||||
|
config interface 2/1 description "TEST_HW_2_1"
|
||||||
|
config interface 2/2 description "TEST_HW_2_2"
|
||||||
|
config interface 2/3 description "TEST_HW_2_3"
|
||||||
|
config interface 2/4 description "TEST_HW_2_4"
|
||||||
|
config interface 3/1 description "TEST_HW_3_1"
|
||||||
|
config interface 3/3 description "TEST_HW_3_3"
|
||||||
|
config interface 3/4 description "TEST_HW_3_4"
|
||||||
|
config interface 4/1 description "TEST_HW_4_1"
|
||||||
|
config interface 4/2 description "TEST_HW_4_2"
|
||||||
|
config interface 4/3 description "TEST_HW_4_3"
|
||||||
|
config interface 4/4 description "TEST_HW_4_4"
|
||||||
94
tests/fixtures/quasar/config_2.expected.json
vendored
Normal file
94
tests/fixtures/quasar/config_2.expected.json
vendored
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
{
|
||||||
|
"system": {
|
||||||
|
"model": "D5232C-T",
|
||||||
|
"serial_number": "WHF1C87123456A",
|
||||||
|
"version": "0.2.23_9.9.1_GA"
|
||||||
|
},
|
||||||
|
"interfaces": [
|
||||||
|
{
|
||||||
|
"interface": "1/1",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": "TEST_HW_1_1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "1/3",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": "TEST_HW_1_3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "2/1",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": "TEST_HW_2_1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "2/2",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": "TEST_HW_2_2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "2/3",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": "TEST_HW_2_3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "2/4",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": "TEST_HW_2_4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "3/1",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": "TEST_HW_3_1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "3/3",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": "TEST_HW_3_3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "3/4",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": "TEST_HW_3_4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "4/1",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": "TEST_HW_4_1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "4/2",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": "TEST_HW_4_2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "4/3",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": "TEST_HW_4_3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "4/4",
|
||||||
|
"ip_address": null,
|
||||||
|
"mask": null,
|
||||||
|
"description": "TEST_HW_4_4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface": "ethernet",
|
||||||
|
"ip_address": "25.25.18.19",
|
||||||
|
"mask": 24,
|
||||||
|
"description": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"vlans": []
|
||||||
|
}
|
||||||
39
tests/test_models.py
Normal file
39
tests/test_models.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from conftest import FIXTURES, load
|
||||||
|
|
||||||
|
from oxi.interfaces import device_registry
|
||||||
|
|
||||||
|
MODEL_CASES = [
|
||||||
|
("mikrotik", "config.conf", "config.expected.json"),
|
||||||
|
("keenetic", "config.conf", "config.expected.json"),
|
||||||
|
("qtech", "config_1.conf", "config_1.expected.json"),
|
||||||
|
("qtech", "config_2.conf", "config_2.expected.json"),
|
||||||
|
("huawei", "config.conf", "config.expected.json"),
|
||||||
|
("eltex", "config.conf", "config.expected.json"),
|
||||||
|
("h3c", "config.conf", "config.expected.json"),
|
||||||
|
("quasar", "config_1.conf", "config_1.expected.json"),
|
||||||
|
("quasar", "config_2.conf", "config_2.expected.json"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("model_key, fixture, expected_file", MODEL_CASES)
|
||||||
|
def test_parse_matches_golden(model_key, fixture, expected_file):
|
||||||
|
cls = device_registry[model_key]
|
||||||
|
raw = load(model_key, fixture)
|
||||||
|
|
||||||
|
parsed = cls(raw).parse().model_dump(by_alias=True, mode="json")
|
||||||
|
|
||||||
|
expected = json.loads((FIXTURES / model_key / expected_file).read_text("utf-8"))
|
||||||
|
assert parsed == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("model_key, fixture, _expected", MODEL_CASES)
|
||||||
|
def test_parse_has_required_sections(model_key, fixture, _expected):
|
||||||
|
cls = device_registry[model_key]
|
||||||
|
device = cls(load(model_key, fixture)).parse()
|
||||||
|
|
||||||
|
assert device.system is not None
|
||||||
|
assert isinstance(device.interfaces, list)
|
||||||
|
assert isinstance(device.vlans, list)
|
||||||
87
tests/test_network.py
Normal file
87
tests/test_network.py
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import pytest
|
||||||
|
import responses
|
||||||
|
from conftest import load
|
||||||
|
|
||||||
|
from oxi import OxiAPI
|
||||||
|
from oxi.exception import OxiAPIError
|
||||||
|
|
||||||
|
BASE = "https://oxi.example.com"
|
||||||
|
|
||||||
|
NODE_DATA = {
|
||||||
|
"name": "HQ",
|
||||||
|
"full_name": "grp/HQ",
|
||||||
|
"model": "keenetic",
|
||||||
|
"ip": "192.168.1.1",
|
||||||
|
"group": "grp",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_node_show_returns_view():
|
||||||
|
responses.get(f"{BASE}/node/show/HQ.json", json=NODE_DATA)
|
||||||
|
|
||||||
|
api = OxiAPI(url=BASE)
|
||||||
|
node = api.node("HQ")
|
||||||
|
|
||||||
|
assert node.ip == "192.168.1.1"
|
||||||
|
assert node.model == "keenetic"
|
||||||
|
assert node.full_name == "grp/HQ"
|
||||||
|
assert node.group == "grp"
|
||||||
|
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_node_config_fetches_and_parses():
|
||||||
|
responses.get(f"{BASE}/node/show/HQ.json", json=NODE_DATA)
|
||||||
|
responses.get(f"{BASE}/node/fetch/grp/HQ", body=load("keenetic"))
|
||||||
|
|
||||||
|
api = OxiAPI(url=BASE)
|
||||||
|
config = api.node("HQ").config
|
||||||
|
|
||||||
|
assert config.system.model == "Sprinter (KN-3710)"
|
||||||
|
assert len(config.interfaces) > 0
|
||||||
|
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_node_not_found_maps_to_404():
|
||||||
|
responses.get(f"{BASE}/node/show/missing.json", status=404)
|
||||||
|
|
||||||
|
api = OxiAPI(url=BASE)
|
||||||
|
with pytest.raises(OxiAPIError) as exc:
|
||||||
|
api.node("missing")
|
||||||
|
|
||||||
|
assert exc.value.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_500_with_node_not_found_html_maps_to_404():
|
||||||
|
responses.get(
|
||||||
|
f"{BASE}/node/show/ghost.json",
|
||||||
|
status=500,
|
||||||
|
content_type="text/html",
|
||||||
|
body="<html><title>Oxidized::NodeNotFound</title></html>",
|
||||||
|
)
|
||||||
|
|
||||||
|
api = OxiAPI(url=BASE)
|
||||||
|
with pytest.raises(OxiAPIError) as exc:
|
||||||
|
api.node("ghost")
|
||||||
|
|
||||||
|
assert exc.value.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_reload_returns_status_code():
|
||||||
|
responses.get(f"{BASE}/reload", status=200)
|
||||||
|
|
||||||
|
api = OxiAPI(url=BASE)
|
||||||
|
assert api.reload() == 200
|
||||||
|
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_unknown_model_raises_value_error():
|
||||||
|
data = {**NODE_DATA, "model": "unknown_vendor"}
|
||||||
|
responses.get(f"{BASE}/node/show/HQ.json", json=data)
|
||||||
|
responses.get(f"{BASE}/node/fetch/grp/HQ", body="whatever")
|
||||||
|
|
||||||
|
api = OxiAPI(url=BASE)
|
||||||
|
with pytest.raises(ValueError, match="not found in registry"):
|
||||||
|
_ = api.node("HQ").config
|
||||||
65
tests/test_units.py
Normal file
65
tests/test_units.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import pytest
|
||||||
|
from conftest import load
|
||||||
|
|
||||||
|
from oxi.exception import OxiAPIError
|
||||||
|
from oxi.interfaces import device_registry
|
||||||
|
from oxi.interfaces.base import BaseDevice
|
||||||
|
from oxi.interfaces.utils import decode_utf, expand_vlan_range
|
||||||
|
|
||||||
|
|
||||||
|
class TestExpandVlanRange:
|
||||||
|
@pytest.mark.parametrize("expand", [expand_vlan_range])
|
||||||
|
def test_simple_and_range(self, expand):
|
||||||
|
assert expand("1,7,14-15") == ["1", "7", "14", "15"]
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("expand", [expand_vlan_range])
|
||||||
|
def test_reversed_range_is_normalized(self, expand):
|
||||||
|
assert expand("15-13") == ["13", "14", "15"]
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("expand", [expand_vlan_range])
|
||||||
|
def test_non_numeric_range_kept_verbatim(self, expand):
|
||||||
|
assert expand("a-b") == ["a-b"]
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("expand", [expand_vlan_range])
|
||||||
|
def test_empty(self, expand):
|
||||||
|
assert expand("") == []
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("expand", [expand_vlan_range])
|
||||||
|
def test_list_input(self, expand):
|
||||||
|
assert expand(["1", "3-4"]) == ["1", "3", "4"]
|
||||||
|
|
||||||
|
|
||||||
|
class TestDecodeUtf:
|
||||||
|
def test_plain_text_passthrough(self):
|
||||||
|
assert decode_utf("Plain ASCII") == "Plain ASCII"
|
||||||
|
|
||||||
|
def test_escaped_utf8_is_decoded(self):
|
||||||
|
assert decode_utf(r'"\xd0\x94\xd0\xbe\xd0\xbc"') == "Дом"
|
||||||
|
|
||||||
|
|
||||||
|
class TestTemplateValidation:
|
||||||
|
def test_missing_required_group_raises(self):
|
||||||
|
class OnlySystem(BaseDevice):
|
||||||
|
template = "dummy.ttp"
|
||||||
|
|
||||||
|
def _load_template(self):
|
||||||
|
return '<group name="system"></group>'
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="missing required groups"):
|
||||||
|
OnlySystem("data")
|
||||||
|
|
||||||
|
def test_missing_template_file_raises(self):
|
||||||
|
class NoTemplate(BaseDevice):
|
||||||
|
template = "does_not_exist.ttp"
|
||||||
|
|
||||||
|
with pytest.raises(FileNotFoundError):
|
||||||
|
NoTemplate("data")
|
||||||
|
|
||||||
|
|
||||||
|
class TestNodeNotFound:
|
||||||
|
def test_not_found_config_raises_on_parse(self):
|
||||||
|
device = device_registry["eltex"](load("eltex", "not_found.conf"), name="HQ")
|
||||||
|
assert device.raw is None
|
||||||
|
with pytest.raises(OxiAPIError) as exc:
|
||||||
|
device.parse()
|
||||||
|
assert exc.value.status_code == 404
|
||||||
72
tests/test_view.py
Normal file
72
tests/test_view.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from oxi.conf import ModelView
|
||||||
|
from oxi.interfaces.contract import Interfaces, System
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def system_view():
|
||||||
|
system = System(model="RB951", serial_number="ABC123", version="7.12")
|
||||||
|
return ModelView(system)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def interfaces_view():
|
||||||
|
items = [
|
||||||
|
Interfaces(interface="eth0", ip_address="192.168.1.1", mask=24),
|
||||||
|
Interfaces(interface="eth1", description="uplink"),
|
||||||
|
]
|
||||||
|
return ModelView(items)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSingleModelView:
|
||||||
|
def test_attribute_proxy(self, system_view):
|
||||||
|
assert system_view.model == "RB951"
|
||||||
|
assert system_view.serial_number == "ABC123"
|
||||||
|
|
||||||
|
def test_dump(self, system_view):
|
||||||
|
assert system_view.dump() == {
|
||||||
|
"model": "RB951",
|
||||||
|
"serial_number": "ABC123",
|
||||||
|
"version": "7.12",
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_dump_json(self, system_view):
|
||||||
|
assert json.loads(system_view.dump_json())["model"] == "RB951"
|
||||||
|
|
||||||
|
def test_iter_raises(self, system_view):
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
iter(system_view)
|
||||||
|
|
||||||
|
def test_len_raises(self, system_view):
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
len(system_view)
|
||||||
|
|
||||||
|
def test_getitem_raises(self, system_view):
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
system_view[0]
|
||||||
|
|
||||||
|
|
||||||
|
class TestListModelView:
|
||||||
|
def test_len(self, interfaces_view):
|
||||||
|
assert len(interfaces_view) == 2
|
||||||
|
|
||||||
|
def test_iter(self, interfaces_view):
|
||||||
|
names = [iface.name for iface in interfaces_view]
|
||||||
|
assert names == ["eth0", "eth1"]
|
||||||
|
|
||||||
|
def test_getitem(self, interfaces_view):
|
||||||
|
assert interfaces_view[0].name == "eth0"
|
||||||
|
|
||||||
|
def test_slice(self, interfaces_view):
|
||||||
|
assert len(interfaces_view[:1]) == 1
|
||||||
|
|
||||||
|
def test_dump_uses_aliases(self, interfaces_view):
|
||||||
|
dumped = interfaces_view.dump()
|
||||||
|
assert dumped[0]["interface"] == "eth0"
|
||||||
|
|
||||||
|
def test_dump_json_keeps_unicode(self):
|
||||||
|
view = ModelView([Interfaces(interface="eth0", description="Дом")])
|
||||||
|
assert "Дом" in view.dump_json()
|
||||||
294
uv.lock
generated
294
uv.lock
generated
@@ -1,6 +1,6 @@
|
|||||||
version = 1
|
version = 1
|
||||||
revision = 3
|
revision = 3
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.10"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "annotated-types"
|
name = "annotated-types"
|
||||||
@@ -26,6 +26,22 @@ version = "3.4.4"
|
|||||||
source = { registry = "https://pypi.org/simple" }
|
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" }
|
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 = [
|
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/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/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" },
|
{ 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" },
|
||||||
@@ -93,6 +109,27 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
|
{ url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "exceptiongroup"
|
||||||
|
version = "1.3.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "3.11"
|
version = "3.11"
|
||||||
@@ -102,6 +139,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iniconfig"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oxipy"
|
name = "oxipy"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -112,12 +158,53 @@ dependencies = [
|
|||||||
{ name = "ttp" },
|
{ name = "ttp" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[package.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
{ name = "pytest" },
|
||||||
|
{ name = "responses" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dev-dependencies]
|
||||||
|
dev = [
|
||||||
|
{ name = "pytest" },
|
||||||
|
{ name = "responses" },
|
||||||
|
{ name = "ruff" },
|
||||||
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "pydantic", specifier = ">=2.12.5" },
|
{ name = "pydantic", specifier = ">=2.12.5" },
|
||||||
|
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.3" },
|
||||||
{ name = "requests", specifier = ">=2.32.5" },
|
{ name = "requests", specifier = ">=2.32.5" },
|
||||||
|
{ name = "responses", marker = "extra == 'dev'", specifier = ">=0.26.1" },
|
||||||
{ name = "ttp", specifier = ">=0.10.0" },
|
{ name = "ttp", specifier = ">=0.10.0" },
|
||||||
]
|
]
|
||||||
|
provides-extras = ["dev"]
|
||||||
|
|
||||||
|
[package.metadata.requires-dev]
|
||||||
|
dev = [
|
||||||
|
{ name = "pytest", specifier = ">=9.0.3" },
|
||||||
|
{ name = "responses", specifier = ">=0.26.1" },
|
||||||
|
{ name = "ruff", specifier = ">=0.15.17" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "packaging"
|
||||||
|
version = "26.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pluggy"
|
||||||
|
version = "1.6.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic"
|
name = "pydantic"
|
||||||
@@ -143,6 +230,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" }
|
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 = [
|
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/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/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" },
|
{ 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 +321,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/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/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/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/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/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" },
|
{ 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" },
|
||||||
@@ -231,6 +339,97 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" },
|
{ url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pygments"
|
||||||
|
version = "2.20.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest"
|
||||||
|
version = "9.0.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
||||||
|
{ name = "iniconfig" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "pluggy" },
|
||||||
|
{ name = "pygments" },
|
||||||
|
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyyaml"
|
||||||
|
version = "6.0.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "requests"
|
name = "requests"
|
||||||
version = "2.32.5"
|
version = "2.32.5"
|
||||||
@@ -246,6 +445,99 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
|
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "responses"
|
||||||
|
version = "0.26.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pyyaml" },
|
||||||
|
{ name = "requests" },
|
||||||
|
{ name = "urllib3" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c2/58/1fb6de3503428196df78638f991ec8095274f1ee9723e272ee4d9ff0092b/responses-0.26.1.tar.gz", hash = "sha256:2eb3218553cc8f79b57d257bac23af5e1bf381f5b9390b1767816f0843e01dc2", size = 83088, upload-time = "2026-05-21T19:56:39.747Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3a/31/6a620b4427d546b9e7cca8b3b8c5f0559d9cef2bb9eedcda7f73c1473c19/responses-0.26.1-py3-none-any.whl", hash = "sha256:8aacc4586eb08fb2208ef64a9eb4258d9b0c6e6f4260845f2f018ab847495345", size = 35502, upload-time = "2026-05-21T19:56:38.046Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ruff"
|
||||||
|
version = "0.15.17"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/8c/a9/3abdf488f1bf3d24c699415e454ed554a6350d5d89ce183be1ee0a3361ac/ruff-0.15.17.tar.gz", hash = "sha256:2ec446937fd16c8c4de2674a209cc5af64d9c6f17d21fbf1151054fa0bcf5219", size = 4743346, upload-time = "2026-06-11T17:54:47.663Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/db/4d/e11259f5da07cb6afb2d074c31bf09da9671993f7329d4f15d2fdc458301/ruff-0.15.17-py3-none-linux_armv6l.whl", hash = "sha256:d9feddb927fc68bd295f5eebc587a7e42cfaf9b65f60ca4a2386febff575da8f", size = 10856677, upload-time = "2026-06-11T17:54:49.533Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/29/3e/772d679e1a0dc058e58875bd2c0cb713a0530877b4a76fee3c7966df0d49/ruff-0.15.17-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:25805a226d741c47d274a35ad5c10a7dde175fcddfa511d7cf3da0a21eb3eab7", size = 11223443, upload-time = "2026-06-11T17:55:00.573Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/58/bd41f7688b2fd5623012605130ed70e60aa7f2244baa3d5066bdd61530c8/ruff-0.15.17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f6ad73b14c2d18a3bf8ad7cb6974294d7f613a7898604826058e6ac64918ef4d", size = 10566458, upload-time = "2026-06-11T17:55:07.52Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d8/5b/733371013fcf1ec339e477ece6ab42bfe10bdd9bba8ee88a9516aa56bfc0/ruff-0.15.17-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ba0c1e4f95bcb3869d0d30cbd5917071ef2e28665abfec970cdab0492c713ed", size = 10914483, upload-time = "2026-06-11T17:55:05.501Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/cc/6f24251cc0252f7239391ccb85833f320efad14ebe5b443943f37ced6332/ruff-0.15.17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:81647960f10bff57d2e51cadd0c3950fe598400c852863a038720ef5b8cca91e", size = 10647497, upload-time = "2026-06-11T17:54:57.733Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/dd/0d10c17ce1a1624d6fc3156309c3f834fdb5dfaad026ec90c85684f3990e/ruff-0.15.17-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e01a84ddbc8c16c23055ba3924476850f1bbc1917cebbb9376665a63e74260d", size = 11416967, upload-time = "2026-06-11T17:54:51.461Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2f/91/556bfb156f6144f355e831c23db00b2fc4120f86b3ce81cc5f7fd2df51f3/ruff-0.15.17-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fe9f653152f8f294f9f7e03bf3a453d8b4a27f7a59c78c8666167f2b17b96c", size = 12335770, upload-time = "2026-06-11T17:54:45.793Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/82/8b5999aa13355e926f06d9f42a32dcca862f623bf0363785ff89d607dffd/ruff-0.15.17-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c0fe88a7676e7a05b73174d4d4a59cb2ac21ff8263583f87a81a6018475a978", size = 11575441, upload-time = "2026-06-11T17:54:32.661Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/11/93/f10377bb04109ca0e8cbc483ff1982c54b6d418210041776f93e8cdc7fa9/ruff-0.15.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecfc3c7878fff94633ab0348524e093f9ce3243080416dd7d14f8ba400174719", size = 11557614, upload-time = "2026-06-11T17:54:34.698Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c7/a6/eeeae7f7d5493df41649ab3db92f086b2d0a30199e4efdf8e3dd7a033f24/ruff-0.15.17-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:b8461180b22420b1bdc289909410930761629fddf2a5aaf60fae1ab26cedc4c4", size = 11544450, upload-time = "2026-06-11T17:54:39.042Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/32/88/5991ce565129a24dd4a00db1254b3b5db2e53018cbe4018ea5a89738e727/ruff-0.15.17-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6eccbe50a038b503e7140b441aa9c7fc8c1f36edf23ebef9f4165c2f28f568b7", size = 10892524, upload-time = "2026-06-11T17:55:09.432Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f5/1d/0fdd248313425f55223968af04b0a42125466a8d88d21c1d99c6af0a51e8/ruff-0.15.17-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:382fc0521025f5a8ad447d8bdd523545d0d7646adb718eb1c2dac5065ec27c0f", size = 10659573, upload-time = "2026-06-11T17:54:36.824Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/0e/072e8260deb9461062ce9311ced27a8e541229a6ffd483013dd37661e43e/ruff-0.15.17-py3-none-musllinux_1_2_i686.whl", hash = "sha256:456d41fcd1b2777ad63f09a6e7121d43f7b688bbc76a800c10f7f8fb1f912c3f", size = 11127818, upload-time = "2026-06-11T17:55:03.124Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ab/b4/55060a34163121498014696b5f656db5b8c6963768f227dbf0d76b311073/ruff-0.15.17-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b1a04bcc94ae6194e9db05d16ad31f298a7194bfbcb08258bbe589cee1d587b8", size = 11655901, upload-time = "2026-06-11T17:54:53.562Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/71/9b29d6b87cef468d697f43c6a91e3fae4a80185779d7d5a4ef27d173439f/ruff-0.15.17-py3-none-win32.whl", hash = "sha256:596065960ab1ff593f744220c9fe6580eda00a95003cffa9f4048bb5b1bf0392", size = 10925574, upload-time = "2026-06-11T17:54:55.723Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/b2/8fc77f3723228836fa5d12497eb71c808f83782e10d058d2b15cfa14640b/ruff-0.15.17-py3-none-win_amd64.whl", hash = "sha256:6769e5fa1710b179b92e0bfa5a51735b35baea9013dadb06d5f44cbcf9547084", size = 12058788, upload-time = "2026-06-11T17:54:41.042Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2d/c7/c53e8dbff9c9dc4b7928773421ae294a5d28fcb8dcda1a089579d3a7e510/ruff-0.15.17-py3-none-win_arm64.whl", hash = "sha256:f3be1fbb34bcdfd146240d8fb92a709d4c2c8191348580a3c044ec60fa0b4456", size = 11355275, upload-time = "2026-06-11T17:54:43.635Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tomli"
|
||||||
|
version = "2.4.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ttp"
|
name = "ttp"
|
||||||
version = "0.10.0"
|
version = "0.10.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user