Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b92e342e5 | ||
|
|
1cc225917e | ||
|
|
61892d8f51 | ||
|
|
8cebbf743a | ||
|
|
a107662e99 | ||
|
|
1d0f5ed685 | ||
|
|
5b8380aeee | ||
|
|
65c82fbaf5 | ||
|
|
974fff6038 | ||
|
|
586e52282b | ||
|
|
e3392f6c76 | ||
|
|
de0e09af9d | ||
|
|
ca96d2600a | ||
|
|
56eae15e27 | ||
|
|
db79199319 | ||
|
|
2e109db121 | ||
|
|
b9dce8e417 | ||
|
|
d185dc6c7c | ||
|
|
68566a24fb | ||
|
|
08733bd493 | ||
|
|
a1c57733f6 | ||
|
|
c9f6f3472f | ||
|
|
1e84452eb5 | ||
|
|
4ed34227d9 | ||
|
|
2a027ed736 | ||
|
|
bf6dd0c189 | ||
|
|
c382627f9f | ||
|
|
197550558d | ||
|
|
9e4574e869 | ||
|
|
b6630a4d30 | ||
|
|
2c3f5ce354 | ||
|
|
77f539254c | ||
|
|
8a126d8806 | ||
|
|
c9d171e2fa | ||
|
|
a016db644d | ||
|
|
fe6d0c4ccf | ||
|
|
ac835d6b56 | ||
|
|
3159570e27 | ||
|
|
16e97692fc | ||
|
|
3fdff33e2e | ||
|
|
753268a381 | ||
|
|
3635a07b27 | ||
|
|
2394296f5b | ||
|
|
a41cb1f7ec | ||
|
|
a938fe2d47 | ||
|
|
685ff19d2f | ||
|
|
c434712309 | ||
|
|
91b6606e3f | ||
|
|
72cd796803 | ||
|
|
e82bc25346 | ||
|
|
2cfcc41e58 | ||
|
|
544688dae1 | ||
|
|
b60182ef3c | ||
|
|
8e85086d98 |
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Python-generated files
|
||||||
|
__pycache__/
|
||||||
|
*.py[oc]
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
wheels/
|
||||||
|
*.egg-info
|
||||||
|
main.py
|
||||||
|
# Virtual environments
|
||||||
|
.venv
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# etc files
|
||||||
|
*.txt
|
||||||
1
.python-version
Normal file
1
.python-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
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.
|
||||||
328
README.md
328
README.md
@@ -0,0 +1,328 @@
|
|||||||
|
# oxipy
|
||||||
|
|
||||||
|
Python-клиент для работы с Oxidized API — системой управления конфигурацией сетевых устройств. Предоставляет удобный интерфейс для получения конфигураций узлов, их парсинга и работы с результатами.
|
||||||
|
|
||||||
|
## Содержание
|
||||||
|
|
||||||
|
- [Установка](#установка)
|
||||||
|
- [Быстрый старт](#быстрый-старт)
|
||||||
|
- [API Reference](#api-reference)
|
||||||
|
- [OxiAPI](#oxiapi)
|
||||||
|
- [NodeView](#nodeview)
|
||||||
|
- [NodeConfig](#nodeconfig)
|
||||||
|
- [ModelView](#modelview)
|
||||||
|
- [Поддерживаемые устройства](#поддерживаемые-устройства)
|
||||||
|
- [Дополнительно](#дополнительно)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Установка
|
||||||
|
|
||||||
|
> Пакет распространяется через Gitea Package Registry и исходники репозитория.
|
||||||
|
> В PyPI пакет не публикуется.
|
||||||
|
|
||||||
|
**Требования:** Python 3.11+
|
||||||
|
|
||||||
|
### Из Gitea Package Registry
|
||||||
|
|
||||||
|
Добавьте registry в конфигурацию pip и установите пакет:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install oxipy \
|
||||||
|
--index-url https://gitea.imbastark.ru/api/packages/Netbox/pypi/simple/
|
||||||
|
```
|
||||||
|
|
||||||
|
Или пропишите registry постоянно в `pip.conf` / `pip.ini`, чтобы не указывать `--index-url` каждый раз:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# ~/.config/pip/pip.conf (Linux/macOS)
|
||||||
|
# %APPDATA%\pip\pip.ini (Windows)
|
||||||
|
|
||||||
|
[global]
|
||||||
|
extra-index-url = https://gitea.imbastark.ru/api/packages/Netbox/pypi/simple/
|
||||||
|
```
|
||||||
|
|
||||||
|
После этого достаточно:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install oxipy
|
||||||
|
```
|
||||||
|
|
||||||
|
Если registry требует аутентификации, передайте токен:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install oxipy \
|
||||||
|
--index-url https://__token__:<your_token>@gitea.imbastark.ru/api/packages/Netbox/pypi/simple/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Из репозитория Gitea
|
||||||
|
|
||||||
|
Установка напрямую через pip без клонирования:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install git+https://gitea.imbastark.ru/Netbox/oxipy.git
|
||||||
|
```
|
||||||
|
|
||||||
|
Конкретный тег или ветка:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install git+https://gitea.imbastark.ru/Netbox/oxipy.git@v0.1.0
|
||||||
|
pip install git+https://gitea.imbastark.ru/Netbox/oxipy.git@dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Для разработки (editable install):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://gitea.imbastark.ru/Netbox/oxipy
|
||||||
|
cd oxipy
|
||||||
|
pip install -e .
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Быстрый старт
|
||||||
|
|
||||||
|
```python
|
||||||
|
from oxi import OxiAPI
|
||||||
|
|
||||||
|
api = OxiAPI(url="https://oxi.example.com", verify=False)
|
||||||
|
|
||||||
|
node = api.node("Router_HOME")
|
||||||
|
|
||||||
|
print(node.ip)
|
||||||
|
print(node.model)
|
||||||
|
print(node.full_name)
|
||||||
|
|
||||||
|
>>> 192.168.1.1
|
||||||
|
>>> keenetic
|
||||||
|
>>> router/HQ
|
||||||
|
|
||||||
|
print(node.config.system.model)
|
||||||
|
print(node.config.interfaces.json())
|
||||||
|
print(node.config.vlans.json())
|
||||||
|
|
||||||
|
>>> Sprinter (KN-3710)
|
||||||
|
>>>
|
||||||
|
[
|
||||||
|
{"name":"Bridge1","ip_address":"192.168.1.1","mask":24,"description":"\"Guest network\""},
|
||||||
|
{"name":"Bridge0","ip_address":"172.16.1.1","mask":24,"description":"\"Home network\""}
|
||||||
|
]
|
||||||
|
>>>
|
||||||
|
[
|
||||||
|
{"vlan_id":1,"name":"Home VLAN"},
|
||||||
|
{"vlan_id":2,"name":"Подключение Ethernet"},
|
||||||
|
{"vlan_id":3,"name":"Home network"}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### OxiAPI
|
||||||
|
|
||||||
|
Точка входа. Управляет HTTP-сессией и предоставляет доступ к узлам.
|
||||||
|
|
||||||
|
```python
|
||||||
|
OxiAPI(
|
||||||
|
url: str,
|
||||||
|
username: str | None = None,
|
||||||
|
password: str | None = None,
|
||||||
|
verify: bool = True,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
| Параметр | Тип | Описание |
|
||||||
|
| ---------- | ------ | --------------------------------------------------------- |
|
||||||
|
| `url` | `str` | Базовый URL Oxi API, например `https://oxi.example.com` |
|
||||||
|
| `username` | `str` | Имя пользователя для базовой аутентификации (опционально) |
|
||||||
|
| `password` | `str` | Пароль для базовой аутентификации (опционально) |
|
||||||
|
| `verify` | `bool` | Проверять SSL-сертификат. `True` по умолчанию |
|
||||||
|
|
||||||
|
|
||||||
|
**Пример:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Без аутентификации
|
||||||
|
api = OxiAPI(url="https://oxi.example.com")
|
||||||
|
|
||||||
|
# С базовой аутентификацией
|
||||||
|
api = OxiAPI(
|
||||||
|
url="https://oxi.example.com",
|
||||||
|
username="admin",
|
||||||
|
password="secret",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Использование как контекстного менеджера (автоматически закрывает сессию)
|
||||||
|
with OxiAPI(url="https://oxi.example.com") as api:
|
||||||
|
node = api.node("HQ")
|
||||||
|
print(node.ip)
|
||||||
|
|
||||||
|
>>> 192.168.1.1
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `api.node(name)`
|
||||||
|
|
||||||
|
Возвращает `[NodeView](#nodeview)` для указанного узла.
|
||||||
|
|
||||||
|
```python
|
||||||
|
node = api.node("HQ")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NodeView
|
||||||
|
|
||||||
|
Представление узла сети. Содержит метаданные и ленивый доступ к конфигурации.
|
||||||
|
|
||||||
|
|
||||||
|
| Свойство | Тип | Описание |
|
||||||
|
| ----------- | ------------ | ---------------------------------------------------- |
|
||||||
|
| `ip` | `str` | IP-адрес узла |
|
||||||
|
| `full_name` | `str` | Полное имя узла в Oxi |
|
||||||
|
| `group` | `str` | Группа, к которой принадлежит узел |
|
||||||
|
| `model` | `str` | Модель устройства (используется для парсинга) |
|
||||||
|
| `config` | `NodeConfig` | Конфигурация узла (загружается при первом обращении) |
|
||||||
|
|
||||||
|
|
||||||
|
**Пример:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
node = api.node("HQ")
|
||||||
|
|
||||||
|
print(node.ip)
|
||||||
|
print(node.group)
|
||||||
|
print(node.model)
|
||||||
|
|
||||||
|
>>> 192.168.1.1
|
||||||
|
>>> branch-office
|
||||||
|
>>> keenetic
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NodeConfig
|
||||||
|
|
||||||
|
Загружает и парсит конфигурацию устройства. Использует TTP-шаблоны, соответствующие модели устройства.
|
||||||
|
|
||||||
|
Доступ к секциям конфигурации осуществляется через свойства, возвращающие `[ModelView](#modelview)`.
|
||||||
|
|
||||||
|
|
||||||
|
| Свойство | Возвращает | Описание |
|
||||||
|
| ------------ | ----------------------------- | ---------------------------------- |
|
||||||
|
| `system` | `ModelView[System]` | Системная информация об устройстве |
|
||||||
|
| `interfaces` | `ModelView[list[Interfaces]]` | Список интерфейсов |
|
||||||
|
| `vlans` | `ModelView[list[Vlans]]` | Список VLAN (если есть) |
|
||||||
|
| `text` | `str` | Сырой текст конфигурации |
|
||||||
|
|
||||||
|
|
||||||
|
**Пример:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
cfg = node.config
|
||||||
|
|
||||||
|
# Системная информация
|
||||||
|
print(cfg.system.model)
|
||||||
|
print(cfg.system.serial_number)
|
||||||
|
print(cfg.system.version)
|
||||||
|
|
||||||
|
>>> Mikrotik RB951Ui-2nD
|
||||||
|
>>> B88C0B31117B
|
||||||
|
>>> 7.16.1
|
||||||
|
|
||||||
|
# Итерация по интерфейсам
|
||||||
|
for iface in cfg.interfaces:
|
||||||
|
print(iface.name, iface.ip_address, iface.mask)
|
||||||
|
|
||||||
|
# Индексация
|
||||||
|
first_iface = cfg.interfaces[0]
|
||||||
|
print(first_iface.name)
|
||||||
|
|
||||||
|
# Количество интерфейсов
|
||||||
|
print(len(cfg.interfaces))
|
||||||
|
|
||||||
|
# JSON-дамп любой секции
|
||||||
|
print(cfg.interfaces.json())
|
||||||
|
print(cfg.vlans.json())
|
||||||
|
print(cfg.system.json())
|
||||||
|
|
||||||
|
# Сырая конфигурация текстом
|
||||||
|
print(cfg.text)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ModelView
|
||||||
|
|
||||||
|
Обёртка над Pydantic-моделью или списком моделей. Обеспечивает сериализацию, итерацию и прозрачный доступ к атрибутам.
|
||||||
|
|
||||||
|
|
||||||
|
| Метод / свойство | Применимо к | Описание |
|
||||||
|
| ---------------- | ------------ | ------------------------------------------------- |
|
||||||
|
| `.json()` | оба варианта | Возвращает JSON-строку (с `by_alias=True`) |
|
||||||
|
| `.<attr>` | оба варианта | Проксирует обращение к атрибутам вложенной модели |
|
||||||
|
| `iter(view)` | список | Итерация по элементам списка моделей |
|
||||||
|
| `len(view)` | список | Количество элементов в списке |
|
||||||
|
| `view[i]` | список | Получение элемента по индексу или срез |
|
||||||
|
|
||||||
|
|
||||||
|
> `__iter__`, `__len__` и `__getitem__` доступны только для `interfaces` и `vlans` (они оборачивают список). Вызов этих методов на `system` (одиночная модель) вызовет `TypeError`.
|
||||||
|
|
||||||
|
**Примеры:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Одиночная модель — system
|
||||||
|
view = node.config.system
|
||||||
|
print(view.json())
|
||||||
|
>>> '{"model":"RB951Ui-2nD","serial_number":"B88C0B31117B","version":"7.12.1"}'
|
||||||
|
print(view.model) # 'RB951Ui-2nD'
|
||||||
|
print(view.serial_number) # 'B88C0B31117B'
|
||||||
|
|
||||||
|
>>> RB951Ui-2nD
|
||||||
|
>>> B88C0B31117B
|
||||||
|
# Список — interfaces
|
||||||
|
interfaces = node.config.interfaces
|
||||||
|
|
||||||
|
# Итерация
|
||||||
|
for iface in interfaces:
|
||||||
|
print(iface.name, iface.ip_address)
|
||||||
|
|
||||||
|
# Длина
|
||||||
|
print(len(interfaces)) # 5
|
||||||
|
|
||||||
|
# Индексация и срезы
|
||||||
|
first = interfaces[0]
|
||||||
|
top3 = interfaces[:3]
|
||||||
|
|
||||||
|
# JSON всего списка
|
||||||
|
print(interfaces.json())
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Поддерживаемые устройства
|
||||||
|
|
||||||
|
|
||||||
|
| Устройство | Ключи реестра |
|
||||||
|
| ---------- | -------------------------------- |
|
||||||
|
| Keenetic | `ndms`, `keenetic`, `keeneticos` |
|
||||||
|
| MikroTik | `routeros`, `ros`, `mikrotik` |
|
||||||
|
| Qtech | `qtech` |
|
||||||
|
| Huawei | `huawei`, `vrp` |
|
||||||
|
| Eltex | `eltex` |
|
||||||
|
| H3C | `h3c` |
|
||||||
|
| Quasar | `qos`, `quasar` |
|
||||||
|
|
||||||
|
|
||||||
|
Ключи реестра — это значения поля `model`, возвращаемого API для узла. Регистр не учитывается.
|
||||||
|
|
||||||
|
Добавить поддержку нового устройства можно самостоятельно — подробнее в разделе [Расширение моделей](docs/extending-models.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Дополнительно
|
||||||
|
|
||||||
|
- [Написание TTP-шаблонов](docs/templates.md)
|
||||||
|
- [Расширение и переопределение моделей устройств](docs/extending-models.md)
|
||||||
|
|
||||||
|
|||||||
325
docs/extending-models.md
Normal file
325
docs/extending-models.md
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
# Расширение и переопределение моделей устройств
|
||||||
|
|
||||||
|
oxipy предоставляет гибкий механизм расширения через наследование от `BaseDevice`. После того как TTP-шаблон разобрал конфигурацию в сырой словарь `self.raw`, данные проходят через три метода экземпляра — `system()`, `interfaces()`, `vlans()` — перед тем как попасть в контракт. Переопределяя эти методы, можно трансформировать, фильтровать и обогащать данные без изменения шаблона или контракта.
|
||||||
|
|
||||||
|
## Содержание
|
||||||
|
|
||||||
|
- [Архитектура: путь данных](#архитектура-путь-данных)
|
||||||
|
- [Регистрация нового устройства](#регистрация-нового-устройства)
|
||||||
|
- [Переопределение методов (monkey patching)](#переопределение-методов-monkey-patching)
|
||||||
|
- [interfaces()](#interfaces)
|
||||||
|
- [vlans()](#vlans)
|
||||||
|
- [system()](#system)
|
||||||
|
- [Полный пример: новое устройство](#полный-пример-новое-устройство)
|
||||||
|
- [Контракт: ожидаемые структуры](#контракт-ожидаемые-структуры)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Архитектура: путь данных
|
||||||
|
|
||||||
|
```
|
||||||
|
текст конфигурации
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
TTP-шаблон (.ttp)
|
||||||
|
│ парсит в сырой словарь
|
||||||
|
▼
|
||||||
|
self.raw: dict
|
||||||
|
│
|
||||||
|
├──► system() → dict
|
||||||
|
├──► interfaces() → list[dict]
|
||||||
|
└──► vlans() → list[dict]
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
_validate_contract()
|
||||||
|
│ создаёт Pydantic-модели
|
||||||
|
▼
|
||||||
|
Device(system, interfaces, vlans)
|
||||||
|
```
|
||||||
|
|
||||||
|
Методы `system()`, `interfaces()`, `vlans()` — это точки расширения. Базовая реализация просто возвращает данные из `self.raw`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# BaseDevice (упрощённо)
|
||||||
|
def interfaces(self) -> list[dict]:
|
||||||
|
return self.raw.get("interfaces", [])
|
||||||
|
|
||||||
|
def vlans(self) -> list[dict]:
|
||||||
|
return self.raw.get("vlans", [])
|
||||||
|
|
||||||
|
def system(self) -> dict:
|
||||||
|
return self.raw.get("system", None)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Регистрация нового устройства
|
||||||
|
|
||||||
|
Чтобы добавить поддержку нового вендора:
|
||||||
|
|
||||||
|
1. Создайте файл в `oxi/interfaces/models/`, например `cisco.py`.
|
||||||
|
2. Создайте шаблон `oxi/interfaces/models/templates/cisco.ttp`.
|
||||||
|
3. Унаследуйте класс от `BaseDevice` и зарегистрируйте его декоратором `@register_parser`.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# oxi/interfaces/models/cisco.py
|
||||||
|
from oxi.interfaces import register_parser
|
||||||
|
from oxi.interfaces.base import BaseDevice
|
||||||
|
|
||||||
|
|
||||||
|
@register_parser(["ios", "cisco", "cisco_ios"])
|
||||||
|
class CiscoIOS(BaseDevice):
|
||||||
|
template = "cisco.ttp"
|
||||||
|
```
|
||||||
|
|
||||||
|
Декоратор `@register_parser` принимает список строк — это ключи, по которым устройство ищется в реестре. Поле `model` от API сравнивается с этими ключами без учёта регистра.
|
||||||
|
|
||||||
|
После добавления файла он автоматически импортируется через `pkgutil` при старте приложения — явно импортировать не нужно.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Переопределение методов (monkey patching)
|
||||||
|
|
||||||
|
### interfaces()
|
||||||
|
|
||||||
|
Используйте переопределение, когда нужно:
|
||||||
|
|
||||||
|
- Преобразовать формат IP-адреса (например, `netmask` → `prefix_length`).
|
||||||
|
- Декодировать escape-последовательности в описаниях.
|
||||||
|
- Переименовать ключи, не совпадающие с контрактом.
|
||||||
|
- Фильтровать служебные интерфейсы.
|
||||||
|
|
||||||
|
**Пример: конвертация маски подсети в префикс**
|
||||||
|
|
||||||
|
TTP возвращает `netmask` как `255.255.255.0`, а контракт `Interfaces` ожидает `mask` как целое число (prefix length):
|
||||||
|
|
||||||
|
```python
|
||||||
|
from ipaddress import ip_interface
|
||||||
|
from oxi.interfaces import register_parser
|
||||||
|
from oxi.interfaces.base import BaseDevice
|
||||||
|
|
||||||
|
|
||||||
|
@register_parser(["myvendor"])
|
||||||
|
class MyVendor(BaseDevice):
|
||||||
|
template = "myvendor.ttp"
|
||||||
|
|
||||||
|
def interfaces(self) -> list[dict]:
|
||||||
|
result = []
|
||||||
|
for item in self.raw.get("interfaces", []):
|
||||||
|
if item.get("ip_address") and item.get("netmask"):
|
||||||
|
iface = ip_interface(f"{item['ip_address']}/{item['netmask']}")
|
||||||
|
item["mask"] = iface.network.prefixlen
|
||||||
|
item.pop("netmask", None)
|
||||||
|
result.append(item)
|
||||||
|
return result
|
||||||
|
```
|
||||||
|
|
||||||
|
**Пример: фильтрация служебных интерфейсов**
|
||||||
|
|
||||||
|
```python
|
||||||
|
def interfaces(self) -> list[dict]:
|
||||||
|
return [
|
||||||
|
item for item in self.raw.get("interfaces", [])
|
||||||
|
if not item.get("name", "").startswith("lo")
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Пример: декодирование Unicode escape-последовательностей**
|
||||||
|
|
||||||
|
Некоторые устройства (например, Keenetic) хранят кириллические описания как `\xd0\xb8\xd0\xbc\xd1\x8f`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _decode_utf(self, text: str) -> str:
|
||||||
|
if "\\x" in text:
|
||||||
|
return (
|
||||||
|
text.strip('"')
|
||||||
|
.encode("utf-8")
|
||||||
|
.decode("unicode_escape")
|
||||||
|
.encode("latin1")
|
||||||
|
.decode("utf-8")
|
||||||
|
)
|
||||||
|
return text
|
||||||
|
|
||||||
|
def interfaces(self) -> list[dict]:
|
||||||
|
interfaces = self.raw.get("interfaces", [])
|
||||||
|
for item in interfaces:
|
||||||
|
if item.get("description"):
|
||||||
|
item["description"] = self._decode_utf(item["description"])
|
||||||
|
return interfaces
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### vlans()
|
||||||
|
|
||||||
|
Аналогично `interfaces()`. Используйте для нормализации ID, декодирования названий, обогащения данными из других секций.
|
||||||
|
|
||||||
|
**Пример: добавление префикса к имени VLAN**
|
||||||
|
|
||||||
|
```python
|
||||||
|
def vlans(self) -> list[dict]:
|
||||||
|
result = []
|
||||||
|
for item in self.raw.get("vlans", []):
|
||||||
|
item["description"] = f"VLAN_{item.get('id', '?')}"
|
||||||
|
result.append(item)
|
||||||
|
return result
|
||||||
|
```
|
||||||
|
|
||||||
|
**Пример: объединение данных из нескольких секций**
|
||||||
|
|
||||||
|
```python
|
||||||
|
def vlans(self) -> list[dict]:
|
||||||
|
vlans = {v["id"]: v for v in self.raw.get("vlans", [])}
|
||||||
|
# обогащаем данными из другой секции, если она есть
|
||||||
|
for extra in self.raw.get("vlan_details", []):
|
||||||
|
vlan_id = extra.get("id")
|
||||||
|
if vlan_id in vlans:
|
||||||
|
vlans[vlan_id].update(extra)
|
||||||
|
return list(vlans.values())
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### system()
|
||||||
|
|
||||||
|
Переопределяйте, если структура системной секции отличается от ожидаемой контрактом, или нужно вычислить поля:
|
||||||
|
|
||||||
|
**Пример: собрать серийный номер из нескольких полей**
|
||||||
|
|
||||||
|
```python
|
||||||
|
def system(self) -> dict:
|
||||||
|
raw_system = self.raw.get("system", {})
|
||||||
|
# Устройство возвращает серийный номер в двух частях
|
||||||
|
part1 = raw_system.get("serial_part1", "")
|
||||||
|
part2 = raw_system.get("serial_part2", "")
|
||||||
|
raw_system["serial_number"] = f"{part1}-{part2}"
|
||||||
|
return raw_system
|
||||||
|
```
|
||||||
|
|
||||||
|
**Пример: нормализация строки версии**
|
||||||
|
|
||||||
|
```python
|
||||||
|
def system(self) -> dict:
|
||||||
|
raw_system = self.raw.get("system", {})
|
||||||
|
# Убираем лишнее из "7.12.1 (stable)" → "7.12.1"
|
||||||
|
version = raw_system.get("version", "")
|
||||||
|
raw_system["version"] = version.split()[0] if version else version
|
||||||
|
return raw_system
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Полный пример: новое устройство
|
||||||
|
|
||||||
|
Допустим, нужно добавить поддержку Cisco IOS, где:
|
||||||
|
- IP-адрес и маска разделены пробелом в конфигурации (`ip address 10.0.0.1 255.255.255.0`).
|
||||||
|
- Описание интерфейса может содержать несколько слов.
|
||||||
|
- Серийный номер разделён дефисом в двух строках.
|
||||||
|
|
||||||
|
**Шаблон** (`oxi/interfaces/models/templates/cisco.ttp`):
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<vars>
|
||||||
|
default_system = {
|
||||||
|
"model": "",
|
||||||
|
"serial_number": "",
|
||||||
|
"version": ""
|
||||||
|
}
|
||||||
|
</vars>
|
||||||
|
|
||||||
|
<group name="system" default="default_system">
|
||||||
|
Cisco IOS Software, {{ ignore }} Version {{ version }},{{ ignore('.*') }}
|
||||||
|
Model Number : {{ model }}
|
||||||
|
System serial number : {{ serial_number }}
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group name="interfaces">
|
||||||
|
interface {{ interface | _start_ }}
|
||||||
|
description {{ description | ORPHRASE }}
|
||||||
|
ip address {{ ip_address }} {{ netmask }}
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group name="vlans">
|
||||||
|
vlan {{ id | _start_ }}
|
||||||
|
name {{ description }}
|
||||||
|
</group>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Класс устройства** (`oxi/interfaces/models/cisco.py`):
|
||||||
|
|
||||||
|
```python
|
||||||
|
from ipaddress import ip_interface
|
||||||
|
from oxi.interfaces import register_parser
|
||||||
|
from oxi.interfaces.base import BaseDevice
|
||||||
|
|
||||||
|
|
||||||
|
@register_parser(["ios", "cisco", "cisco_ios"])
|
||||||
|
class CiscoIOS(BaseDevice):
|
||||||
|
template = "cisco.ttp"
|
||||||
|
|
||||||
|
def interfaces(self) -> list[dict]:
|
||||||
|
result = []
|
||||||
|
for item in self.raw.get("interfaces", []):
|
||||||
|
# Конвертируем маску подсети в длину префикса
|
||||||
|
if item.get("ip_address") and item.get("netmask"):
|
||||||
|
iface = ip_interface(f"{item['ip_address']}/{item['netmask']}")
|
||||||
|
item["mask"] = iface.network.prefixlen
|
||||||
|
item.pop("netmask", None)
|
||||||
|
# Фильтруем интерфейсы управления
|
||||||
|
if item.get("interface", "").startswith("Mgmt"):
|
||||||
|
continue
|
||||||
|
result.append(item)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def system(self) -> dict:
|
||||||
|
raw_system = self.raw.get("system", {})
|
||||||
|
# Нормализуем версию: "15.2(4)M3" → оставляем как есть
|
||||||
|
# Убираем лишние пробелы в модели
|
||||||
|
if raw_system.get("model"):
|
||||||
|
raw_system["model"] = raw_system["model"].strip()
|
||||||
|
return raw_system
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Контракт: ожидаемые структуры
|
||||||
|
|
||||||
|
Методы должны возвращать данные в следующем формате. Контракт жёстко проверяется Pydantic.
|
||||||
|
|
||||||
|
### `system()` → `dict`
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"model": "RB951Ui-2nD", # str, обязательно
|
||||||
|
"serial_number": "B88C0B31117B", # str, обязательно
|
||||||
|
"version": "7.12.1", # str, обязательно
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `interfaces()` → `list[dict]`
|
||||||
|
|
||||||
|
```python
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"interface": "ether1", # str, обязательно (alias для поля name)
|
||||||
|
"ip_address": "192.168.1.1", # str | None
|
||||||
|
"mask": 24, # int | None (длина префикса)
|
||||||
|
"description": "LAN", # str | None
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### `vlans()` → `list[dict]`
|
||||||
|
|
||||||
|
```python
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 10, # int, обязательно (alias для поля vlan_id)
|
||||||
|
"description": "MGMT", # str | None (alias для поля name)
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
> Если имя ключа в словаре совпадает с **alias** поля Pydantic-модели, а не с именем атрибута — используйте alias. Модели сконфигурированы с `populate_by_name=True`, поэтому принимаются оба варианта.
|
||||||
306
docs/templates.md
Normal file
306
docs/templates.md
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
# Написание TTP-шаблонов
|
||||||
|
|
||||||
|
oxipy использует библиотеку [TTP (Template Text Parser)](https://ttp.readthedocs.io/) для парсинга конфигураций сетевых устройств в структурированные данные. Шаблоны хранятся в директории `oxi/interfaces/models/templates/`.
|
||||||
|
|
||||||
|
## Содержание
|
||||||
|
|
||||||
|
- [Структура шаблона](#структура-шаблона)
|
||||||
|
- [Обязательные группы](#обязательные-группы)
|
||||||
|
- [Секция system](#секция-system)
|
||||||
|
- [Секция interfaces](#секция-interfaces)
|
||||||
|
- [Секция vlans](#секция-vlans)
|
||||||
|
- [TTP: основные возможности](#ttp-основные-возможности)
|
||||||
|
- [Переменные по умолчанию](#переменные-по-умолчанию)
|
||||||
|
- [Практические примеры](#практические-примеры)
|
||||||
|
- [Валидация шаблона](#валидация-шаблона)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Структура шаблона
|
||||||
|
|
||||||
|
Каждый шаблон — это `.ttp`-файл, состоящий из следующих блоков:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<doc>
|
||||||
|
Описание шаблона (опционально)
|
||||||
|
</doc>
|
||||||
|
|
||||||
|
<vars>
|
||||||
|
<!-- Переменные по умолчанию для групп -->
|
||||||
|
</vars>
|
||||||
|
|
||||||
|
<group name="system">
|
||||||
|
<!-- Правила для системной информации -->
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group name="interfaces">
|
||||||
|
<!-- Правила для интерфейсов -->
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group name="vlans">
|
||||||
|
<!-- Правила для VLAN (опционально) -->
|
||||||
|
</group>
|
||||||
|
```
|
||||||
|
|
||||||
|
Файл-заготовка находится в `oxi/interfaces/models/templates/_template.ttp`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Обязательные группы
|
||||||
|
|
||||||
|
Фреймворк требует наличия в шаблоне **двух обязательных групп**:
|
||||||
|
|
||||||
|
| Группа | Обязательна | Описание |
|
||||||
|
|--------------|-------------|-------------------------------|
|
||||||
|
| `system` | Да | Системная информация |
|
||||||
|
| `interfaces` | Да | Конфигурация интерфейсов |
|
||||||
|
| `vlans` | Нет | Конфигурация VLAN |
|
||||||
|
|
||||||
|
Если обязательная группа отсутствует в шаблоне или TTP не вернул её данные, будет выброшено `ValueError`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Секция system
|
||||||
|
|
||||||
|
Должна возвращать словарь со следующими полями:
|
||||||
|
|
||||||
|
| Поле | Тип | Обязательное | Описание |
|
||||||
|
|-----------------|------|--------------|---------------------|
|
||||||
|
| `model` | str | Да | Модель устройства |
|
||||||
|
| `serial_number` | str | Да | Серийный номер |
|
||||||
|
| `version` | str | Да | Версия прошивки |
|
||||||
|
|
||||||
|
**Пример (MikroTik):**
|
||||||
|
|
||||||
|
Конфигурация:
|
||||||
|
```
|
||||||
|
# version: 7.12.1 (stable)
|
||||||
|
# model = RB951Ui-2nD
|
||||||
|
# serial number = B88C0B31117B
|
||||||
|
```
|
||||||
|
|
||||||
|
Шаблон:
|
||||||
|
```
|
||||||
|
<group name="system">
|
||||||
|
# version: {{ version }}{{ ignore('.*') }}
|
||||||
|
# model = {{ model }}
|
||||||
|
# serial number = {{ serial_number }}
|
||||||
|
</group>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Пример (Keenetic):**
|
||||||
|
|
||||||
|
Конфигурация:
|
||||||
|
```
|
||||||
|
! release: 4.1.7.1-1
|
||||||
|
! model: Keenetic Extra
|
||||||
|
! hw_version: F02B4E7A1C90
|
||||||
|
```
|
||||||
|
|
||||||
|
Шаблон:
|
||||||
|
```
|
||||||
|
<group name="system">
|
||||||
|
! release: {{ version }}
|
||||||
|
! model: {{ model | ORPHRASE }}
|
||||||
|
! hw_version: {{ serial_number }}
|
||||||
|
</group>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Секция interfaces
|
||||||
|
|
||||||
|
Должна возвращать список словарей. Каждый словарь описывает один интерфейс.
|
||||||
|
|
||||||
|
Поля, которые ожидает контракт `Interfaces`:
|
||||||
|
|
||||||
|
| Поле | TTP-имя / alias | Тип | Обязательное |
|
||||||
|
|---------------|-----------------|------------------|--------------|
|
||||||
|
| `name` | `interface` | str | Да |
|
||||||
|
| `ip_address` | `ip_address` | IPv4Address | Нет |
|
||||||
|
| `mask` | `mask` | int (prefix len) | Нет |
|
||||||
|
| `description` | `description` | str | Нет |
|
||||||
|
|
||||||
|
> **Важно:** поле `name` в Pydantic-модели имеет алиас `interface`, поэтому в шаблоне переменную нужно называть именно `interface` **или** переопределить метод `interfaces()` в классе модели (см. [Расширение моделей](extending-models.md)).
|
||||||
|
|
||||||
|
**Пример (MikroTik):**
|
||||||
|
|
||||||
|
Конфигурация:
|
||||||
|
```
|
||||||
|
/ip address
|
||||||
|
add address=192.168.1.1/24 interface=ether1 network=192.168.1.0
|
||||||
|
add address=10.0.0.1/30 comment="WAN link" interface=ether2 network=10.0.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Шаблон:
|
||||||
|
```
|
||||||
|
<group name="interfaces">
|
||||||
|
/ip address
|
||||||
|
add address={{ ip_address | _start_ }}/{{ mask }} interface={{ interface }} network={{ network }}
|
||||||
|
add address={{ ip_address | _start_ }}/{{ mask }} comment={{ description | ORPHRASE | strip('"') }} interface={{ interface }} network={{ network }}
|
||||||
|
</group>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Пример (Keenetic):**
|
||||||
|
|
||||||
|
Конфигурация:
|
||||||
|
```
|
||||||
|
interface GigabitEthernet0/0
|
||||||
|
description "WAN"
|
||||||
|
ip address 10.0.0.2 255.255.255.252
|
||||||
|
interface GigabitEthernet0/1
|
||||||
|
ip address 192.168.1.1 255.255.255.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Шаблон:
|
||||||
|
```
|
||||||
|
<group name="interfaces">
|
||||||
|
interface {{ name | _start_ | exclude("Vlan") }}
|
||||||
|
description {{ description | ORPHRASE }}
|
||||||
|
ip address {{ ip_address }} {{ netmask }}
|
||||||
|
</group>
|
||||||
|
```
|
||||||
|
|
||||||
|
Здесь переменная называется `name`, а не `interface` — это покрывается переопределением метода `interfaces()` в классе `Keenetic`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Секция vlans
|
||||||
|
|
||||||
|
Необязательная группа. Если объявлена в шаблоне, фреймворк ожидает её наличия в результате TTP.
|
||||||
|
|
||||||
|
Поля контракта `Vlans`:
|
||||||
|
|
||||||
|
| Поле | TTP-имя / alias | Тип | Обязательное |
|
||||||
|
|-----------|-----------------|------|--------------|
|
||||||
|
| `vlan_id` | `id` | int | Да |
|
||||||
|
| `name` | `description` | str | Нет |
|
||||||
|
|
||||||
|
> `vlan_id` имеет алиас `id`, поэтому в шаблоне переменная должна называться `id` либо переименовываться в методе `vlans()`.
|
||||||
|
|
||||||
|
**Пример (Keenetic):**
|
||||||
|
|
||||||
|
Конфигурация:
|
||||||
|
```
|
||||||
|
interface Bridge0/Vlan10
|
||||||
|
description "MGMT"
|
||||||
|
interface Bridge0/Vlan20
|
||||||
|
description "SERVERS"
|
||||||
|
```
|
||||||
|
|
||||||
|
Шаблон:
|
||||||
|
```
|
||||||
|
<group name="vlans">
|
||||||
|
interface {{ ignore }}/Vlan{{ id }}
|
||||||
|
description {{ description | ORPHRASE | strip('"') }}
|
||||||
|
</group>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TTP: основные возможности
|
||||||
|
|
||||||
|
### Маркеры строк
|
||||||
|
|
||||||
|
| Маркер | Описание |
|
||||||
|
|-------------|---------------------------------------------------------------|
|
||||||
|
| `_start_` | Строка с этой переменной считается началом нового совпадения |
|
||||||
|
| `_end_` | Строка с этой переменной завершает совпадение группы |
|
||||||
|
|
||||||
|
```
|
||||||
|
add address={{ ip_address | _start_ }}/{{ mask }} interface={{ name }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Модификаторы переменных
|
||||||
|
|
||||||
|
| Модификатор | Описание |
|
||||||
|
|------------------------|-----------------------------------------------------------|
|
||||||
|
| `ORPHRASE` | Захватывает одно слово или фразу (до конца строки) |
|
||||||
|
| `exclude("pattern")` | Пропускает строку, если захваченное значение содержит паттерн |
|
||||||
|
| `strip('"')` | Удаляет символ из начала и конца захваченного значения |
|
||||||
|
| `replace("old","new")` | Заменяет подстроку в захваченном значении |
|
||||||
|
| `re("pattern")` | Принимает значение, только если оно соответствует regex |
|
||||||
|
| `ignore` | Захватывает, но игнорирует значение (не включает в результат) |
|
||||||
|
| `ignore('.*')` | Игнорирует всё до конца строки |
|
||||||
|
|
||||||
|
### Комментарии в шаблоне
|
||||||
|
|
||||||
|
Строки, начинающиеся с `##`, — это комментарии TTP и не влияют на парсинг:
|
||||||
|
|
||||||
|
```
|
||||||
|
## disabled no comment
|
||||||
|
add address={{ ip_address | _start_ }}/{{ mask }} interface={{ name }}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Переменные по умолчанию
|
||||||
|
|
||||||
|
Блок `<vars>` позволяет задавать значения по умолчанию для группы через атрибут `default`:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<vars>
|
||||||
|
default_system = {
|
||||||
|
"model": "",
|
||||||
|
"serial_number": ""
|
||||||
|
}
|
||||||
|
</vars>
|
||||||
|
|
||||||
|
<group name="system" default="default_system">
|
||||||
|
# version: {{ version }}
|
||||||
|
# model = {{ model }}
|
||||||
|
# serial number = {{ serial_number }}
|
||||||
|
</group>
|
||||||
|
```
|
||||||
|
|
||||||
|
Если шаблон не нашёл совпадений для группы, будет возвращён словарь из `default_system`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Практические примеры
|
||||||
|
|
||||||
|
### Полный шаблон для нового устройства (пример: Cisco IOS)
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<doc>
|
||||||
|
Шаблон для парсинга Cisco IOS running-config
|
||||||
|
</doc>
|
||||||
|
|
||||||
|
<vars>
|
||||||
|
default_system = {
|
||||||
|
"model": "",
|
||||||
|
"serial_number": "",
|
||||||
|
"version": ""
|
||||||
|
}
|
||||||
|
</vars>
|
||||||
|
|
||||||
|
<group name="system" default="default_system">
|
||||||
|
Cisco IOS Software, {{ ignore }} Version {{ version }},{{ ignore('.*') }}
|
||||||
|
Model Number : {{ model }}
|
||||||
|
System serial number : {{ serial_number }}
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group name="interfaces">
|
||||||
|
interface {{ interface | _start_ }}
|
||||||
|
description {{ description | ORPHRASE }}
|
||||||
|
ip address {{ ip_address }} {{ netmask }}
|
||||||
|
shutdown {{ shutdown | set("True") }}
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group name="vlans">
|
||||||
|
vlan {{ id | _start_ }}
|
||||||
|
name {{ description }}
|
||||||
|
</group>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Валидация шаблона
|
||||||
|
|
||||||
|
Фреймворк автоматически выполняет два уровня проверки:
|
||||||
|
|
||||||
|
1. **Валидация структуры шаблона** — при создании объекта устройства парсятся XML-теги `<group>` и проверяется наличие обязательных секций (`system`, `interfaces`).
|
||||||
|
|
||||||
|
2. **Валидация результата парсинга** — после запуска TTP проверяется, что обязательные группы действительно присутствуют в результате (т.е. конфигурация содержала соответствующие строки).
|
||||||
|
|
||||||
|
При нарушении любого условия выбрасывается `ValueError` с подробным описанием проблемы.
|
||||||
6
oxi/__init__.py
Normal file
6
oxi/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from .core import OxiAPI
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"OxiAPI",
|
||||||
|
]
|
||||||
21
oxi/adapter.py
Normal file
21
oxi/adapter.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from typing import Optional
|
||||||
|
from requests.adapters import HTTPAdapter
|
||||||
|
from urllib3.util import Retry
|
||||||
|
|
||||||
|
|
||||||
|
class OxiAdapter(HTTPAdapter):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
timeout: Optional[int] = 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)
|
||||||
94
oxi/conf.py
Normal file
94
oxi/conf.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
from functools import cached_property
|
||||||
|
import json
|
||||||
|
from typing import TYPE_CHECKING, Generic, Iterator, Type, TypeVar
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from .interfaces import BaseDevice, device_registry
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from requests import Session
|
||||||
|
|
||||||
|
TModel = TypeVar("TModel", bound=BaseModel)
|
||||||
|
|
||||||
|
|
||||||
|
class ModelView(Generic[TModel]):
|
||||||
|
def __init__(self, model: TModel | list[TModel]):
|
||||||
|
self._model = model
|
||||||
|
|
||||||
|
def dump_json(self) -> str:
|
||||||
|
if isinstance(self._model, list):
|
||||||
|
return json.dumps(
|
||||||
|
[item.model_dump(by_alias=True) for item in self._model],
|
||||||
|
ensure_ascii=False,
|
||||||
|
)
|
||||||
|
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]:
|
||||||
|
if isinstance(self._model, list):
|
||||||
|
return iter(self._model)
|
||||||
|
raise TypeError("This view wraps a single model, not a list")
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
if isinstance(self._model, list):
|
||||||
|
return len(self._model)
|
||||||
|
raise TypeError("This view wraps a single model, not a list")
|
||||||
|
|
||||||
|
def __getitem__(self, item):
|
||||||
|
if isinstance(self._model, list):
|
||||||
|
return self._model[item]
|
||||||
|
raise TypeError("This view wraps a single model, not a list")
|
||||||
|
|
||||||
|
def __getattr__(self, item):
|
||||||
|
return getattr(self._model, item)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return repr(self._model)
|
||||||
|
|
||||||
|
|
||||||
|
class NodeConfig:
|
||||||
|
def __init__(self, session: "Session", full_name: str, model: str, base_url: str):
|
||||||
|
self._session = session
|
||||||
|
self._full_name = full_name
|
||||||
|
self._model = model.lower()
|
||||||
|
self._url = f"{base_url}/node/fetch/{full_name}"
|
||||||
|
self._device: type[BaseDevice] = device_registry.get(self._model.lower())
|
||||||
|
if self._device is None:
|
||||||
|
raise ValueError(f"Device model '{self._model}' not found in registry")
|
||||||
|
self._parsed_data = self._device(self.text).parse()
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def _response(self):
|
||||||
|
response = self._session.get(self._url)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response
|
||||||
|
|
||||||
|
@property
|
||||||
|
def text(self):
|
||||||
|
return self._response.text
|
||||||
|
|
||||||
|
def dump_json(self):
|
||||||
|
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):
|
||||||
|
return self.text
|
||||||
|
|
||||||
|
@property
|
||||||
|
def vlans(self):
|
||||||
|
return ModelView(self._parsed_data.vlans)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def interfaces(self):
|
||||||
|
return ModelView(self._parsed_data.interfaces)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def system(self):
|
||||||
|
return ModelView(self._parsed_data.system)
|
||||||
51
oxi/core.py
Normal file
51
oxi/core.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
from typing import Optional
|
||||||
|
from requests import HTTPError, Session
|
||||||
|
|
||||||
|
from oxi.adapter import OxiAdapter
|
||||||
|
from oxi.exception import OxiAPIError
|
||||||
|
from .node import Node
|
||||||
|
|
||||||
|
|
||||||
|
class OxiAPI:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
url: str,
|
||||||
|
username: Optional[str] = None,
|
||||||
|
password: Optional[str] = None,
|
||||||
|
verify: bool = True,
|
||||||
|
):
|
||||||
|
self.base_url = url.rstrip("/")
|
||||||
|
self._session = self.__create_session(username, password, verify)
|
||||||
|
self.node = Node(self._session, self.base_url)
|
||||||
|
|
||||||
|
def __create_session(
|
||||||
|
self,
|
||||||
|
username: Optional[str] = None,
|
||||||
|
password: Optional[str] = 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):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *args):
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
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
|
||||||
34
oxi/exception.py
Normal file
34
oxi/exception.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
|
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",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class OxiAPIError(Exception):
|
||||||
|
def __init__(self, message: str, status_code: Optional[int] = None):
|
||||||
|
super().__init__(message)
|
||||||
|
self.status_code = status_code
|
||||||
|
|
||||||
|
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":
|
||||||
|
status = e.response.status_code
|
||||||
|
if status == 404:
|
||||||
|
message = f"{context} not found" if context else "Not found"
|
||||||
|
else:
|
||||||
|
base = _STATUS_MESSAGES.get(status) or e.response.reason or f"HTTP {status}"
|
||||||
|
message = f"{context}: {base}" if context else base
|
||||||
|
return cls(message, status)
|
||||||
26
oxi/interfaces/__init__.py
Normal file
26
oxi/interfaces/__init__.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from typing import Callable, Type
|
||||||
|
|
||||||
|
from .base import BaseDevice
|
||||||
|
|
||||||
|
device_registry = {}
|
||||||
|
|
||||||
|
|
||||||
|
def register_parser(
|
||||||
|
name: list[str] | str,
|
||||||
|
) -> Callable[[Type[BaseDevice]], Type[BaseDevice]]:
|
||||||
|
def wrapper(cls):
|
||||||
|
name_list = []
|
||||||
|
if isinstance(name, str):
|
||||||
|
name_list.append(name)
|
||||||
|
else:
|
||||||
|
name_list.extend(name)
|
||||||
|
for item in name_list:
|
||||||
|
device_registry[item.lower()] = cls
|
||||||
|
return cls
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
from . import models # noqa: E402, F401
|
||||||
|
|
||||||
|
__all__ = ["register_parser", "device_registry", "BaseDevice"]
|
||||||
130
oxi/interfaces/base.py
Normal file
130
oxi/interfaces/base.py
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from pathlib import Path
|
||||||
|
from ttp import ttp
|
||||||
|
from oxi.interfaces.contract import Device
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
from oxi.interfaces.contract import Interfaces, System, Vlans
|
||||||
|
|
||||||
|
|
||||||
|
class BaseDevice(ABC):
|
||||||
|
_REQUIRED_SECTIONS: frozenset[str] = frozenset({"system", "interfaces"})
|
||||||
|
_OPTIONAL_SECTIONS: frozenset[str] = frozenset({"vlans"})
|
||||||
|
|
||||||
|
def __init__(self, config: str):
|
||||||
|
self.config: str = config
|
||||||
|
|
||||||
|
self._loaded_template = self._load_template()
|
||||||
|
self._declared_sections = None
|
||||||
|
self._validate_template_groups()
|
||||||
|
self.raw: dict = self._run_ttp()
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def template(self) -> str:
|
||||||
|
"""
|
||||||
|
Expected structure:
|
||||||
|
Название файла с парсером ttp
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
|
||||||
|
def vlans(self) -> list[dict]:
|
||||||
|
"""
|
||||||
|
Parse VLAN configuration from self.raw['vlans'].
|
||||||
|
|
||||||
|
Expected structure:
|
||||||
|
[{"id": 10, "description": "MGMT"}, {"id": 15, "name": "SSH"}, ...]
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[Vlans]: список VLAN из секции vlans,
|
||||||
|
пустой список если секция отсутствует.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: если raw содержит некорректные данные.
|
||||||
|
"""
|
||||||
|
return self.raw.get("vlans", [])
|
||||||
|
|
||||||
|
def interfaces(self) -> list[dict]:
|
||||||
|
"""
|
||||||
|
Parse Interface configuration from self.raw['interfaces'].
|
||||||
|
|
||||||
|
Expected raw structure:
|
||||||
|
[{"name": "GEthernet1/0/1", "ip_address": "192.168.1.1", "mask": "24", "description": "IPBB interface"}]
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: если raw содержит некорректные данные.
|
||||||
|
"""
|
||||||
|
return self.raw.get("interfaces", [])
|
||||||
|
|
||||||
|
def system(self) -> dict:
|
||||||
|
"""
|
||||||
|
Parse System configuration from self.raw['system'].
|
||||||
|
|
||||||
|
Expected raw structure:
|
||||||
|
{"model":"RB951Ui-2nD", serial_number: "B88C0B31117B", "version": "7.12.1"}
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: если raw содержит некорректные данные.
|
||||||
|
"""
|
||||||
|
return self.raw.get("system", None)
|
||||||
|
|
||||||
|
def _validate_contract(self) -> dict:
|
||||||
|
system_data = self.system()
|
||||||
|
interfaces_data = self.interfaces() or []
|
||||||
|
result = {
|
||||||
|
"system": System(**system_data),
|
||||||
|
"interfaces": [Interfaces(**item) for item in interfaces_data],
|
||||||
|
"vlans": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
if "vlans" in self._declared_sections:
|
||||||
|
if "vlans" not in self.raw:
|
||||||
|
raise ValueError(
|
||||||
|
f"{self.__class__.__name__}: template '{self.template}' declares optional group "
|
||||||
|
f"'vlans', but TTP did not return it."
|
||||||
|
)
|
||||||
|
vlans_data = self.vlans() or []
|
||||||
|
result["vlans"] = [Vlans(**item) for item in vlans_data]
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _load_template(self):
|
||||||
|
"""Подгрузка темплейтов из папки models/templates"""
|
||||||
|
path = Path(__file__).parent / "models" / "templates" / self.template
|
||||||
|
if not path.exists():
|
||||||
|
raise FileNotFoundError(f"Template {self.template} not found")
|
||||||
|
return path.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
def _validate_template_groups(self) -> None:
|
||||||
|
"""Проверяем только обязательные группы в template."""
|
||||||
|
try:
|
||||||
|
root = ET.fromstring(self._loaded_template)
|
||||||
|
except ET.ParseError:
|
||||||
|
root = ET.fromstring(f"<template>{self._loaded_template}</template>")
|
||||||
|
|
||||||
|
declared = {g.get("name") for g in root.iter("group") if g.get("name")}
|
||||||
|
self._declared_sections = declared
|
||||||
|
|
||||||
|
missing_required = self._REQUIRED_SECTIONS - declared
|
||||||
|
if missing_required:
|
||||||
|
raise ValueError(
|
||||||
|
f"{self.__class__.__name__}: template '{self.template}' "
|
||||||
|
f"missing required groups: {sorted(missing_required)}. "
|
||||||
|
f"Declared groups: {sorted(declared)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _run_ttp(self) -> dict:
|
||||||
|
"""Основной парсер"""
|
||||||
|
p = ttp(data=self.config, template=self._loaded_template)
|
||||||
|
p.parse()
|
||||||
|
raw: dict = p.result()[0][0]
|
||||||
|
missing = self._REQUIRED_SECTIONS - raw.keys()
|
||||||
|
if missing:
|
||||||
|
raise ValueError(
|
||||||
|
f"{self.__class__.__name__}: TTP template '{self.template}' "
|
||||||
|
f"did not produce required groups: {sorted(missing)}. "
|
||||||
|
f"Return only: {(raw.keys())}"
|
||||||
|
)
|
||||||
|
return raw
|
||||||
|
|
||||||
|
def parse(self) -> Device:
|
||||||
|
return Device(**self._validate_contract())
|
||||||
42
oxi/interfaces/contract.py
Normal file
42
oxi/interfaces/contract.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
from ipaddress import IPv4Address
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
|
||||||
|
|
||||||
|
class Base(BaseModel):
|
||||||
|
model_config = ConfigDict(populate_by_name=True)
|
||||||
|
|
||||||
|
|
||||||
|
class System(BaseModel):
|
||||||
|
"""
|
||||||
|
Required
|
||||||
|
"""
|
||||||
|
|
||||||
|
model: str
|
||||||
|
serial_number: str
|
||||||
|
version: str
|
||||||
|
|
||||||
|
|
||||||
|
class Interfaces(Base):
|
||||||
|
"""
|
||||||
|
Required
|
||||||
|
"""
|
||||||
|
|
||||||
|
name: str = Field(alias="interface")
|
||||||
|
ip_address: IPv4Address | None = None
|
||||||
|
mask: int | None = None
|
||||||
|
description: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class Vlans(Base):
|
||||||
|
"""
|
||||||
|
Optional
|
||||||
|
"""
|
||||||
|
|
||||||
|
vlan_id: int
|
||||||
|
name: str | None = Field(default=None, alias="description")
|
||||||
|
|
||||||
|
|
||||||
|
class Device(BaseModel):
|
||||||
|
system: System
|
||||||
|
interfaces: list[Interfaces]
|
||||||
|
vlans: list[Vlans] = []
|
||||||
7
oxi/interfaces/models/__init__.py
Normal file
7
oxi/interfaces/models/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import importlib
|
||||||
|
import pkgutil
|
||||||
|
|
||||||
|
package = __package__
|
||||||
|
|
||||||
|
for loader, module_name, is_pkg in pkgutil.iter_modules(__path__):
|
||||||
|
importlib.import_module(f"{package}.{module_name}")
|
||||||
43
oxi/interfaces/models/eltex.py
Normal file
43
oxi/interfaces/models/eltex.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
from oxi.interfaces import register_parser
|
||||||
|
from oxi.interfaces.base import BaseDevice
|
||||||
|
|
||||||
|
|
||||||
|
@register_parser("eltex")
|
||||||
|
class Eltex(BaseDevice):
|
||||||
|
template = "eltex.ttp"
|
||||||
|
|
||||||
|
def system(self) -> dict:
|
||||||
|
system = self.raw["system"]
|
||||||
|
serial_num = self.raw["serial"]
|
||||||
|
if serial_num:
|
||||||
|
if len(serial_num) > 1:
|
||||||
|
serial_num = serial_num[0]
|
||||||
|
system["serial_number"] = serial_num.get("serial_number")
|
||||||
|
return system
|
||||||
|
|
||||||
|
def vlans(self) -> list[dict]:
|
||||||
|
vlans_ttp = self.raw.get("vlans", [])
|
||||||
|
vlans = []
|
||||||
|
named_vlan = set()
|
||||||
|
for item in vlans_ttp:
|
||||||
|
if item.get("vlan_id"):
|
||||||
|
named_vlan.add(item.get("vlan_id"))
|
||||||
|
vlans.append(item)
|
||||||
|
else:
|
||||||
|
ids = item.get("vlan_ids", "")
|
||||||
|
tail = item.get("vlan_tail")
|
||||||
|
if tail:
|
||||||
|
ids = f"{ids},{tail}"
|
||||||
|
for vid in ids:
|
||||||
|
vid = vid.strip()
|
||||||
|
if vid in named_vlan:
|
||||||
|
continue
|
||||||
|
vlans.append({"vlan_id": vid})
|
||||||
|
return vlans
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
with open("./test6.txt") as file:
|
||||||
|
data = file.read()
|
||||||
|
eltex = Eltex(data)
|
||||||
|
print(eltex.parse())
|
||||||
22
oxi/interfaces/models/h3c.py
Normal file
22
oxi/interfaces/models/h3c.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
from oxi.interfaces import BaseDevice, register_parser
|
||||||
|
|
||||||
|
|
||||||
|
@register_parser("h3c")
|
||||||
|
class H3C(BaseDevice):
|
||||||
|
template = "h3c.ttp"
|
||||||
|
|
||||||
|
def vlans(self) -> list[dict]:
|
||||||
|
vlan_list = self.raw["vlans"]
|
||||||
|
vlans = []
|
||||||
|
for item in vlan_list:
|
||||||
|
if item.get("vlans_id"):
|
||||||
|
vlans.extend([{'vlan_id': vln }for vln in item.get("vlans_id")])
|
||||||
|
else:
|
||||||
|
vlans.append(item)
|
||||||
|
return vlans
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
with open("./test5.txt") as file:
|
||||||
|
data = file.read()
|
||||||
|
h3c = H3C(data)
|
||||||
|
print(h3c.parse())
|
||||||
18
oxi/interfaces/models/huawei.py
Normal file
18
oxi/interfaces/models/huawei.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from oxi.interfaces import register_parser
|
||||||
|
from oxi.interfaces.base import BaseDevice
|
||||||
|
|
||||||
|
|
||||||
|
@register_parser(["vrp", "huawei"])
|
||||||
|
class Huawei(BaseDevice):
|
||||||
|
template = "huawei.ttp"
|
||||||
|
|
||||||
|
def vlans(self):
|
||||||
|
vlan_ids = self.raw["vlans"].get("vlan_ids")
|
||||||
|
return [{"vlan_id": vlan} for vlan in vlan_ids]
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
with open("./test4.txt") as file:
|
||||||
|
data = file.read()
|
||||||
|
huawei = Huawei(data)
|
||||||
|
print(huawei.parse())
|
||||||
49
oxi/interfaces/models/keenetic.py
Normal file
49
oxi/interfaces/models/keenetic.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
from ipaddress import ip_interface
|
||||||
|
from oxi.interfaces import register_parser
|
||||||
|
from oxi.interfaces.base import BaseDevice
|
||||||
|
|
||||||
|
|
||||||
|
@register_parser(["NDMS", "keenetic", "KeeneticOS"])
|
||||||
|
class Keenetic(BaseDevice):
|
||||||
|
template = "keenetic.ttp"
|
||||||
|
|
||||||
|
def _decode_utf(self, text: str):
|
||||||
|
if "\\x" in text:
|
||||||
|
desc = text.strip('"')
|
||||||
|
decoded = (
|
||||||
|
desc.encode("utf-8")
|
||||||
|
.decode("unicode_escape")
|
||||||
|
.encode("latin1")
|
||||||
|
.decode("utf-8")
|
||||||
|
)
|
||||||
|
return decoded
|
||||||
|
return text
|
||||||
|
|
||||||
|
def interfaces(self):
|
||||||
|
interfaces: list[dict] = self.raw["interfaces"]
|
||||||
|
for item in interfaces:
|
||||||
|
if item.get("ip_address") and item.get("netmask"):
|
||||||
|
ipaddress = ip_interface(
|
||||||
|
f"{item.get('ip_address')}/{item.get('netmask')}"
|
||||||
|
)
|
||||||
|
item["mask"] = ipaddress.network.prefixlen
|
||||||
|
item.pop("netmask", "Key not found")
|
||||||
|
if item.get("description"):
|
||||||
|
decoded = self._decode_utf(item.get("description", ""))
|
||||||
|
item["description"] = decoded
|
||||||
|
return interfaces
|
||||||
|
|
||||||
|
def vlans(self):
|
||||||
|
vlans = self.raw["vlans"]
|
||||||
|
for item in vlans:
|
||||||
|
if item.get("description"):
|
||||||
|
decoded = self._decode_utf(item.get("description", ""))
|
||||||
|
item["description"] = decoded
|
||||||
|
return vlans
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
with open("./test2.txt") as file:
|
||||||
|
data = file.read()
|
||||||
|
mikr = Keenetic(data)
|
||||||
|
print(mikr.parse().model_dump_json())
|
||||||
16
oxi/interfaces/models/mikrotik.py
Normal file
16
oxi/interfaces/models/mikrotik.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import os
|
||||||
|
from oxi.interfaces import register_parser
|
||||||
|
from oxi.interfaces.base import BaseDevice
|
||||||
|
|
||||||
|
|
||||||
|
@register_parser(["routeros", "ros", "mikrotik"])
|
||||||
|
class Mikrotik(BaseDevice):
|
||||||
|
template = "mikrotik.ttp"
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print(os.path.abspath(os.curdir))
|
||||||
|
with open("./test.txt") as file:
|
||||||
|
data = file.read()
|
||||||
|
mikr = Mikrotik(data)
|
||||||
|
print(mikr.parse().json())
|
||||||
41
oxi/interfaces/models/qtech.py
Normal file
41
oxi/interfaces/models/qtech.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import os
|
||||||
|
from oxi.interfaces import register_parser
|
||||||
|
from oxi.interfaces.base import BaseDevice
|
||||||
|
|
||||||
|
|
||||||
|
@register_parser(["QTECH"])
|
||||||
|
class Qtech(BaseDevice):
|
||||||
|
template = "qtech.ttp"
|
||||||
|
|
||||||
|
def system(self) -> dict:
|
||||||
|
system = self.raw["system"]
|
||||||
|
return system
|
||||||
|
|
||||||
|
def vlans(self) -> list[dict]:
|
||||||
|
vlans_ttp = self.raw["vlans"]
|
||||||
|
vlans = []
|
||||||
|
named_vlan = set()
|
||||||
|
for item in vlans_ttp:
|
||||||
|
if item.get("vlan_id"):
|
||||||
|
named_vlan.add(item.get("vlan_id"))
|
||||||
|
vlans.append(item)
|
||||||
|
else:
|
||||||
|
ids = item.get("vlan_ids", "")
|
||||||
|
tail = item.get("vlan_tail")
|
||||||
|
if tail:
|
||||||
|
ids = f"{ids},{tail}"
|
||||||
|
for vid in ids.split(","):
|
||||||
|
vid = vid.strip()
|
||||||
|
if vid in named_vlan:
|
||||||
|
continue
|
||||||
|
vlans.append({"vlan_id": vid})
|
||||||
|
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)
|
||||||
36
oxi/interfaces/models/quasar.py
Normal file
36
oxi/interfaces/models/quasar.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
from oxi.interfaces import BaseDevice, register_parser
|
||||||
|
|
||||||
|
|
||||||
|
@register_parser(["quasar", "qos"])
|
||||||
|
class Quasar(BaseDevice):
|
||||||
|
template = "quasar.ttp"
|
||||||
|
|
||||||
|
def interfaces(self) -> list[dict]:
|
||||||
|
ether_interfaces: dict = self.raw["interfaces"]
|
||||||
|
interfaces: list[dict] = []
|
||||||
|
bulk_interfaces: dict = self.raw["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"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
interfaces.append(ether_interfaces)
|
||||||
|
return interfaces
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
with open("./test7.txt") as file:
|
||||||
|
data = file.read()
|
||||||
|
quasar = Quasar(data)
|
||||||
|
qt = quasar.parse()
|
||||||
|
print(qt)
|
||||||
|
print()
|
||||||
|
with open("./test8.txt") as file:
|
||||||
|
data = file.read()
|
||||||
|
quasar = Quasar(data)
|
||||||
|
qt = quasar.parse()
|
||||||
|
print(qt)
|
||||||
58
oxi/interfaces/models/templates/_template.ttp
Normal file
58
oxi/interfaces/models/templates/_template.ttp
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<doc>
|
||||||
|
Базовый шаблон для нового устройства. Скопируйте этот файл, переименуйте
|
||||||
|
в <vendor>.ttp и заполните группы под формат конфигурации вашего устройства.
|
||||||
|
|
||||||
|
Обязательные группы: system, interfaces.
|
||||||
|
Опциональная группа: vlans — добавляйте только если устройство поддерживает VLAN.
|
||||||
|
|
||||||
|
--- Группа system ---
|
||||||
|
Должна возвращать одиночный словарь с полями:
|
||||||
|
model (str) — модель устройства
|
||||||
|
serial_number (str) — серийный номер
|
||||||
|
version (str) — версия прошивки
|
||||||
|
|
||||||
|
--- Группа interfaces ---
|
||||||
|
Должна возвращать список словарей. Каждый элемент:
|
||||||
|
interface (str) — имя интерфейса (alias поля name)
|
||||||
|
ip_address (str|None) — IPv4-адрес
|
||||||
|
mask (int|None) — длина префикса (напр. 24)
|
||||||
|
description (str|None) — описание интерфейса
|
||||||
|
|
||||||
|
Если устройство возвращает маску в виде 255.255.255.0, конвертируйте
|
||||||
|
её в prefix length в методе interfaces() класса устройства.
|
||||||
|
|
||||||
|
--- Группа vlans ---
|
||||||
|
Должна возвращать список словарей. Каждый элемент:
|
||||||
|
id (int) — номер VLAN (alias поля vlan_id)
|
||||||
|
description (str|None) — название VLAN (alias поля name)
|
||||||
|
|
||||||
|
--- Полезные модификаторы TTP ---
|
||||||
|
{{ field | ORPHRASE }} — одно слово или фраза до конца строки
|
||||||
|
{{ field | _start_ }} — начало новой записи группы
|
||||||
|
{{ field | strip('"') }} — убрать кавычки
|
||||||
|
{{ field | replace("yes","True") }} — замена подстроки
|
||||||
|
{{ field | exclude("pattern") }} — пропустить строку при совпадении
|
||||||
|
{{ ignore }} — захватить и выбросить значение
|
||||||
|
{{ ignore('.*') }} — выбросить всё до конца строки
|
||||||
|
|
||||||
|
Подробнее: docs/templates.md
|
||||||
|
</doc>
|
||||||
|
<vars>
|
||||||
|
default_system = {
|
||||||
|
"model": "",
|
||||||
|
"serial_number": "",
|
||||||
|
"version": ""
|
||||||
|
}
|
||||||
|
</vars>
|
||||||
|
|
||||||
|
<group name="system" default="default_system">
|
||||||
|
...
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group name="interfaces">
|
||||||
|
...
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group name="vlans">
|
||||||
|
...
|
||||||
|
</group>
|
||||||
68
oxi/interfaces/models/templates/eltex.ttp
Normal file
68
oxi/interfaces/models/templates/eltex.ttp
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<doc>
|
||||||
|
Базовый шаблон для нового устройства. Скопируйте этот файл, переименуйте
|
||||||
|
в <vendor>.ttp и заполните группы под формат конфигурации вашего устройства.
|
||||||
|
|
||||||
|
Обязательные группы: system, interfaces.
|
||||||
|
Опциональная группа: vlans — добавляйте только если устройство поддерживает VLAN.
|
||||||
|
|
||||||
|
--- Группа system ---
|
||||||
|
Должна возвращать одиночный словарь с полями:
|
||||||
|
model (str) — модель устройства
|
||||||
|
serial_number (str) — серийный номер
|
||||||
|
version (str) — версия прошивки
|
||||||
|
|
||||||
|
--- Группа interfaces ---
|
||||||
|
Должна возвращать список словарей. Каждый элемент:
|
||||||
|
interface (str) — имя интерфейса (alias поля name)
|
||||||
|
ip_address (str|None) — IPv4-адрес
|
||||||
|
mask (int|None) — длина префикса (напр. 24)
|
||||||
|
description (str|None) — описание интерфейса
|
||||||
|
|
||||||
|
Если устройство возвращает маску в виде 255.255.255.0, конвертируйте
|
||||||
|
её в prefix length в методе interfaces() класса устройства.
|
||||||
|
|
||||||
|
--- Группа vlans ---
|
||||||
|
Должна возвращать список словарей. Каждый элемент:
|
||||||
|
id (int) — номер VLAN (alias поля vlan_id)
|
||||||
|
description (str|None) — название VLAN (alias поля name)
|
||||||
|
|
||||||
|
--- Полезные модификаторы TTP ---
|
||||||
|
{{ field | ORPHRASE }} — одно слово или фраза до конца строки
|
||||||
|
{{ field | _start_ }} — начало новой записи группы
|
||||||
|
{{ field | strip('"') }} — убрать кавычки
|
||||||
|
{{ field | replace("yes","True") }} — замена подстроки
|
||||||
|
{{ field | exclude("pattern") }} — пропустить строку при совпадении
|
||||||
|
{{ ignore }} — захватить и выбросить значение
|
||||||
|
{{ ignore('.*') }} — выбросить всё до конца строки
|
||||||
|
|
||||||
|
Подробнее: docs/templates.md
|
||||||
|
</doc>
|
||||||
|
<vars>
|
||||||
|
default_system = {
|
||||||
|
"model": "",
|
||||||
|
"serial_number": "",
|
||||||
|
"version": ""
|
||||||
|
}
|
||||||
|
</vars>
|
||||||
|
|
||||||
|
<group name="system" default="default_system">
|
||||||
|
Active-image: {{ ignore }} {{ _start_ }}
|
||||||
|
! Version: {{ version }}
|
||||||
|
</group>
|
||||||
|
<group name="serial" method="table">
|
||||||
|
! Unit MAC address Hardware version Serial number
|
||||||
|
! {{ unit | exclude("-") }} {{ mac_address }} {{ hardware_version }} {{ serial_number }}
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group name="interfaces">
|
||||||
|
interface {{ interface | ORPHRASE }}
|
||||||
|
ip address {{ ip_address }} {{ mask | to_cidr }}
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group name="vlans">
|
||||||
|
interface vlan {{ vlan_id | _start_ }}
|
||||||
|
name {{ name }}
|
||||||
|
|
||||||
|
vlan {{ _db_ | _start_ }}
|
||||||
|
vlan {{ vlan_ids | joinmatches(',') | unrange("-", ",") | split(",")}}
|
||||||
|
</group>
|
||||||
66
oxi/interfaces/models/templates/h3c.ttp
Normal file
66
oxi/interfaces/models/templates/h3c.ttp
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<doc>
|
||||||
|
Базовый шаблон для нового устройства. Скопируйте этот файл, переименуйте
|
||||||
|
в <vendor>.ttp и заполните группы под формат конфигурации вашего устройства.
|
||||||
|
|
||||||
|
Обязательные группы: system, interfaces.
|
||||||
|
Опциональная группа: vlans — добавляйте только если устройство поддерживает VLAN.
|
||||||
|
|
||||||
|
--- Группа system ---
|
||||||
|
Должна возвращать одиночный словарь с полями:
|
||||||
|
model (str) — модель устройства
|
||||||
|
serial_number (str) — серийный номер
|
||||||
|
version (str) — версия прошивки
|
||||||
|
|
||||||
|
--- Группа interfaces ---
|
||||||
|
Должна возвращать список словарей. Каждый элемент:
|
||||||
|
interface (str) — имя интерфейса (alias поля name)
|
||||||
|
ip_address (str|None) — IPv4-адрес
|
||||||
|
mask (int|None) — длина префикса (напр. 24)
|
||||||
|
description (str|None) — описание интерфейса
|
||||||
|
|
||||||
|
Если устройство возвращает маску в виде 255.255.255.0, конвертируйте
|
||||||
|
её в prefix length в методе interfaces() класса устройства.
|
||||||
|
|
||||||
|
--- Группа vlans ---
|
||||||
|
Должна возвращать список словарей. Каждый элемент:
|
||||||
|
id (int) — номер VLAN (alias поля vlan_id)
|
||||||
|
description (str|None) — название VLAN (alias поля name)
|
||||||
|
|
||||||
|
--- Полезные модификаторы TTP ---
|
||||||
|
{{ field | ORPHRASE }} — одно слово или фраза до конца строки
|
||||||
|
{{ field | _start_ }} — начало новой записи группы
|
||||||
|
{{ field | strip('"') }} — убрать кавычки
|
||||||
|
{{ field | replace("yes","True") }} — замена подстроки
|
||||||
|
{{ field | exclude("pattern") }} — пропустить строку при совпадении
|
||||||
|
{{ ignore }} — захватить и выбросить значение
|
||||||
|
{{ ignore('.*') }} — выбросить всё до конца строки
|
||||||
|
|
||||||
|
Подробнее: docs/templates.md
|
||||||
|
</doc>
|
||||||
|
<vars>
|
||||||
|
default_system = {
|
||||||
|
"model": "",
|
||||||
|
"serial_number": "",
|
||||||
|
"version": ""
|
||||||
|
}
|
||||||
|
</vars>
|
||||||
|
|
||||||
|
<group name="system" default="default_system">
|
||||||
|
# Boot image version: {{ version }}, Release {{ release }}
|
||||||
|
# {{ mpu }} Slot {{ slot }}:
|
||||||
|
# BOARD TYPE: {{ model }}
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group name="interfaces">
|
||||||
|
interface {{ interface }}
|
||||||
|
ip address {{ ip_address }} {{ mask | to_cidr }}
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group name="vlans">
|
||||||
|
vlan {{ vlan_id }}
|
||||||
|
name {{ name }}
|
||||||
|
description {{ description }}
|
||||||
|
</group>
|
||||||
|
<group name="vlans">
|
||||||
|
vlan {{ vlans_id | ORPHRASE | contains(" to ") | unrange(" to ", ",") | split(",") }}
|
||||||
|
</group>
|
||||||
58
oxi/interfaces/models/templates/huawei.ttp
Normal file
58
oxi/interfaces/models/templates/huawei.ttp
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<doc>
|
||||||
|
Базовый шаблон для нового устройства. Скопируйте этот файл, переименуйте
|
||||||
|
в <vendor>.ttp и заполните группы под формат конфигурации вашего устройства.
|
||||||
|
|
||||||
|
Обязательные группы: system, interfaces.
|
||||||
|
Опциональная группа: vlans — добавляйте только если устройство поддерживает VLAN.
|
||||||
|
|
||||||
|
--- Группа system ---
|
||||||
|
Должна возвращать одиночный словарь с полями:
|
||||||
|
model (str) — модель устройства
|
||||||
|
serial_number (str) — серийный номер
|
||||||
|
version (str) — версия прошивки
|
||||||
|
|
||||||
|
--- Группа interfaces ---
|
||||||
|
Должна возвращать список словарей. Каждый элемент:
|
||||||
|
interface (str) — имя интерфейса (alias поля name)
|
||||||
|
ip_address (str|None) — IPv4-адрес
|
||||||
|
mask (int|None) — длина префикса (напр. 24)
|
||||||
|
description (str|None) — описание интерфейса
|
||||||
|
|
||||||
|
Если устройство возвращает маску в виде 255.255.255.0, конвертируйте
|
||||||
|
её в prefix length в методе interfaces() класса устройства.
|
||||||
|
|
||||||
|
--- Группа vlans ---
|
||||||
|
Должна возвращать список словарей. Каждый элемент:
|
||||||
|
id (int) — номер VLAN (alias поля vlan_id)
|
||||||
|
description (str|None) — название VLAN (alias поля name)
|
||||||
|
|
||||||
|
--- Полезные модификаторы TTP ---
|
||||||
|
{{ field | ORPHRASE }} — одно слово или фраза до конца строки
|
||||||
|
{{ field | _start_ }} — начало новой записи группы
|
||||||
|
{{ field | strip('"') }} — убрать кавычки
|
||||||
|
{{ field | replace("yes","True") }} — замена подстроки
|
||||||
|
{{ field | exclude("pattern") }} — пропустить строку при совпадении
|
||||||
|
{{ ignore }} — захватить и выбросить значение
|
||||||
|
{{ ignore('.*') }} — выбросить всё до конца строки
|
||||||
|
Подробнее: docs/templates.md
|
||||||
|
</doc>
|
||||||
|
<vars>
|
||||||
|
default_system = {
|
||||||
|
"model": "",
|
||||||
|
"serial_number": "",
|
||||||
|
"version": ""
|
||||||
|
}
|
||||||
|
</vars>
|
||||||
|
|
||||||
|
<group name="system" default="default_system">
|
||||||
|
# VRP (R) software, Version {{ version }} ({{ model }} {{ serial_number }})
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group name="interfaces">
|
||||||
|
interface {{ interface }}
|
||||||
|
description {{ description }}
|
||||||
|
ip address {{ ip_address }} {{ mask | to_cidr }}
|
||||||
|
</group>
|
||||||
|
<group name="vlans">
|
||||||
|
vlan batch {{ vlan_ids | ORPHRASE | unrange(" to ", " ") | split(" ")}}
|
||||||
|
</group>
|
||||||
29
oxi/interfaces/models/templates/keenetic.ttp
Normal file
29
oxi/interfaces/models/templates/keenetic.ttp
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<doc>
|
||||||
|
</doc>
|
||||||
|
<vars>
|
||||||
|
default_system = {
|
||||||
|
"model": "",
|
||||||
|
"serial_number": ""
|
||||||
|
}
|
||||||
|
default_interfaces = {}
|
||||||
|
</vars>
|
||||||
|
|
||||||
|
<group name="system" default="default_system">
|
||||||
|
! release: {{ version }}
|
||||||
|
! model: {{ model | ORPHRASE }}
|
||||||
|
! hw_version: {{ serial_number }}
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group name="interfaces">
|
||||||
|
interface {{ name | _start_ | exclude("Vlan") }}
|
||||||
|
rename {{ rename }}
|
||||||
|
description {{ description | ORPHRASE | strip('"') }}
|
||||||
|
ip address {{ ip_address }} {{ netmask }}
|
||||||
|
{{ shutdown | re("up") | replace("up","False") | strip('"') }}
|
||||||
|
{{ shutdown | re("down") | replace("down","True") | strip('"') }}
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group name="vlans">
|
||||||
|
interface {{ ignore }}/Vlan{{ vlan_id }}
|
||||||
|
description {{ description | ORPHRASE | strip('"') }}
|
||||||
|
</group>
|
||||||
47
oxi/interfaces/models/templates/mikrotik.ttp
Normal file
47
oxi/interfaces/models/templates/mikrotik.ttp
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<doc>
|
||||||
|
some templates
|
||||||
|
</doc>
|
||||||
|
<vars>
|
||||||
|
default_system = {
|
||||||
|
"model": "",
|
||||||
|
"serial_number": ""
|
||||||
|
}
|
||||||
|
default_interfaces = {
|
||||||
|
"disabled": "False"
|
||||||
|
}
|
||||||
|
default_vlans = {
|
||||||
|
"disabled": "False",
|
||||||
|
"mtu": None
|
||||||
|
}
|
||||||
|
</vars>
|
||||||
|
|
||||||
|
<group name="system" default="default_system">
|
||||||
|
# version: {{ version }}{{ ignore('.*') }}
|
||||||
|
# model = {{ model }}
|
||||||
|
# serial number = {{ serial_number }}
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group name="interfaces" default="default_interfaces">
|
||||||
|
/ip address
|
||||||
|
## not disabled and no comment
|
||||||
|
add address={{ ip_address | _start_ }}/{{ mask }} interface={{ name }} network={{ network }}
|
||||||
|
## not disabled and comment with/without quotes
|
||||||
|
add address={{ ip_address | _start_ }}/{{ mask }} comment={{ description | ORPHRASE | exclude("disabled=") | strip('"')}} interface={{ name }} network={{ network }}
|
||||||
|
## disabled no comment
|
||||||
|
add address={{ ip_address | _start_ }}/{{ mask }} disabled={{ disabled | replace("yes","True") | strip('"')}} interface={{ name }} network={{ network }}
|
||||||
|
## disabled with comment with/without quotes
|
||||||
|
add address={{ ip_address | _start_ }}/{{ mask }} comment={{ description | ORPHRASE | exclude("disabled=") | strip('"') }} disabled={{ disabled }} interface={{ name }} network={{ network }}
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group name="vlans">
|
||||||
|
/interface vlan
|
||||||
|
## not disabled and no comment
|
||||||
|
add interface={{ interface | _start_ }} name={{ name | ORPHRASE | strip('"') }} vlan-id={{ vlan_id }}
|
||||||
|
## not disabled and comment with/without quotes
|
||||||
|
add comment={{ comment | _start_ | ORPHRASE | exclude("disabled=") | strip('"')}} interface={{ interface }} name={{ name | ORPHRASE | strip('"') }} vlan-id={{ vlan_id }}
|
||||||
|
## disabled with comment with/without quotes
|
||||||
|
add comment={{ comment | _start_ | ORPHRASE | exclude("disabled=") | strip('"')}} disabled={{ disabled | replace("yes","True") | strip('"') }} interface={{ interface }} name={{ name | ORPHRASE | strip('"') }} vlan-id={{ vlan_id }}
|
||||||
|
## disabled no comment
|
||||||
|
add interface={{ interface | _start_ }} name={{ name | ORPHRASE | strip('"') }} vlan-id={{ vlan_id }} disabled={{ disabled | replace("yes","True") | strip('"') }}
|
||||||
|
</group>
|
||||||
|
|
||||||
32
oxi/interfaces/models/templates/qtech.ttp
Normal file
32
oxi/interfaces/models/templates/qtech.ttp
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<doc>
|
||||||
|
</doc>
|
||||||
|
<vars>
|
||||||
|
default_system = {
|
||||||
|
"model": "",
|
||||||
|
"serial_number": "",
|
||||||
|
"version": ""
|
||||||
|
}
|
||||||
|
</vars>
|
||||||
|
|
||||||
|
<group name="system" default="default_system">
|
||||||
|
! {{ model | ORPHRASE | _start_ }} Series Software, Version {{ ignore }} Build {{ version | 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 name="interfaces">
|
||||||
|
interface {{ interface | ORPHRASE }}
|
||||||
|
description {{ description | ORPHRASE }}
|
||||||
|
ip address {{ ip_address }} {{ mask | to_cidr }}
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group name="vlans">
|
||||||
|
vlan {{ vlan_ids | contains(",", "-") | unrange("-", ",") }}
|
||||||
|
,{{ vlan_tail | unrange("-", ",") }}
|
||||||
|
vlan {{ vlan_id | _start_ }}
|
||||||
|
name {{ name | ORPHRASE }}
|
||||||
|
</group>
|
||||||
66
oxi/interfaces/models/templates/quasar.ttp
Normal file
66
oxi/interfaces/models/templates/quasar.ttp
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<doc>
|
||||||
|
Базовый шаблон для нового устройства. Скопируйте этот файл, переименуйте
|
||||||
|
в <vendor>.ttp и заполните группы под формат конфигурации вашего устройства.
|
||||||
|
|
||||||
|
Обязательные группы: system, interfaces.
|
||||||
|
Опциональная группа: vlans — добавляйте только если устройство поддерживает VLAN.
|
||||||
|
|
||||||
|
--- Группа system ---
|
||||||
|
Должна возвращать одиночный словарь с полями:
|
||||||
|
model (str) — модель устройства
|
||||||
|
serial_number (str) — серийный номер
|
||||||
|
version (str) — версия прошивки
|
||||||
|
|
||||||
|
--- Группа interfaces ---
|
||||||
|
Должна возвращать список словарей. Каждый элемент:
|
||||||
|
interface (str) — имя интерфейса (alias поля name)
|
||||||
|
ip_address (str|None) — IPv4-адрес
|
||||||
|
mask (int|None) — длина префикса (напр. 24)
|
||||||
|
description (str|None) — описание интерфейса
|
||||||
|
|
||||||
|
Если устройство возвращает маску в виде 255.255.255.0, конвертируйте
|
||||||
|
её в prefix length в методе interfaces() класса устройства.
|
||||||
|
|
||||||
|
--- Группа vlans ---
|
||||||
|
Должна возвращать список словарей. Каждый элемент:
|
||||||
|
id (int) — номер VLAN (alias поля vlan_id)
|
||||||
|
description (str|None) — название VLAN (alias поля name)
|
||||||
|
|
||||||
|
--- Полезные модификаторы TTP ---
|
||||||
|
{{ field | ORPHRASE }} — одно слово или фраза до конца строки
|
||||||
|
{{ field | _start_ }} — начало новой записи группы
|
||||||
|
{{ field | strip('"') }} — убрать кавычки
|
||||||
|
{{ field | replace("yes","True") }} — замена подстроки
|
||||||
|
{{ field | exclude("pattern") }} — пропустить строку при совпадении
|
||||||
|
{{ ignore }} — захватить и выбросить значение
|
||||||
|
{{ ignore('.*') }} — выбросить всё до конца строки
|
||||||
|
|
||||||
|
Подробнее: docs/templates.md
|
||||||
|
</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>
|
||||||
30
oxi/node.py
Normal file
30
oxi/node.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from requests import HTTPError
|
||||||
|
|
||||||
|
from oxi.exception import OxiAPIError
|
||||||
|
|
||||||
|
from .view import NodeView
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from requests import Session
|
||||||
|
|
||||||
|
|
||||||
|
class Node:
|
||||||
|
def __init__(self, session: "Session", base_url: str):
|
||||||
|
self._session = session
|
||||||
|
self._base_url = base_url
|
||||||
|
|
||||||
|
def __call__(self, name: str) -> NodeView:
|
||||||
|
try:
|
||||||
|
url = f"{self._base_url}/node/show/{name}"
|
||||||
|
if not url.endswith(".json"):
|
||||||
|
url += ".json"
|
||||||
|
response = self._session.get(url)
|
||||||
|
response.raise_for_status()
|
||||||
|
except HTTPError as e:
|
||||||
|
raise OxiAPIError.from_http_error(e, context=f"Node {name}") from e
|
||||||
|
return NodeView(
|
||||||
|
session=self._session, base_url=self._base_url, data=response.json()
|
||||||
|
)
|
||||||
60
oxi/view.py
Normal file
60
oxi/view.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
from functools import cached_property
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from .conf import NodeConfig
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from requests import Session
|
||||||
|
|
||||||
|
|
||||||
|
class NodeView:
|
||||||
|
def __init__(self, session: "Session", base_url: str, data: dict):
|
||||||
|
self._session = session
|
||||||
|
self._base_url = base_url
|
||||||
|
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
|
||||||
|
def name(self) -> str:
|
||||||
|
return self._data.get("name")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ip(self) -> str:
|
||||||
|
return self._data.get("ip")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def full_name(self) -> str:
|
||||||
|
return self._data.get("full_name")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def group(self) -> str:
|
||||||
|
return self._data.get("group")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def model(self) -> str:
|
||||||
|
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
|
||||||
|
def config(self) -> NodeConfig:
|
||||||
|
return NodeConfig(self._session, self.full_name, self.model, self._base_url)
|
||||||
@@ -5,12 +5,27 @@ build-backend = "setuptools.build_meta"
|
|||||||
[project]
|
[project]
|
||||||
name = "oxipy"
|
name = "oxipy"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "Oxi API client"
|
description = "Oxidized API client"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.13"
|
license = { file = "LICENSE" }
|
||||||
dependencies = [
|
requires-python = ">=3.11"
|
||||||
"requests>=2.32.5",
|
classifiers = [
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"License :: OSI Approved :: Apache Software License",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
]
|
]
|
||||||
|
dependencies = [
|
||||||
|
"pydantic>=2.12.5",
|
||||||
|
"requests>=2.32.5",
|
||||||
|
"ttp>=0.10.0",
|
||||||
|
]
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
where = ["."]
|
where = ["."]
|
||||||
include = ["oxi*"]
|
include = ["oxi*"]
|
||||||
|
|
||||||
|
[tool.setuptools]
|
||||||
|
include-package-data = true
|
||||||
|
|
||||||
|
[tool.setuptools.package-data]
|
||||||
|
"oxi" = ["**/*.ttp"]
|
||||||
286
uv.lock
generated
Normal file
286
uv.lock
generated
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
version = 1
|
||||||
|
revision = 3
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "annotated-types"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "certifi"
|
||||||
|
version = "2026.1.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "charset-normalizer"
|
||||||
|
version = "3.4.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
|
||||||
|
{ 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 = "idna"
|
||||||
|
version = "3.11"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "oxipy"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { editable = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pydantic" },
|
||||||
|
{ name = "requests" },
|
||||||
|
{ name = "ttp" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "pydantic", specifier = ">=2.12.5" },
|
||||||
|
{ name = "requests", specifier = ">=2.32.5" },
|
||||||
|
{ name = "ttp", specifier = ">=0.10.0" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pydantic"
|
||||||
|
version = "2.12.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "annotated-types" },
|
||||||
|
{ name = "pydantic-core" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
{ name = "typing-inspection" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pydantic-core"
|
||||||
|
version = "2.41.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" },
|
||||||
|
{ 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 = "requests"
|
||||||
|
version = "2.32.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "certifi" },
|
||||||
|
{ name = "charset-normalizer" },
|
||||||
|
{ name = "idna" },
|
||||||
|
{ name = "urllib3" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ttp"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/83/e6/9169d35574be82df2a0cdd2546f4f83d0d30964cf0043fc9784df855b024/ttp-0.10.0.tar.gz", hash = "sha256:40f1ca61ee1431f5b1ab5326fb55f852a04749e9574792d45455b62c5e7ac97b", size = 64665, upload-time = "2025-11-02T08:47:50.329Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/c3/60abb45bd8eb973997f133eb76949523478d35dfc551a0dbd8906b6a8075/ttp-0.10.0-py3-none-any.whl", hash = "sha256:9985e0ca414e85d41493a6291a924624b9a08c48c78d2d01477cc60ba2a347c1", size = 84287, upload-time = "2025-11-02T08:47:48.656Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.15.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-inspection"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "urllib3"
|
||||||
|
version = "2.6.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user