Merge pull request 'dev' (#3) from dev into main
All checks were successful
CI / lint (push) Successful in 29s
CI / test (3.10) (push) Successful in 28s
CI / test (3.11) (push) Successful in 43s
CI / test (3.12) (push) Successful in 41s
CI / test (3.13) (push) Successful in 43s

Reviewed-on: #3
This commit was merged in pull request #3.
This commit is contained in:
2026-06-12 15:59:57 +03:00
60 changed files with 5121 additions and 6 deletions

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

@@ -0,0 +1,40 @@
name: CI
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
jobs:
lint:
runs-on: rassbery
container: catthehacker/ubuntu:act-22.04
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v6
- name: Sync dependencies
run: uv sync --group dev
- name: Ruff lint
run: uv run ruff check --output-format=github .
- name: Ruff format check
run: uv run ruff format --check .
test:
runs-on: rassbery
container: catthehacker/ubuntu:act-22.04
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
python-version: ${{ matrix.python-version }}
- name: Sync dependencies
run: uv sync --group dev
- name: Run tests
run: uv run pytest -q

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

@@ -0,0 +1,38 @@
name: CI
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v6
- name: Sync dependencies
run: uv sync --group dev
- name: Ruff lint
run: uv run ruff check --output-format=github .
- name: Ruff format check
run: uv run ruff format --check .
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
python-version: ${{ matrix.python-version }}
- name: Sync dependencies
run: uv sync --group dev
- name: Run tests
run: uv run pytest -q

16
.gitignore vendored Normal file
View File

@@ -0,0 +1,16 @@
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
main.py
# Virtual environments
.venv
.python-version
.idea
.DS_Store
.vscode
# etc files
*.txt

201
LICENSE Normal file
View 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.

262
README.md
View File

@@ -0,0 +1,262 @@
# oxipy
`oxipy` is a Python client for the [Oxidized](https://github.com/ytti/oxidized) API.
It fetches device configurations from Oxidized and parses them into structured
Pydantic models using bundled TTP templates.
Oxidized remains responsible for collecting and storing configuration backups.
`oxipy` focuses on consuming those backups from Python code and exposing common
configuration sections such as system data, interfaces, and VLANs.
## Contents
- [Installation](#installation)
- [Quick Start](#quick-start)
- [API Reference](#api-reference)
- [OxiAPI](#oxiapi)
- [NodeView](#nodeview)
- [NodeConfig](#nodeconfig)
- [ModelView](#modelview)
- [Supported Devices](#supported-devices)
- [Additional Documentation](#additional-documentation)
## Installation
The package is distributed from the source repository. It is not published to
PyPI yet.
**Requirements:** Python 3.10+
### From GitHub Source
Install directly from the repository:
```bash
pip install git+https://github.com/sttarsky/oxipy.git
```
Install a specific tag or branch:
```bash
pip install git+https://github.com/sttarsky/oxipy.git@v0.1.0
pip install git+https://github.com/sttarsky/oxipy.git@dev
```
For local development:
```bash
git clone https://github.com/sttarsky/oxipy
cd oxipy
pip install -e .
```
## Quick Start
```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)
print(node.config.system.model)
print(node.config.interfaces.dump_json())
print(node.config.vlans.dump_json())
```
Example output:
```text
192.168.1.1
keenetic
router/HQ
Sprinter (KN-3710)
[
{"interface": "Bridge1", "ip_address": "192.168.1.1", "mask": 24, "description": "Guest network"},
{"interface": "Bridge0", "ip_address": "172.16.1.1", "mask": 24, "description": "Home network"}
]
[
{"vlan_id": 1, "description": "Home VLAN"},
{"vlan_id": 2, "description": "Ethernet uplink"},
{"vlan_id": 3, "description": "Home network"}
]
```
## API Reference
### OxiAPI
`OxiAPI` is the entry point. It manages the HTTP session and provides access to
Oxidized nodes.
```python
OxiAPI(
url: str,
username: str | None = None,
password: str | None = None,
verify: bool = True,
)
```
| Parameter | Type | Description |
| --- | --- | --- |
| `url` | `str` | Base URL of the Oxidized API, for example `https://oxi.example.com`. |
| `username` | `str | None` | Optional username for HTTP basic authentication. |
| `password` | `str | None` | Optional password for HTTP basic authentication. |
| `verify` | `bool` | Whether to verify TLS certificates. Defaults to `True`. |
Example:
```python
# Without authentication
api = OxiAPI(url="https://oxi.example.com")
# With HTTP basic authentication
api = OxiAPI(
url="https://oxi.example.com",
username="admin",
password="secret",
)
# As a context manager. The HTTP session is closed automatically.
with OxiAPI(url="https://oxi.example.com") as api:
node = api.node("HQ")
print(node.ip)
```
#### `api.node(name)`
Returns a `NodeView` for the requested Oxidized node.
```python
node = api.node("HQ")
```
### NodeView
`NodeView` represents one network device. It contains metadata returned by
Oxidized and lazy access to the fetched configuration.
| Property | Type | Description |
| --- | --- | --- |
| `ip` | `str` | Node IP address. |
| `full_name` | `str` | Full node name in Oxidized. |
| `group` | `str` | Oxidized group the node belongs to. |
| `model` | `str` | Device model key used to select a parser. |
| `config` | `NodeConfig` | Device configuration, fetched and parsed on first access. |
Example:
```python
node = api.node("HQ")
print(node.ip)
print(node.group)
print(node.model)
```
### NodeConfig
`NodeConfig` fetches and parses a device configuration. The parser is selected
from the device registry by the node `model` value returned by Oxidized.
Configuration sections are exposed through properties that return `ModelView`
objects.
| Property | Returns | Description |
| --- | --- | --- |
| `system` | `ModelView[System]` | System information. |
| `interfaces` | `ModelView[list[Interfaces]]` | Parsed interface list. |
| `vlans` | `ModelView[list[Vlans]]` | Parsed VLAN list, if the template provides VLAN data. |
| `text` | `str` | Raw configuration text fetched from Oxidized. |
Example:
```python
cfg = node.config
print(cfg.system.model)
print(cfg.system.serial_number)
print(cfg.system.version)
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))
print(cfg.interfaces.dump_json())
print(cfg.vlans.dump_json())
print(cfg.system.dump_json())
print(cfg.text)
```
`NodeConfig` also provides `dump()` and `dump_json()` methods for the whole
parsed device object.
### ModelView
`ModelView` wraps either a single Pydantic model or a list of Pydantic models.
It provides serialization, iteration for list sections, and transparent access
to model attributes.
| Method / operation | Applies to | Description |
| --- | --- | --- |
| `.dump()` | single model and list | Returns a Python `dict` or `list` using aliases. |
| `.dump_json()` | single model and list | Returns a JSON string using aliases. |
| `.<attr>` | single model and list | Proxies attribute access to the wrapped model. |
| `iter(view)` | list only | Iterates over wrapped models. |
| `len(view)` | list only | Returns the number of wrapped models. |
| `view[i]` | list only | Returns an item or slice. |
`__iter__`, `__len__`, and `__getitem__` are available only for list-backed
sections such as `interfaces` and `vlans`. Calling them on `system` raises
`TypeError`.
Examples:
```python
system = node.config.system
print(system.dump_json())
print(system.model)
print(system.serial_number)
interfaces = node.config.interfaces
for iface in interfaces:
print(iface.name, iface.ip_address)
print(len(interfaces))
print(interfaces[0])
print(interfaces[:3])
print(interfaces.dump())
```
## Supported Devices
Registry keys are compared with the Oxidized node `model` value
case-insensitively.
| Device | Registry keys |
| --- | --- |
| Keenetic | `ndms`, `keenetic`, `keeneticos` |
| MikroTik | `routeros`, `ros`, `mikrotik` |
| Qtech | `qtech` |
| Huawei | `huawei`, `vrp` |
| Eltex | `eltex` |
| H3C | `h3c` |
| Quasar | `qos`, `quasar` |
You can add support for another device family by creating a new device model
and TTP template. See [Extending Device Models](docs/extending-models.md).
## Additional Documentation
- [Writing TTP Templates](docs/templates.md)
- [Extending Device Models](docs/extending-models.md)

326
docs/extending-models.md Normal file
View File

@@ -0,0 +1,326 @@
# Extending Device Models
`oxipy` parses an Oxidized configuration in two stages. A TTP template first
extracts raw dictionaries from the text, then a device model normalizes those
dictionaries before Pydantic validates them against the public contract.
Device models extend `BaseDevice`. Override `system()`, `interfaces()`, or
`vlans()` when the raw TTP result needs vendor-specific cleanup.
## Contents
- [Data Flow](#data-flow)
- [Registering a Device](#registering-a-device)
- [Method Overrides](#method-overrides)
- [interfaces()](#interfaces)
- [vlans()](#vlans)
- [system()](#system)
- [Complete Example](#complete-example)
- [Expected Contract](#expected-contract)
## Data Flow
```text
configuration text
|
v
TTP template (.ttp)
|
v
self.raw: dict
|
+--> system() -> dict
+--> interfaces() -> list[dict]
+--> vlans() -> list[dict]
|
v
Pydantic validation
|
v
Device(system, interfaces, vlans)
```
The extension methods are intentionally small. The base implementation returns
data directly from `self.raw`:
```python
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)
```
## Registering a Device
To add support for a new vendor:
1. Create a Python file in `oxi/interfaces/models/`, for example `cisco.py`.
2. Create a template in `oxi/interfaces/models/templates/`, for example
`cisco.ttp`.
3. Subclass `BaseDevice` and register it with `@register_parser`.
```python
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` accepts a string or a list of strings. These values are the
registry keys used to match the Oxidized node `model` field. Matching is
case-insensitive.
Model modules are imported automatically through `pkgutil` when
`oxi.interfaces` is loaded, so you do not need to import your model class
manually.
## Method Overrides
### interfaces()
Override `interfaces()` when you need to:
- Convert dotted decimal netmasks to prefix lengths.
- Decode escaped descriptions.
- Rename keys that do not match the contract.
- Filter service-only interfaces.
Example: convert a netmask to a 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
```
Example: filter management interfaces.
```python
def interfaces(self) -> list[dict]:
return [
item for item in self.raw.get("interfaces", [])
if not item.get("interface", "").startswith("Mgmt")
]
```
Example: decode escaped UTF-8 descriptions.
```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()
Override `vlans()` to normalize VLAN IDs, expand compressed ranges, decode
names, or merge details from multiple template groups.
Example: add a generated VLAN name.
```python
def vlans(self) -> list[dict]:
result = []
for item in self.raw.get("vlans", []):
item["description"] = f"VLAN_{item.get('vlan_id', '?')}"
result.append(item)
return result
```
Example: merge data from another raw group.
```python
def vlans(self) -> list[dict]:
vlans = {item["vlan_id"]: item for item in self.raw.get("vlans", [])}
for extra in self.raw.get("vlan_details", []):
vlan_id = extra.get("vlan_id")
if vlan_id in vlans:
vlans[vlan_id].update(extra)
return list(vlans.values())
```
Example: expand a comma-separated VLAN range.
```python
def _expand_vlan_range(value: str) -> list[str]:
result = []
for part in value.split(","):
if "-" not in part:
result.append(part.strip())
continue
start, end = (int(item) for item in part.split("-", 1))
result.extend(str(vlan_id) for vlan_id in range(start, end + 1))
return result
```
### system()
Override `system()` when the system section needs computed fields or data from
another raw group.
Example: assemble a serial number from two fields.
```python
def system(self) -> dict:
raw_system = self.raw.get("system", {})
part1 = raw_system.get("serial_part1", "")
part2 = raw_system.get("serial_part2", "")
raw_system["serial_number"] = f"{part1}-{part2}"
return raw_system
```
Example: normalize a version string.
```python
def system(self) -> dict:
raw_system = self.raw.get("system", {})
version = raw_system.get("version", "")
raw_system["version"] = version.split()[0] if version else version
return raw_system
```
## Complete Example
Assume a Cisco IOS-like device where:
- IP address and netmask are separated by a space.
- Interface descriptions can contain several words.
- System fields are present in separate lines.
Template: `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 {{ vlan_id | _start_ }}
name {{ name | ORPHRASE }}
</group>
```
Device model: `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", {})
if raw_system.get("model"):
raw_system["model"] = raw_system["model"].strip()
return raw_system
```
## Expected Contract
Methods must return structures accepted by `oxi.interfaces.contract`.
### `system() -> dict`
```python
{
"model": "RB951Ui-2nD",
"serial_number": "B88C0B31117B",
"version": "7.12.1",
}
```
### `interfaces() -> list[dict]`
```python
[
{
"interface": "ether1",
"ip_address": "192.168.1.1",
"mask": 24,
"description": "LAN",
},
]
```
### `vlans() -> list[dict]`
```python
[
{
"vlan_id": 10,
"description": "MGMT",
},
]
```
The Pydantic models use `populate_by_name=True` for aliased models, so both
field names and aliases are accepted where aliases exist.

299
docs/templates.md Normal file
View File

@@ -0,0 +1,299 @@
# Writing TTP Templates
`oxipy` uses [TTP (Template Text Parser)](https://ttp.readthedocs.io/) to turn
network device configurations fetched from Oxidized into structured data.
Templates are stored in `oxi/interfaces/models/templates/`.
## Contents
- [Template Structure](#template-structure)
- [Required Groups](#required-groups)
- [The system Group](#the-system-group)
- [The interfaces Group](#the-interfaces-group)
- [The vlans Group](#the-vlans-group)
- [Useful TTP Features](#useful-ttp-features)
- [Default Variables](#default-variables)
- [Full Example](#full-example)
- [Validation](#validation)
## Template Structure
Each template is a `.ttp` file with a small set of conventional blocks:
```xml
<doc>
Optional template documentation.
</doc>
<vars>
<!-- Default values for groups. -->
</vars>
<group name="system">
<!-- Rules for system information. -->
</group>
<group name="interfaces">
<!-- Rules for interfaces. -->
</group>
<group name="vlans">
<!-- Optional rules for VLANs. -->
</group>
```
Use `oxi/interfaces/models/templates/_template.ttp` as the starting point for a
new parser.
## Required Groups
The framework requires two groups in every template:
| Group | Required | Description |
| --- | --- | --- |
| `system` | Yes | Device system information. |
| `interfaces` | Yes | Interface configuration. |
| `vlans` | No | VLAN configuration. |
If a required group is missing from the template or from the TTP result,
`BaseDevice` raises `ValueError`.
If a template declares an optional `vlans` group, `oxipy` expects TTP to return
that group. Omit the group completely for devices where VLAN parsing is not
implemented.
## The system Group
The `system` group must return one dictionary with these fields:
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `model` | `str` | Yes | Device model. |
| `serial_number` | `str` | Yes | Device serial number. |
| `version` | `str` | Yes | Firmware, software, or build version chosen by the parser. |
Example for MikroTik:
```text
# version: 7.12.1 (stable)
# model = RB951Ui-2nD
# serial number = B88C0B31117B
```
```xml
<group name="system">
# version: {{ version }}{{ ignore('.*') }}
# model = {{ model }}
# serial number = {{ serial_number }}
</group>
```
Example for Keenetic:
```text
! release: 4.1.7.1-1
! model: Keenetic Extra
! hw_version: F02B4E7A1C90
```
```xml
<group name="system">
! release: {{ version }}
! model: {{ model | ORPHRASE }}
! hw_version: {{ serial_number }}
</group>
```
## The interfaces Group
The `interfaces` group must return a list of dictionaries. Each dictionary
describes one interface.
The `Interfaces` contract expects these fields:
| Contract field | TTP name / alias | Type | Required |
| --- | --- | --- | --- |
| `name` | `interface` | `str` | Yes |
| `ip_address` | `ip_address` | `IPv4Address | None` | No |
| `mask` | `mask` | `int | None` | No |
| `description` | `description` | `str | None` | No |
The Pydantic field `name` has the alias `interface`, so templates should usually
emit `interface`. You can also emit `name` because the models allow population
by field name, or you can normalize keys in the device class by overriding
`interfaces()`.
Example for MikroTik:
```text
/ip address
add address=192.168.1.1/24 interface=ether1 network=192.168.1.0
add address=10.0.0.1/30 comment="WAN link" interface=ether2 network=10.0.0.0
```
```xml
<group name="interfaces">
/ip address
add address={{ ip_address | _start_ }}/{{ mask }} interface={{ interface }} network={{ network }}
add address={{ ip_address | _start_ }}/{{ mask }} comment={{ description | ORPHRASE | strip('"') }} interface={{ interface }} network={{ network }}
</group>
```
Example for CLI-style devices:
```text
interface Vlanif120
description SSH
ip address 10.26.196.254 255.255.255.0
```
```xml
<group name="interfaces">
interface {{ interface | _start_ }}
description {{ description | ORPHRASE }}
ip address {{ ip_address }} {{ mask | to_cidr }}
</group>
```
Use TTP's `to_cidr` formatter when the device uses dotted decimal masks.
## The vlans Group
The `vlans` group is optional. If it is declared, it must return a list of VLAN
dictionaries.
The `Vlans` contract expects these fields:
| Contract field | Alias | Type | Required |
| --- | --- | --- | --- |
| `vlan_id` | none | `int` | Yes |
| `name` | `description` | `str | None` | No |
`name` has the alias `description`, so either key is accepted. Existing parsers
use both forms depending on the vendor format.
Example:
```text
vlan 10
name MGMT
```
```xml
<group name="vlans">
vlan {{ vlan_id | _start_ }}
name {{ name | ORPHRASE }}
</group>
```
For compressed vendor syntax such as `vlan batch 101 to 103 110`, parse the raw
range in the template and normalize it in the device class when needed.
## Useful TTP Features
### Line markers
| Marker | Description |
| --- | --- |
| `_start_` | Starts a new group match from the current line. |
| `_end_` | Ends the current group match. |
```xml
interface {{ interface | _start_ }}
```
### Variable modifiers
| Modifier | Description |
| --- | --- |
| `ORPHRASE` | Captures a word or phrase to the end of the line. |
| `exclude("pattern")` | Skips the match when the captured value contains the pattern. |
| `strip('"')` | Removes a character from both ends of the captured value. |
| `replace("old","new")` | Replaces text inside the captured value. |
| `re("pattern")` | Accepts the value only if it matches the regex. |
| `ignore` | Captures and discards the value. |
| `ignore('.*')` | Discards the rest of the line. |
| `to_cidr` | Converts a dotted decimal netmask to a prefix length. |
| `unrange("-", ",")` | Expands ranges such as `10-12` using a comma separator. |
| `split(",")` | Splits a captured string into a list. |
### Template comments
Lines beginning with `##` are TTP comments:
```xml
## disabled no comment
add address={{ ip_address | _start_ }}/{{ mask }} interface={{ interface }}
```
## Default Variables
The `<vars>` block can define default values for a group through the group's
`default` attribute:
```xml
<vars>
default_system = {
"model": "",
"serial_number": "",
"version": ""
}
</vars>
<group name="system" default="default_system">
# version: {{ version }}
# model = {{ model }}
# serial number = {{ serial_number }}
</group>
```
If the group does not match anything, TTP returns the default dictionary.
## Full Example
This simplified Cisco IOS-style example shows the expected shape of a complete
template:
```xml
<doc>
Cisco IOS running-config parser.
</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 }} {{ mask | to_cidr }}
</group>
<group name="vlans">
vlan {{ vlan_id | _start_ }}
name {{ name | ORPHRASE }}
</group>
```
## Validation
`BaseDevice` performs two validation passes:
1. Template structure validation checks that the template declares the required
`system` and `interfaces` groups.
2. Parse result validation checks that TTP actually returned the required groups
for the given configuration.
After that, parsed data is validated by Pydantic models from
`oxi.interfaces.contract`. Invalid structures raise the original Pydantic
validation error.

5
oxi/__init__.py Normal file
View File

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

20
oxi/adapter.py Normal file
View File

@@ -0,0 +1,20 @@
from requests.adapters import HTTPAdapter
from urllib3.util import Retry
class OxiAdapter(HTTPAdapter):
def __init__(
self,
timeout: int | None = None,
max_retries: int = 3,
*args,
**kwargs,
):
self.timeout = timeout
retry = Retry(total=max_retries, backoff_factor=0.3)
super().__init__(*args, max_retries=retry, **kwargs)
def send(self, request, **kwargs):
if kwargs.get("timeout") is None:
kwargs["timeout"] = self.timeout
return super().send(request, **kwargs)

95
oxi/conf.py Normal file
View File

@@ -0,0 +1,95 @@
import json
from collections.abc import Iterator
from functools import cached_property
from typing import TYPE_CHECKING, Generic, 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, name=self._full_name).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
View File

@@ -0,0 +1,51 @@
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: str | None = None,
password: str | None = 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: str | None = None,
password: str | None = None,
verify: bool = True,
) -> Session:
session = Session()
adapter = OxiAdapter(timeout=10, max_retries=3)
session.mount("https://", adapter)
session.mount("http://", adapter)
session.verify = verify
if username and password:
session.auth = (username, password)
return session
def __enter__(self):
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

65
oxi/exception.py Normal file
View File

@@ -0,0 +1,65 @@
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from requests import HTTPError
_STATUS_MESSAGES: dict[int, str] = {
401: "Unauthorized",
403: "Forbidden",
500: "Internal Server Error",
502: "Bad Gateway",
503: "Service Unavailable",
504: "Gateway Timeout",
}
def _looks_like_node_not_found_html(e: "HTTPError") -> bool:
resp = getattr(e, "response", None)
if resp is None:
return False
try:
content_type = (resp.headers or {}).get("Content-Type", "")
except Exception:
content_type = ""
if "text/html" not in (content_type or "").lower():
return False
try:
body = (resp.text or "")[:20_000]
except Exception:
return False
return (
"Oxidized::NodeNotFound" in body
or "NodeNotFound" in body
or "<title>Oxidized::NodeNotFound" in body
)
class OxiAPIError(Exception):
def __init__(self, message: str, status_code: int | None = None):
super().__init__(message)
self.status_code = status_code
self.message = message
def __str__(self):
if self.status_code is not None:
return f"OxiAPIError: {self.args[0]} (HTTP {self.status_code})"
return f"OxiAPIError: {self.args[0]}"
@classmethod
def from_http_error(cls, e: "HTTPError", context: str = "") -> "OxiAPIError":
resp = getattr(e, "response", None)
status = resp.status_code if resp is not None else None
if status == 500 and _looks_like_node_not_found_html(e):
status = 404
if status == 404:
message = f"{context} not found" if context else "Not found"
else:
base = (
(_STATUS_MESSAGES.get(status) if status is not None else None)
or (resp.reason if resp is not None else None)
or (f"HTTP {status}" if status is not None else "Request failed")
)
message = f"{context}: {base}" if context else base
return cls(message, status)

View File

@@ -0,0 +1,26 @@
from collections.abc import Callable
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"]

153
oxi/interfaces/base.py Normal file
View File

@@ -0,0 +1,153 @@
import xml.etree.ElementTree as ET
from abc import ABC, abstractmethod
from pathlib import Path
from ttp import ttp
from oxi.exception import OxiAPIError
from oxi.interfaces.contract import Device, Interfaces, System, Vlans
class BaseDevice(ABC):
_REQUIRED_SECTIONS: frozenset[str] = frozenset({"system", "interfaces"})
_OPTIONAL_SECTIONS: frozenset[str] = frozenset({"vlans"})
def __init__(self, config: str, name: str | None = None):
self.config: str = config
self.name = name
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:
"""
Name of the TTP template file used by this device parser.
"""
def vlans(self) -> list[dict]:
"""
Parse VLAN configuration from self.raw['vlans'].
Expected structure:
[{"vlan_id": 10, "description": "MGMT"}, {"vlan_id": 15, "name": "SSH"}, ...]
Returns:
list[Vlans]: VLANs from the vlans section, or an empty list
when the section is absent.
Raises:
ValueError: if raw data cannot be validated by the contract.
""" # noqa: E501
return self.raw.get("vlans", [])
def interfaces(self) -> list[dict]:
"""
Parse Interface configuration from self.raw['interfaces'].
Expected raw structure:
[{"interface": "GEthernet1/0/1", "ip_address": "192.168.1.1", "mask": "24", "description": "IPBB interface"}]
Raises:
ValueError: if raw data cannot be validated by the contract.
""" # noqa: E501
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: if raw data cannot be validated by the contract.
"""
return self.raw.get("system", None)
@staticmethod
def _as_list(data) -> list:
"""Normalize a TTP group result to a list.
TTP returns a single dict when a group matches exactly one entry and a
list when it matches several. Callers always expect a list.
"""
if data is None:
return []
if isinstance(data, dict):
return [data]
return data
def _validate_contract(self) -> dict:
if self.raw is None:
msg = f"Node {self.name} not found" if self.name else "Node not found"
raise OxiAPIError(msg, status_code=404)
system_data = self.system()
interfaces_data = self._as_list(self.interfaces())
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}' "
f"declares optional group 'vlans', but TTP did not return it."
)
vlans_data = self._as_list(self.vlans())
result["vlans"] = [Vlans(**item) for item in vlans_data]
return result
def _load_template(self):
"""Load the device TTP template from models/templates."""
path = Path(__file__).parent / "models" / "templates" / self.template
if not path.exists():
raise FileNotFoundError(f"Template {self.template} not found")
return path.read_text(encoding="utf-8")
def _validate_template_groups(self) -> None:
"""Validate that the template declares all required groups."""
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:
"""Run the node-not-found check and then parse the config with TTP."""
pattern = """node not {{found}}"""
parser = ttp(data=self.config, template=pattern)
parser.parse()
res = parser.result()
if res[0][0]:
# raise OxiAPIError(f"Node {self.name} not found", status_code=404)
return None
p = ttp(data=self.config, template=self._loaded_template)
p.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())

View File

@@ -0,0 +1,43 @@
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] = []

View File

@@ -0,0 +1,7 @@
import importlib
import pkgutil
package = __package__
for _, module_name, _ in pkgutil.iter_modules(__path__):
importlib.import_module(f"{package}.{module_name}")

View File

@@ -0,0 +1,38 @@
from oxi.interfaces import register_parser
from oxi.interfaces.base import BaseDevice
from oxi.interfaces.utils import expand_vlan_range
@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: list[dict] = []
named_vlan: set[str] = set()
for item in vlans_ttp:
vlan_id = item.get("vlan_id")
if vlan_id:
named_vlan.add(str(vlan_id))
vlans.append(item)
continue
ids = item.get("vlan_ids", "")
tail = item.get("vlan_tail")
if tail:
ids = [*ids, tail] if isinstance(ids, list) else f"{ids},{tail}"
for vid in expand_vlan_range(ids):
if vid in named_vlan:
continue
vlans.append({"vlan_id": vid})
return vlans

View File

@@ -0,0 +1,17 @@
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.get("vlans", [])
vlans: list[dict] = []
for item in vlan_list:
vlan_ids = item.get("vlans_id")
if not vlan_ids:
vlans.append(item)
continue
vlans.extend({"vlan_id": vlan_id} for vlan_id in vlan_ids)
return vlans

View File

@@ -0,0 +1,11 @@
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) -> list[dict]:
vlan_ids = self.raw.get("vlans", {}).get("vlan_ids", [])
return [{"vlan_id": vlan} for vlan in vlan_ids]

View File

@@ -0,0 +1,32 @@
from ipaddress import ip_interface
from oxi.interfaces import register_parser
from oxi.interfaces.base import BaseDevice
from oxi.interfaces.utils import decode_utf
@register_parser(["NDMS", "keenetic", "KeeneticOS"])
class Keenetic(BaseDevice):
template = "keenetic.ttp"
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 = 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 = decode_utf(item.get("description", ""))
item["description"] = decoded
return vlans

View File

@@ -0,0 +1,7 @@
from oxi.interfaces import register_parser
from oxi.interfaces.base import BaseDevice
@register_parser(["routeros", "ros", "mikrotik"])
class Mikrotik(BaseDevice):
template = "mikrotik.ttp"

View File

@@ -0,0 +1,29 @@
from oxi.interfaces import register_parser
from oxi.interfaces.base import BaseDevice
from oxi.interfaces.utils import expand_vlan_range
@register_parser(["QTECH"])
class Qtech(BaseDevice):
template = "qtech.ttp"
def vlans(self) -> list[dict]:
vlans_ttp = self.raw.get("vlans", [])
vlans: list[dict] = []
named_vlan: set[str] = set()
for item in vlans_ttp:
vlan_id = item.get("vlan_id")
if vlan_id and "," not in vlan_id and "-" not in vlan_id:
named_vlan.add(vlan_id)
vlans.append(item)
continue
ids = item.get("vlan_ids") or vlan_id or ""
tail = item.get("vlan_tail")
if tail:
ids = [*ids, tail] if isinstance(ids, list) else f"{ids},{tail}"
for vid in expand_vlan_range(ids):
if vid in named_vlan:
continue
vlans.append({"vlan_id": vid})
return vlans

View File

@@ -0,0 +1,23 @@
from oxi.interfaces import BaseDevice, register_parser
@register_parser(["quasar", "qos"])
class Quasar(BaseDevice):
template = "quasar.ttp"
def interfaces(self) -> list[dict]:
ether_interface: dict = self.raw.get("interfaces", {})
interfaces: list[dict] = []
bulk_interfaces: dict = self.raw.get("bulkinterfaces", {})
for key, value in bulk_interfaces.items():
interfaces.append(
{
"interface": key,
"description": value.get("description"),
"ip_address": value.get("ip_address"),
"mask": value.get("mask"),
}
)
if ether_interface:
interfaces.append(ether_interface)
return interfaces

View File

@@ -0,0 +1,37 @@
<doc>
Base template for a new device parser. Copy this file, rename it to
&lt;vendor&gt;.ttp, and fill the groups for the target configuration format.
Required groups: system, interfaces.
Optional group: vlans. Add it only when VLAN parsing is implemented.
system must return one dictionary with: model, serial_number, version.
interfaces must return a list of dictionaries with: interface, ip_address,
mask, description. Use a prefix length for mask; convert dotted decimal masks
with `to_cidr` or in the device class.
vlans must return dictionaries with vlan_id and optional name/description.
Useful TTP modifiers: ORPHRASE, _start_, strip(), replace(), exclude(),
ignore, ignore('.*'), to_cidr, unrange(), split().
See docs/templates.md for details.
</doc>
<vars>
default_system = {
"model": "",
"serial_number": "",
"version": ""
}
</vars>
<group name="system" default="default_system">
...
</group>
<group name="interfaces">
...
</group>
<group name="vlans">
...
</group>

View File

@@ -0,0 +1,39 @@
<doc>
Eltex configuration parser.
The system group reads software version data and the serial group extracts
serial numbers from the unit table. The interfaces group parses interface IP
settings. The vlans group supports named VLAN interfaces and compressed VLAN
lists.
</doc>
<vars>
default_system = {
"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 }}
name {{ description | ORPHRASE}}
description {{ description | ORPHRASE }}
</group>
<group name="vlans">
interface vlan {{ vlan_id | _start_ }}
name {{ name | ORPHRASE }}
vlan {{ _db_ | _start_ }}
vlan {{ vlan_ids | joinmatches(',') | unrange("-", ",") | split(",")}}
</group>

View File

@@ -0,0 +1,35 @@
<doc>
H3C configuration parser.
The system group reads boot image version and board model data. The interfaces
group parses interface IP settings. The vlans groups parse both named VLANs and
range-style VLAN declarations.
</doc>
<vars>
default_system = {
"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 }}
description {{ description | ORPHRASE }}
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>

View File

@@ -0,0 +1,28 @@
<doc>
Huawei VRP configuration parser.
The system group reads VRP version and slot ESN data. The interfaces group
parses interface blocks and converts dotted decimal masks to prefix lengths.
The vlans group parses `vlan batch` declarations and emits VLAN IDs.
</doc>
<vars>
default_system = {
"model": "",
"serial_number": "",
"version": ""
}
</vars>
<group name="system" default="default_system">
# VRP (R) software, Version {{ version }} ({{ model }} {{ _line_ }}
# ESN of slot {{ slot_number }}: {{ serial_number }}
</group>
<group name="interfaces">
interface {{ interface }}
description {{ description | ORPHRASE }}
ip address {{ ip_address }} {{ mask | to_cidr }}
</group>
<group name="vlans">
vlan batch {{ vlan_ids | ORPHRASE | unrange(" to ", " ") | split(" ")}}
</group>

View 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>

View 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 disabled={{ disabled | _start_ | replace("yes","True") | strip('"') }} interface={{ interface }} name={{ name | ORPHRASE | strip('"') }} vlan-id={{ vlan_id }}
</group>

View File

@@ -0,0 +1,41 @@
<doc>
Qtech switch configuration parser.
The system group reads the model, serial number, and build number. For Qtech,
system.version intentionally stores the build number from lines like
`Version 2.2.0C Build 96279`.
The interfaces group parses CLI interface blocks and converts dotted decimal
masks to prefix lengths. The vlans group supports named VLANs, comma-separated
VLAN lists, ranges, and continuation lines.
</doc>
<vars>
default_system = {
"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>

View File

@@ -0,0 +1,35 @@
<doc>
Quasar configuration parser.
The system group supports Assembly-based and Engine-based firmware blocks. The
interfaces group parses the management Ethernet address, while bulkinterfaces
collects per-port descriptions that the Python model merges into interface
records.
</doc>
<vars>
default_system = {
"model": "",
"serial_number": "",
"version": ""
}
</vars>
<group name="system" default="default_system">
# Component Version {{ _start_ }}
# Assembly {{ version }}
# Product Name {{ model | ORPHRASE }}
# Product Serial Number {{ serial_number }}
# Subsystem Version {{ _start_ }}
# Engine {{ version }}
# Product Name {{ model | ORPHRASE }}
# Product Serial Number {{ serial_number }}
</group>
<group name="interfaces">
{{ ignore("\\s*") }}config {{ interface }} ipv4 address {{ ip_address }}/{{ mask }} gateway {{ gateway }} {{ ignore }}
</group>
<group name="bulkinterfaces.{{ interface }}">
{{ ignore("\\s*") }}config interface {{ interface | _start_ }} description {{ description | ORPHRASE | strip('"')}}
</group>

39
oxi/interfaces/utils.py Normal file
View File

@@ -0,0 +1,39 @@
def expand_vlan_range(value: str | list[str]) -> list[str]:
"""Expand values like '1,7,14-15' into individual VLAN IDs."""
if isinstance(value, list):
value = ",".join(str(item) for item in value)
result: list[str] = []
if not value:
return result
for part in value.split(","):
part = part.strip()
if not part:
continue
if "-" in part:
start_s, end_s = part.split("-", 1)
try:
start, end = int(start_s), int(end_s)
except ValueError:
result.append(part)
continue
if start > end:
start, end = end, start
result.extend(str(i) for i in range(start, end + 1))
else:
result.append(part)
return result
def decode_utf(text: str):
"""Decode escaped UTF-8 descriptions."""
if "\\x" in text:
desc = text.strip('"')
decoded = (
desc.encode("utf-8")
.decode("unicode_escape")
.encode("latin1")
.decode("utf-8")
)
return decoded
return text

29
oxi/node.py Normal file
View File

@@ -0,0 +1,29 @@
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()
)

59
oxi/view.py Normal file
View File

@@ -0,0 +1,59 @@
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)

View File

@@ -1,16 +1,58 @@
[build-system]
requires = ["setuptools>=61"]
requires = ["setuptools>=77"]
build-backend = "setuptools.build_meta"
[project]
name = "oxipy"
version = "0.1.0"
description = "Oxi API client"
description = "Python client for Oxidized API with TTP-based config parsing"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"requests>=2.32.5",
license = "Apache-2.0"
license-files = ["LICENSE"]
requires-python = ">=3.10"
classifiers = [
"Programming Language :: Python :: 3",
"Operating System :: OS Independent",
]
dependencies = [
"pydantic>=2.12.5",
"requests>=2.32.5",
"ttp>=0.10.0",
]
[project.optional-dependencies]
dev = [
"pytest>=9.0.3",
"responses>=0.26.1",
]
[project.urls]
Repository = "https://github.com/sttarsky/oxipy"
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["."]
[tool.setuptools.packages.find]
where = ["."]
include = ["oxi*"]
include = ["oxi*"]
[tool.setuptools]
include-package-data = true
[tool.setuptools.package-data]
"oxi" = ["**/*.ttp"]
[dependency-groups]
dev = [
"pytest>=9.0.3",
"responses>=0.26.1",
"ruff>=0.15.17",
]
[tool.ruff]
target-version = "py310"
line-length = 88
[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B"]

8
tests/conftest.py Normal file
View File

@@ -0,0 +1,8 @@
from pathlib import Path
FIXTURES = Path(__file__).parent / "fixtures"
def load(device: str, name: str = "config.conf") -> str:
"""Read a device config fixture from tests/fixtures/<device>/<name>."""
return (FIXTURES / device / name).read_text(encoding="utf-8")

55
tests/fixtures/eltex/config.conf vendored Normal file
View File

@@ -0,0 +1,55 @@
!
Active-image: flash://system/images/mes3300-669-3R3.ros
! Version: 6.6.9.3
! Commit: 3a5c2e39
! Build: 3 (master)
! MD5 Digest: 7bc289cc18be560954bd5cb0afd9b2d5
! Date: 22-Sep-2025
! Time: 12:38:20
! Inactive-image: flash://system/images/_image1.bin
! Version: 6.6.2
! Commit: 3ebc7503
! Build: 5 (master)
! MD5 Digest: a3f15a788c97c71e07e90d84c0ff3b12
! Date: 20-Nov-2023
! Time: 16:39:20
!
! Unit MAC address Hardware version Serial number
! ---- ----------------- ---------------- -------------
! 1 90:54:b7:6b:9d:40 01.01.01 ESG7007778
! 2 90:54:b7:6b:bb:80 01.01.01 ESG7007777
!
!
!
interface TenGigabitEthernet1/0/2
shutdown
description FREE
storm-control broadcast pps 3000
storm-control multicast pps 3000
exit
!
interface TenGigabitEthernet1/0/11
shutdown
description FREE
storm-control broadcast pps 3000
storm-control multicast pps 3000
exit
!
interface vlan 1700
name sw-test_HW
ip address 13.36.8.1 255.255.255.0
exit
!
vlan database
vlan 114-115,120,130,414,610,999-1000,1701-1703,1705,1801,2001,2011
vlan 2021-2022,3157-3158,3333-3334
exit
!
interface vlan 666
name test
exit
!
interface vlan 777
name test2
exit
!

View File

@@ -0,0 +1,137 @@
{
"system": {
"model": "",
"serial_number": "ESG7007778",
"version": "6.6.9.3"
},
"interfaces": [
{
"interface": "TenGigabitEthernet1/0/2",
"ip_address": null,
"mask": null,
"description": "FREE"
},
{
"interface": "TenGigabitEthernet1/0/11",
"ip_address": null,
"mask": null,
"description": "FREE"
},
{
"interface": "vlan 1700",
"ip_address": "13.36.8.1",
"mask": 24,
"description": "sw-test_HW"
},
{
"interface": "vlan 666",
"ip_address": null,
"mask": null,
"description": "test"
},
{
"interface": "vlan 777",
"ip_address": null,
"mask": null,
"description": "test2"
}
],
"vlans": [
{
"vlan_id": 1700,
"description": "sw-test_HW"
},
{
"vlan_id": 114,
"description": null
},
{
"vlan_id": 115,
"description": null
},
{
"vlan_id": 120,
"description": null
},
{
"vlan_id": 130,
"description": null
},
{
"vlan_id": 414,
"description": null
},
{
"vlan_id": 610,
"description": null
},
{
"vlan_id": 999,
"description": null
},
{
"vlan_id": 1000,
"description": null
},
{
"vlan_id": 1701,
"description": null
},
{
"vlan_id": 1702,
"description": null
},
{
"vlan_id": 1703,
"description": null
},
{
"vlan_id": 1705,
"description": null
},
{
"vlan_id": 1801,
"description": null
},
{
"vlan_id": 2001,
"description": null
},
{
"vlan_id": 2011,
"description": null
},
{
"vlan_id": 2021,
"description": null
},
{
"vlan_id": 2022,
"description": null
},
{
"vlan_id": 3157,
"description": null
},
{
"vlan_id": 3158,
"description": null
},
{
"vlan_id": 3333,
"description": null
},
{
"vlan_id": 3334,
"description": null
},
{
"vlan_id": 666,
"description": "test"
},
{
"vlan_id": 777,
"description": "test2"
}
]
}

1
tests/fixtures/eltex/not_found.conf vendored Normal file
View File

@@ -0,0 +1 @@
node not found

169
tests/fixtures/h3c/config.conf vendored Normal file
View File

@@ -0,0 +1,169 @@
# H3C Comware Software, Version 7.1.070, Release 6616P01
# Copyright (c) 2004-2021 New H3C Technologies Co., Ltd. All rights reserved.
# Last reboot reason : User reboot
#
# Boot image: flash:/s9820-cmw710-boot-r6616p01.bin
# Boot image version: 7.1.070, Release 6616P01
# Compiled May 06 2021 11:00:00
# System image: flash:/s9820-cmw710-system-r6616p01.bin
# System image version: 7.1.070, Release 6616P01
# Compiled May 06 2021 11:00:00
#
#
# MPU(M) Slot 1:
# H3C S9820-64H MPU(M) with 1 C2538 Processor(s)
# BOARD TYPE: S9820-64H
# DRAM: 8192M bytes
# FLASH: 3630M bytes
# NVRAM: 0K bytes
# PCB 1 Version: VER.A
# PCB 2 Version: VER.B
# PCB 3 Version: VER.A
# PCB 4 Version: VER.A
# Basic BootWare Version: 105
# Extended BootWare Version: 108
# CPLD 1 Version: 002
# CPLD 2 Version: 027
# CPLD 3 Version: 002
# CPLD 4 Version: 002
# FPGA 1 Version: 001
# Release Version: H3C S9820-64H-6616P01
# Patch Version: None
# Reboot Cause: UserReboot
# [SubSlot 0] 64QSFP28
#
# MPU(S) Slot 2:
# H3C S9820-64H MPU(S) with 1 C2538 Processor(s)
# BOARD TYPE: S9820-64H
# DRAM: 8192M bytes
# FLASH: 3630M bytes
# NVRAM: 0K bytes
# PCB 1 Version: VER.A
# PCB 2 Version: VER.B
# PCB 3 Version: VER.A
# PCB 4 Version: VER.A
# Basic BootWare Version: 105
# Extended BootWare Version: 108
# CPLD 1 Version: 002
# CPLD 2 Version: 027
# CPLD 3 Version: 002
# CPLD 4 Version: 002
# FPGA 1 Version: 001
# Release Version: H3C S9820-64H-6616P01
# Patch Version: None
# Reboot Cause: IRFMergeReboot
# [SubSlot 0] 64QSFP28
# Slot Type State Subslot Soft Ver Patch Ver
# 1 S9820-64H Master 0 S9820-64H-6616P01 None
# 2 S9820-64H Standby 0 S9820-64H-6616P01 None
#
vlan 1
#
vlan 12
name BGP to OSPF1
#
vlan 13
name BGP to OSPF2
#
vlan 15
name HW_TEST_1
description HW_TEST_1
#
vlan 222
name MGMT
description MGMT
#
vlan 222
#
vlan 1112 to 1116
#
vlan 1122
name DATA
#
vlan 1123
#
vlan 1200
#
vlan 1512
#
vlan 1513
description cluster HW_TEST_1
#
vlan 2000
description cluster HW_TEST_2
#
vlan 3377
name VRF3377
#
irf-port 1/2
port group interface HundredGigE1/0/63
port group interface HundredGigE1/0/64
#
irf-port 2/1
port group interface HundredGigE2/0/63
port group interface HundredGigE2/0/64
#
interface Bridge-Aggregation1
description HW_TEST_1
port link-type trunk
undo port trunk permit vlan 1
port trunk permit vlan 221 1112 to 1116 1512 2000
link-aggregation mode dynamic
#
interface Bridge-Aggregation2
description HW_TEST_2
port link-type trunk
undo port trunk permit vlan 1
port trunk permit vlan 221 1112 to 1116 1512
link-aggregation mode dynamic
#
interface Bridge-Aggregation3
description HW_TEST_3
port link-type trunk
undo port trunk permit vlan 1
port trunk permit vlan 221 1112 to 1116 1512 2000
link-aggregation mode dynamic
#
interface Bridge-Aggregation4
description HW_TEST_4
port link-type trunk
undo port trunk permit vlan 1
port trunk permit vlan 221 1112 to 1116 1512
link-aggregation mode dynamic
#
interface NULL0
#
interface Vlan-interface1
dhcp client identifier ascii 0098a92d5735b0-VLAN0001
#
interface Vlan-interface12
description BGP to OSPF1
mtu 9008
ip address 15.12.16.246 255.255.255.252
#
interface Vlan-interface3000
description L3 to HW_TEST_3
ip binding vpn-instance HW_TEST_3
ip address 192.168.19.254 255.255.255.128
#
interface HundredGigE1/0/3
port link-mode bridge
description HW_TEST_1
port link-type trunk
undo port trunk permit vlan 1
port trunk permit vlan 221 1112 to 1116 1512
storm-constrain broadcast pps 1100 1000
storm-constrain multicast pps 1100 1000
storm-constrain control shutdown
port link-aggregation group 2
#
interface HundredGigE1/0/63
description HW_TEST_2
#
interface M-GigabitEthernet0/0/0
ip address 192.168.10.101 255.255.255.0
dhcp client identifier hex 0298a92d5735b0
#
interface M-GigabitEthernet0/0/1
dhcp client identifier hex 0298a92d5735b0
#

155
tests/fixtures/h3c/config.expected.json vendored Normal file
View File

@@ -0,0 +1,155 @@
{
"system": {
"model": "S9820-64H",
"serial_number": "",
"version": "7.1.070"
},
"interfaces": [
{
"interface": "Bridge-Aggregation1",
"ip_address": null,
"mask": null,
"description": "HW_TEST_1"
},
{
"interface": "Bridge-Aggregation2",
"ip_address": null,
"mask": null,
"description": "HW_TEST_2"
},
{
"interface": "Bridge-Aggregation3",
"ip_address": null,
"mask": null,
"description": "HW_TEST_3"
},
{
"interface": "Bridge-Aggregation4",
"ip_address": null,
"mask": null,
"description": "HW_TEST_4"
},
{
"interface": "NULL0",
"ip_address": null,
"mask": null,
"description": null
},
{
"interface": "Vlan-interface1",
"ip_address": null,
"mask": null,
"description": null
},
{
"interface": "Vlan-interface12",
"ip_address": "15.12.16.246",
"mask": 30,
"description": "BGP to OSPF1"
},
{
"interface": "Vlan-interface3000",
"ip_address": "192.168.19.254",
"mask": 25,
"description": "L3 to HW_TEST_3"
},
{
"interface": "HundredGigE1/0/3",
"ip_address": null,
"mask": null,
"description": "HW_TEST_1"
},
{
"interface": "HundredGigE1/0/63",
"ip_address": null,
"mask": null,
"description": "HW_TEST_2"
},
{
"interface": "M-GigabitEthernet0/0/0",
"ip_address": "192.168.10.101",
"mask": 24,
"description": null
},
{
"interface": "M-GigabitEthernet0/0/1",
"ip_address": null,
"mask": null,
"description": null
}
],
"vlans": [
{
"vlan_id": 1,
"description": null
},
{
"vlan_id": 12,
"description": null
},
{
"vlan_id": 13,
"description": null
},
{
"vlan_id": 15,
"description": "HW_TEST_1"
},
{
"vlan_id": 222,
"description": "MGMT"
},
{
"vlan_id": 222,
"description": null
},
{
"vlan_id": 1122,
"description": "DATA"
},
{
"vlan_id": 1123,
"description": null
},
{
"vlan_id": 1200,
"description": null
},
{
"vlan_id": 1512,
"description": null
},
{
"vlan_id": 1513,
"description": null
},
{
"vlan_id": 2000,
"description": null
},
{
"vlan_id": 3377,
"description": "HW_TEST_1"
},
{
"vlan_id": 1112,
"description": null
},
{
"vlan_id": 1113,
"description": null
},
{
"vlan_id": 1114,
"description": null
},
{
"vlan_id": 1115,
"description": null
},
{
"vlan_id": 1116,
"description": null
}
]
}

38
tests/fixtures/huawei/config.conf vendored Normal file
View File

@@ -0,0 +1,38 @@
# Huawei Versatile Routing Platform Software
# VRP (R) software, Version 5.170 (S5731 V200R019C00SPC500)
# Copyright (C) 2000-2019 HUAWEI TECH Co., Ltd.
#
# DDR Memory Size : 4096 M bytes
# FLASH Total Memory Size : 1024 M bytes
# FLASH Available Memory Size : 739 M bytes
# Pcb Version : VER.B
# BootROM Version : 0000.04e4
# BootLoad Version : 0213.0000
# CPLD Version : 0104
# Software Version : VRP (R) Software, Version 5.170 (V200R019C00SPC500)
# FLASH Version : 0000.0000
# PWR1 information
# Pcb Version : PWR VER.D
# PWR2 information
# Pcb Version : PWR VER.D
# FAN1 information
# Pcb Version : NA
# FAN2 information
# Pcb Version : NA
# ESN of slot 1: 102266666666
# ESN of slot 2: 102288888888
interface GigabitEthernet0/0/33
port link-type access
port default vlan 101
loopback-detect enable
stp disable
storm-control broadcast min-rate 1500 max-rate 2500
storm-control multicast min-rate 1000 max-rate 2000
storm-control action error-down
#
interface Vlanif120
description SSH
ip address 10.26.196.254 255.255.255.0
#
vlan batch 13 26 101 to 103 110 120 130 201 to 204 209 to 212 350 360
#

View File

@@ -0,0 +1,95 @@
{
"system": {
"model": "S5731",
"serial_number": "102266666666",
"version": "5.170"
},
"interfaces": [
{
"interface": "GigabitEthernet0/0/33",
"ip_address": null,
"mask": null,
"description": null
},
{
"interface": "Vlanif120",
"ip_address": "10.26.196.254",
"mask": 24,
"description": "SSH"
}
],
"vlans": [
{
"vlan_id": 13,
"description": null
},
{
"vlan_id": 26,
"description": null
},
{
"vlan_id": 101,
"description": null
},
{
"vlan_id": 102,
"description": null
},
{
"vlan_id": 103,
"description": null
},
{
"vlan_id": 110,
"description": null
},
{
"vlan_id": 120,
"description": null
},
{
"vlan_id": 130,
"description": null
},
{
"vlan_id": 201,
"description": null
},
{
"vlan_id": 202,
"description": null
},
{
"vlan_id": 203,
"description": null
},
{
"vlan_id": 204,
"description": null
},
{
"vlan_id": 209,
"description": null
},
{
"vlan_id": 210,
"description": null
},
{
"vlan_id": 211,
"description": null
},
{
"vlan_id": 212,
"description": null
},
{
"vlan_id": 350,
"description": null
},
{
"vlan_id": 360,
"description": null
}
]
}

341
tests/fixtures/keenetic/config.conf vendored Normal file
View File

@@ -0,0 +1,341 @@
!
! release: 4.03.C.6.2-7
! sandbox: stable
! title: 4.3.6.2
! arch: mips
!
! ndm:
! exact: 0-a3057529fd
! cdate: 29 Sep 2025
!
! bsp:
! exact: 0-03b50470c4
! cdate: 30 Sep 2025
!
! ndw:
! features: dual_image,led_control,wifi_button,wifi5ghz,
! vht2ghz,mimo2ghz,mimo5ghz,atf2ghz,atf5ghz,wifi6,wifi_ft,
! wpa3,hwnat
! components: base,cloudcontrol,corewireless,ddns,dhcpd,
! dns-filter,dns-https,dns-tls,dot1x,easyconfig,igmp,ip6,
! lang-en,lang-ru,miniupnpd,mws,nathelper-ftp,nathelper-
! h323,nathelper-pptp,nathelper-rtsp,nathelper-sip,ndmp,
! ndns,openvpn,pingcheck,ppe,pppoe,pptp,ssh,trafficcontrol,
! wireguard
!
! ndw3:
! version: 1.101.18.1
!
! ndw4:
! version: 4.3.C.6.2
!
! manufacturer: Keenetic Ltd.
! vendor: Keenetic
! series: KN
! model: Sprinter (KN-3710)
! hw_version: 7777777
! hw_type: router
! hw_id: KN-3710
! device: Sprinter
! region: EA
! description: Keenetic Sprinter (KN-3710)
! $$$ Agent: http/rci
! $$$ Last change: Fri, 3 Oct 2025 18:37:40 GMT
! $$$ Model: Keenetic Sprinter
! $$$ Username: admin
! $$$ Version: 2.06.1
system
set net.ipv4.ip_forward 1
set net.ipv4.neigh.default.gc_thresh1 256
set net.ipv4.neigh.default.gc_thresh2 1024
set net.ipv4.neigh.default.gc_thresh3 2048
set net.ipv4.tcp_fin_timeout 30
set net.ipv4.tcp_keepalive_time 120
set net.ipv6.conf.all.forwarding 1
set net.ipv6.neigh.default.gc_thresh1 256
set net.ipv6.neigh.default.gc_thresh2 1024
set net.ipv6.neigh.default.gc_thresh3 2048
set net.netfilter.nf_conntrack_tcp_timeout_established 1200
set vm.overcommit_memory 0
set vm.vfs_cache_pressure 1000
clock timezone Europe/Berlin
domainname WORKGROUP
hostname test_HW
caption default
description "Keenetic Sprinter (KN-3710)"
ndss dump-report disable
!
dyndns profile _WEBADMIN
!
interface GigabitEthernet0
up
!
interface GigabitEthernet0/1
rename 1
switchport mode access
switchport access vlan 1
up
!
interface GigabitEthernet0/2
rename 2
switchport mode access
switchport access vlan 1
up
!
interface GigabitEthernet0/3
rename 3
switchport mode access
switchport access vlan 1
up
!
interface GigabitEthernet0/Vlan1
description "Home VLAN"
ip dhcp client dns-routes
ip name-servers
up
!
interface GigabitEthernet0/Vlan2
rename ISP
description "\xd0\x9f\xd0\xbe\xd0\xb4\xd0\xba\xd0\xbb\xd1\x8e\xd1\x87\xd0\xb5\xd0\xbd\xd0\xb8\xd0\xb5 Ethernet"
dyndns nobind
mac address factory wan
security-level public
ip address dhcp
ip dhcp client hostname test_HW
ip dhcp client dns-routes
ip mtu 1500
ip access-group _WEBADMIN_ISP in
ip global 57342
ip no name-servers
igmp upstream
ipv6 address auto
ipv6 prefix auto
ipv6 no name-servers auto
up
!
interface GigabitEthernet0/0
rename 0
role inet for ISP
switchport mode access
switchport access vlan 2
up
!
interface GigabitEthernet0/Vlan3
dyndns nobind
ip dhcp client dns-routes
ip name-servers
up
!
interface WifiMaster0
country-code RU
compatibility BGN+AX
rekey-interval 86400
up
!
interface WifiMaster0/AccessPoint0
mac access-list type none
authentication wpa-psk ns3 7777ggggddddsss
encryption enable
encryption wpa2
ip dhcp client dns-routes
ssid test_HW_2.4G
up
!
interface WifiMaster0/AccessPoint1
mac access-list type none
security-level private
encryption no enable
ip dhcp client dns-routes
down
!
interface WifiMaster0/AccessPoint2
mac access-list type none
security-level private
encryption no enable
ip dhcp client dns-routes
down
!
interface WifiMaster0/WifiStation0
security-level public
encryption no enable
ip dhcp client dns-routes
standby enable
standby timeout 600
down
!
interface WifiMaster1
country-code RU
compatibility AN+AC+AX
channel width 40-above/80
rekey-interval 86400
up
!
interface WifiMaster1/AccessPoint0
mac access-list type none
authentication wpa-psk ns3 7777ggggddddsss
encryption enable
encryption wpa2
ip dhcp client dns-routes
ssid test_HW_5G
up
!
interface WifiMaster1/AccessPoint1
mac access-list type none
security-level private
encryption no enable
ip dhcp client dns-routes
down
!
interface WifiMaster1/AccessPoint2
mac access-list type none
security-level private
encryption no enable
ip dhcp client dns-routes
down
!
interface WifiMaster1/WifiStation0
security-level public
encryption no enable
ip dhcp client dns-routes
standby enable
standby timeout 600
down
!
interface Bridge0
rename Home
description "Home network"
dyndns nobind
include GigabitEthernet0/Vlan1
include WifiMaster0/AccessPoint0
include WifiMaster1/AccessPoint0
mac access-list type none
security-level private
ip address 17.36.1.1 255.255.255.0
ip dhcp client dns-routes
ip access-group _WEBADMIN_Home in
ip name-servers
band-steering
up
!
interface Bridge1
rename Guest
description "Guest network"
traffic-shape rate 5120
dyndns nobind
include GigabitEthernet0/Vlan3
mac access-list type none
peer-isolation
security-level protected
ip address 10.1.30.1 255.255.255.0
ip dhcp client dns-routes
ip name-servers
down
!
interface Bridge2
rename Test
mac access-list type none
security-level public
ip dhcp client dns-routes
up
!
interface OpenVPN0
description test_HW-udp
role misc
security-level public
ip dhcp client dns-routes
ip tcp adjust-mss pmtu
ip name-servers
ipv6 name-servers auto
openvpn accept-routes
openvpn connect
up
!
interface OpenVPN2
description test_HW-tcp
role misc
dyndns nobind
security-level public
ip dhcp client dns-routes
ip tcp adjust-mss pmtu
openvpn accept-routes
openvpn connect
down
!
interface Wireguard0
description test_HW
dyndns nobind
security-level public
ip address 10.3.100.1 255.255.255.0
ip mtu 1324
ip tcp adjust-mss pmtu
wireguard listen-port 65513
wireguard peer 7777ggggddddsss= !test_HW
allow-ips 0.0.0.0 0.0.0.0
connect
!
up
!
interface Wireguard1
description test_HW
dyndns nobind
security-level private
ip address 10.1.100.1 255.255.255.0
ip mtu 1324
ip access-group _WEBADMIN_Wireguard1 in
ip tcp adjust-mss pmtu
wireguard listen-port 65511
wireguard peer 7777ggggddddsss= !test_HW
allow-ips 10.1.100.0 255.255.255.0
allow-ips 17.36.3.0 255.255.255.0
allow-ips 17.36.1.0 255.255.255.0
allow-ips 0.0.0.0 0.0.0.0
connect
!
up
!
interface Wireguard2
description test_HW
dyndns nobind
security-level private
ip address 10.2.100.1 255.255.255.0
ip access-group _WEBADMIN_Wireguard2 in
ip tcp adjust-mss pmtu
wireguard listen-port 65512
wireguard peer 7777ggggddddsss= !test_HW
allow-ips 0.0.0.0 0.0.0.0
connect
!
up
!
ip ssh
port 22
security-level public
lockout-policy 5 15 3
!
ip hotspot
policy Home permit
host 7777ggggddddsss permit
host 7777ggggddddsss priority 4
!
ipv6 subnet Default
bind Home
mode slaac
prefix length 64
number 0
!
ppe software
ppe hardware
upnp lan Home
service dhcp
service dns-proxy
service http
service telnet
service ssh
service ntp
service upnp
!
easyconfig disable
components
auto-update disable
auto-update channel stable
!

View File

@@ -0,0 +1,161 @@
{
"system": {
"model": "Sprinter (KN-3710)",
"serial_number": "7777777",
"version": "4.03.C.6.2-7"
},
"interfaces": [
{
"interface": "GigabitEthernet0",
"ip_address": null,
"mask": null,
"description": null
},
{
"interface": "GigabitEthernet0/1",
"ip_address": null,
"mask": null,
"description": null
},
{
"interface": "GigabitEthernet0/2",
"ip_address": null,
"mask": null,
"description": null
},
{
"interface": "GigabitEthernet0/3",
"ip_address": null,
"mask": null,
"description": null
},
{
"interface": "GigabitEthernet0/0",
"ip_address": null,
"mask": null,
"description": null
},
{
"interface": "WifiMaster0",
"ip_address": null,
"mask": null,
"description": null
},
{
"interface": "WifiMaster0/AccessPoint0",
"ip_address": null,
"mask": null,
"description": null
},
{
"interface": "WifiMaster0/AccessPoint1",
"ip_address": null,
"mask": null,
"description": null
},
{
"interface": "WifiMaster0/AccessPoint2",
"ip_address": null,
"mask": null,
"description": null
},
{
"interface": "WifiMaster0/WifiStation0",
"ip_address": null,
"mask": null,
"description": null
},
{
"interface": "WifiMaster1",
"ip_address": null,
"mask": null,
"description": null
},
{
"interface": "WifiMaster1/AccessPoint0",
"ip_address": null,
"mask": null,
"description": null
},
{
"interface": "WifiMaster1/AccessPoint1",
"ip_address": null,
"mask": null,
"description": null
},
{
"interface": "WifiMaster1/AccessPoint2",
"ip_address": null,
"mask": null,
"description": null
},
{
"interface": "WifiMaster1/WifiStation0",
"ip_address": null,
"mask": null,
"description": null
},
{
"interface": "Bridge0",
"ip_address": "17.36.1.1",
"mask": 24,
"description": "Home network"
},
{
"interface": "Bridge1",
"ip_address": "10.1.30.1",
"mask": 24,
"description": "Guest network"
},
{
"interface": "Bridge2",
"ip_address": null,
"mask": null,
"description": null
},
{
"interface": "OpenVPN0",
"ip_address": null,
"mask": null,
"description": "test_HW-udp"
},
{
"interface": "OpenVPN2",
"ip_address": null,
"mask": null,
"description": "test_HW-tcp"
},
{
"interface": "Wireguard0",
"ip_address": "10.3.100.1",
"mask": 24,
"description": "test_HW"
},
{
"interface": "Wireguard1",
"ip_address": "10.1.100.1",
"mask": 24,
"description": "test_HW"
},
{
"interface": "Wireguard2",
"ip_address": "10.2.100.1",
"mask": 24,
"description": "test_HW"
}
],
"vlans": [
{
"vlan_id": 1,
"description": "Home VLAN"
},
{
"vlan_id": 2,
"description": "Подключение Ethernet"
},
{
"vlan_id": 3,
"description": "Home network"
}
]
}

123
tests/fixtures/mikrotik/config.conf vendored Normal file
View File

@@ -0,0 +1,123 @@
# 2026-02-18 22:32:59 by RouterOS 7.19.3
# software id = 0V5S-56MC
# version: 7.12
# model = C52iG-5HaxD2HaxD
# serial number = HE108BBGW0B
/interface bridge
add name=bridge.LAN
/interface ethernet
set [ find default-name=ether1 ] mac-address=C4:AD:32:B2:A1:9A poe-out=off
set [ find default-name=ether4 ] comment=test
/interface vlan
add comment="super test vlan" interface=ether4 name="test vlan" vlan-id=255
add disabled=yes interface=ether5 name="test test vlan" vlan-id=254
/interface list
add name=LAN
add name=WAN
/interface wifi channel
add band=2ghz-ax disabled=no name=ch-2ghz width=20/40mhz
add band=5ghz-ax disabled=no name=ch-5ghz width=20/40mhz
/interface wifi security
add authentication-types=wpa2-psk name=common-auth wps=disable
/interface wifi configuration
add name=common-auth security=common-auth ssid=test_HW
/interface wifi
set [ find default-name=wifi1 ] channel=ch-5ghz configuration=common-auth configuration.mode=ap disabled=no
set [ find default-name=wifi2 ] channel=ch-2ghz configuration=common-auth configuration.mode=ap disabled=no
/ip pool
add name=dhcp_pool0 ranges=172.16.3.2-172.16.3.254
add name=dhcp_pool1 ranges=192.168.3.2-192.168.3.254
/ip dhcp-server
add address-pool=dhcp_pool0 interface=bridge.LAN lease-time=10h name=dhcp1
/ppp profile
add name=new_antizapret on-down="/ip dns cache flush\r\
\n" on-up="/ip dns cache flush\r\
\n"
add name=robovps on-down="/ip dns cache flush" on-up="/ip dns cache flush" use-ipv6=no
/routing table
add disabled=no fib name=test_HW_table
/interface bridge port
add bridge=bridge.LAN interface=ether2 learn=yes
add bridge=bridge.LAN interface=ether3
add bridge=bridge.LAN interface=ether4
add bridge=bridge.LAN interface=ether5
add bridge=bridge.LAN interface=wifi1
add bridge=bridge.LAN interface=wifi2
/ipv6 settings
set disable-ipv6=yes forward=no
/interface detect-internet
set detect-interface-list=all
/interface list member
add interface=bridge.LAN list=LAN
add interface=ether1 list=WAN
/interface ovpn-server server
add mac-address=FE:25:E0:B8:66:01 name=ovpn-server1
/ip address
add address=172.16.3.1/24 interface=bridge.LAN network=172.16.3.0
add address=10.38.3.245/24 interface=ether1 network=10.38.3.0
add address=10.1.100.2/24 interface=wireguard2 network=10.1.100.0
add address=100.10.10.1/24 disabled=yes interface=ether4 network=100.10.10.0
/ip dhcp-server lease
add address=172.16.3.20 client-id=1:d8:3a:dd:22:28:1d mac-address=D8:3A:DD:21:28:1D server=dhcp1
add address=172.16.3.4 client-id=1:2c:cd:29:1a:ea:6d comment=test_HW mac-address=1E:CD:29:8A:EA:6D server=dhcp1
/ip dhcp-server network
add address=172.16.3.0/24 dns-server=172.16.3.1 gateway=172.16.3.1
/ip dns
set allow-remote-requests=yes servers=217.10.44.35
/ip dns static
add address=172.16.3.20 regexp=".*\\.home\$" type=A
add address=172.16.3.20 regexp=".*\\.home.uk\$" type=A
add address=172.16.3.20 disabled=yes regexp=".*\\.home.uk\$" type=A
/ip firewall address-list
add address=172.16.3.0/24 list=test_HW
add address=172.16.2.0/24 list=test_HW
add address=172.16.1.0/24 list=test_HW
add address=255.255.255.255 list=test_HW
/ip firewall filter
add action=drop chain=forward out-interface=ether1 src-address=172.16.3.11
add action=accept chain=input src-address=255.255.255.255
add action=accept chain=forward in-interface=wireguard2
add action=accept chain=forward connection-state=established,related in-interface=ether1
add action=accept chain=input connection-state=established,related in-interface=ether1
add action=accept chain=input in-interface=bridge.LAN
add action=drop chain=input in-interface=ether1
add action=drop chain=forward in-interface=ether1
/ip firewall mangle
add action=passthrough chain=prerouting connection-mark=test_HW src-address=172.16.3.20
/ip firewall nat
add action=masquerade chain=srcnat out-interface=ether1
add action=masquerade chain=srcnat comment=test_HW out-interface=test_HW
/ip firewall service-port
set ftp disabled=yes
set tftp disabled=yes
set h323 disabled=yes
/ip route
add disabled=no distance=1 dst-address=172.16.1.0/24 gateway=10.1.100.1
add disabled=no distance=1 dst-address=172.16.2.0/24 gateway=10.1.100.1
add disabled=no distance=1 dst-address=255.255.255.255/32 gateway=10.38.3.1
add disabled=no distance=3 dst-address=0.0.0.0/0 gateway=10.38.3.1 routing-table=main scope=30 suppress-hw-offload=no target-scope=10
add disabled=yes distance=2 dst-address=0.0.0.0/0 gateway=10.38.3.1 routing-table=main scope=30 suppress-hw-offload=no target-scope=10
add comment=test_HW disabled=no distance=1 dst-address=185.255.255.255/32 gateway=10.38.3.1 routing-table=main scope=30 suppress-hw-offload=no target-scope=10
add comment=test_HW disabled=no distance=1 dst-address=192.168.255.255/32 gateway=10.38.3.1 routing-table=main scope=30 suppress-hw-offload=no target-scope=10
/ip service
set telnet disabled=yes
/system clock
set time-zone-name=Europe/Berlin
/system identity
set name=test_HW
/system logging
set 0 topics=info,!wireless
/system note
set show-at-login=no
/system ntp client
set enabled=yes
/system ntp client servers
add address=pool.ntp.org
/tool mac-server
set allowed-interface-list=LAN
/tool mac-server mac-winbox
set allowed-interface-list=LAN
/tool mac-server ping
set enabled=no
/user aaa
set default-group=full use-radius=yes

View File

@@ -0,0 +1,43 @@
{
"system": {
"model": "C52iG-5HaxD2HaxD",
"serial_number": "HE108BBGW0B",
"version": "7.12"
},
"interfaces": [
{
"interface": "bridge.LAN",
"ip_address": "172.16.3.1",
"mask": 24,
"description": null
},
{
"interface": "ether1",
"ip_address": "10.38.3.245",
"mask": 24,
"description": null
},
{
"interface": "wireguard2",
"ip_address": "10.1.100.2",
"mask": 24,
"description": null
},
{
"interface": "ether4",
"ip_address": "100.10.10.1",
"mask": 24,
"description": null
}
],
"vlans": [
{
"vlan_id": 255,
"description": "test vlan"
},
{
"vlan_id": 254,
"description": "test test vlan"
}
]
}

42
tests/fixtures/qtech/config_1.conf vendored Normal file
View File

@@ -0,0 +1,42 @@
! QTECH LLC Internetwork Operating System Software
! QSW-8330-40T-DC Series Software, Version 2.2.0C Build 96279, RELEASE SOFTWARE
! ROM: System Bootstrap, Version 0.4.7,hardware version:A
! Serial num:6060606060606060, ID num:555555555555
! System image file is "Switch.bin"
! QTECH LLC QSW-8330-40T-DC RISC
! 524288K bytes of memory,16384K bytes of flash
! Base ethernet MAC Address: 08:c6:b3:08:cf:ff
! snmp info:
! vend_ID:27514 product_ID:404 system_ID:1.3.6.1.4.1.27514
interface GigaEthernet1/0/9
shutdown
description FREE
switchport pvid 102
storm-control broadcast threshold 15
storm-control broadcast action shutdown
storm-control broadcast auto_resume 60s
storm-control multicast threshold 10
storm-control multicast action shutdown
storm-control multicast auto_resume 60s
qos policy IPP3 ingress
!
interface VLAN1
ip address 192.168.0.1 255.255.0.0
ip mtu 1500
no ip directed-broadcast
!
interface VLAN1002
description test-1002
ip address 13.36.8.1 255.255.255.0
ip mtu 1500
no ip directed-broadcast
!
vlan 772
name test
!
vlan 888
name test_super
!
vlan 1,7,14-15,44,101-102,115,117-124,130-136,139,167,200-205,772
,1607
vlan 888,2016,2085-2088

View File

@@ -0,0 +1,185 @@
{
"system": {
"model": "QSW-8330-40T-DC",
"serial_number": "6060606060606060",
"version": "96279"
},
"interfaces": [
{
"interface": "GigaEthernet1/0/9",
"ip_address": null,
"mask": null,
"description": "FREE"
},
{
"interface": "VLAN1",
"ip_address": "192.168.0.1",
"mask": 16,
"description": null
},
{
"interface": "VLAN1002",
"ip_address": "13.36.8.1",
"mask": 24,
"description": "test-1002"
}
],
"vlans": [
{
"vlan_id": 772,
"description": "test"
},
{
"vlan_id": 888,
"description": "test_super"
},
{
"vlan_id": 1,
"description": null
},
{
"vlan_id": 7,
"description": null
},
{
"vlan_id": 14,
"description": null
},
{
"vlan_id": 15,
"description": null
},
{
"vlan_id": 44,
"description": null
},
{
"vlan_id": 101,
"description": null
},
{
"vlan_id": 102,
"description": null
},
{
"vlan_id": 115,
"description": null
},
{
"vlan_id": 117,
"description": null
},
{
"vlan_id": 118,
"description": null
},
{
"vlan_id": 119,
"description": null
},
{
"vlan_id": 120,
"description": null
},
{
"vlan_id": 121,
"description": null
},
{
"vlan_id": 122,
"description": null
},
{
"vlan_id": 123,
"description": null
},
{
"vlan_id": 124,
"description": null
},
{
"vlan_id": 130,
"description": null
},
{
"vlan_id": 131,
"description": null
},
{
"vlan_id": 132,
"description": null
},
{
"vlan_id": 133,
"description": null
},
{
"vlan_id": 134,
"description": null
},
{
"vlan_id": 135,
"description": null
},
{
"vlan_id": 136,
"description": null
},
{
"vlan_id": 139,
"description": null
},
{
"vlan_id": 167,
"description": null
},
{
"vlan_id": 200,
"description": null
},
{
"vlan_id": 201,
"description": null
},
{
"vlan_id": 202,
"description": null
},
{
"vlan_id": 203,
"description": null
},
{
"vlan_id": 204,
"description": null
},
{
"vlan_id": 205,
"description": null
},
{
"vlan_id": 1607,
"description": null
},
{
"vlan_id": 2016,
"description": null
},
{
"vlan_id": 2085,
"description": null
},
{
"vlan_id": 2086,
"description": null
},
{
"vlan_id": 2087,
"description": null
},
{
"vlan_id": 2088,
"description": null
}
]
}

36
tests/fixtures/qtech/config_2.conf vendored Normal file
View File

@@ -0,0 +1,36 @@
! QTECH LLC Internetwork Operating System Software
! QSW-8330-40T-DC Series Software, Version 2.2.0C Build 96279, RELEASE SOFTWARE
! ROM: System Bootstrap, Version 0.4.7,hardware version:A
! Serial num:6060606060606060, ID num:555555555555
! System image file is "Switch.bin"
! QTECH LLC QSW-8330-40T-DC RISC
! 524288K bytes of memory,16384K bytes of flash
! Base ethernet MAC Address: 08:c6:b3:08:cf:f7
! snmp info:
! vend_ID:27514 product_ID:404 system_ID:1.3.6.1.4.1.27514
interface GigaEthernet1/0/9
shutdown
description FREE
switchport pvid 102
storm-control broadcast threshold 15
storm-control broadcast action shutdown
storm-control broadcast auto_resume 60s
storm-control multicast threshold 10
storm-control multicast action shutdown
storm-control multicast auto_resume 60s
qos policy IPP3 ingress
!
interface VLAN1
ip address 192.168.0.1 255.255.0.0
ip mtu 1500
no ip directed-broadcast
!
interface VLAN1002
description test-1002
ip address 13.36.8.1 255.255.255.0
ip mtu 1500
no ip directed-broadcast
!
vlan 1,7,14-15,44,101-102,115,117-124,130-136,139,167,200-205,772
,1607
vlan 888,2016,2085-2088

View File

@@ -0,0 +1,185 @@
{
"system": {
"model": "QSW-8330-40T-DC",
"serial_number": "6060606060606060",
"version": "96279"
},
"interfaces": [
{
"interface": "GigaEthernet1/0/9",
"ip_address": null,
"mask": null,
"description": "FREE"
},
{
"interface": "VLAN1",
"ip_address": "192.168.0.1",
"mask": 16,
"description": null
},
{
"interface": "VLAN1002",
"ip_address": "13.36.8.1",
"mask": 24,
"description": "test-1002"
}
],
"vlans": [
{
"vlan_id": 1,
"description": null
},
{
"vlan_id": 7,
"description": null
},
{
"vlan_id": 14,
"description": null
},
{
"vlan_id": 15,
"description": null
},
{
"vlan_id": 44,
"description": null
},
{
"vlan_id": 101,
"description": null
},
{
"vlan_id": 102,
"description": null
},
{
"vlan_id": 115,
"description": null
},
{
"vlan_id": 117,
"description": null
},
{
"vlan_id": 118,
"description": null
},
{
"vlan_id": 119,
"description": null
},
{
"vlan_id": 120,
"description": null
},
{
"vlan_id": 121,
"description": null
},
{
"vlan_id": 122,
"description": null
},
{
"vlan_id": 123,
"description": null
},
{
"vlan_id": 124,
"description": null
},
{
"vlan_id": 130,
"description": null
},
{
"vlan_id": 131,
"description": null
},
{
"vlan_id": 132,
"description": null
},
{
"vlan_id": 133,
"description": null
},
{
"vlan_id": 134,
"description": null
},
{
"vlan_id": 135,
"description": null
},
{
"vlan_id": 136,
"description": null
},
{
"vlan_id": 139,
"description": null
},
{
"vlan_id": 167,
"description": null
},
{
"vlan_id": 200,
"description": null
},
{
"vlan_id": 201,
"description": null
},
{
"vlan_id": 202,
"description": null
},
{
"vlan_id": 203,
"description": null
},
{
"vlan_id": 204,
"description": null
},
{
"vlan_id": 205,
"description": null
},
{
"vlan_id": 772,
"description": null
},
{
"vlan_id": 1607,
"description": null
},
{
"vlan_id": 888,
"description": null
},
{
"vlan_id": 2016,
"description": null
},
{
"vlan_id": 2085,
"description": null
},
{
"vlan_id": 2086,
"description": null
},
{
"vlan_id": 2087,
"description": null
},
{
"vlan_id": 2088,
"description": null
}
]
}

41
tests/fixtures/quasar/config_1.conf vendored Normal file
View File

@@ -0,0 +1,41 @@
# Copyright © 2021-2022, TechArgos LLC
# ----------- -----------------
# Subsystem Version
# ----------- -----------------
# Engine 0.2.17.2022-10-21
# DPlane 0.2.18.2022-05-16
# BfMonitor 1.1.2.2022-08-25
# CLI.core 1.0.3.2022-09-30
# CLI.engine 1.2.2.2022-10-12
# RConsole 0.3.4.2022-01-12
# RcAppParams 0.3.1.2022-01-12
# SNMP 0.0.11.2022-04-26
# Zabbix 0.2.13.2022-01-26
# WebUI 1.1.3.2022-10-19
# BF.core 9.3.1.2021-01-30
#
# ------------------------- ---------------
# Platform EEPROM field Value
# ------------------------- ---------------
# Product Name Quasar-T-Q-0002
# Product Number HB4NC011234M
# Local MAC N/A
# Product Serial Number WEE1C1CC0004A
# Product Version 0.0
# System Manufacturing Date 2026-06-07
config ethernet ipv4 address 25.25.1.221/24 gateway 25.25.1.254 enable
config interface 7/4 fec none mode force-up enable
config interface 8/1 fec none mode force-up enable
config interface 8/2 fec none mode force-up enable
config interface 8/3 fec none mode force-up enable
config interface 8/4 fec none mode force-up enable
config interface 9/1 fec none mode force-up enable
config interface 9/2 fec none mode force-up enable
config interface 7/4 description "IN DWDM / OUT TEST_HW_08_N0_p1"
config interface 8/1 description "IN DWDM / OUT TEST_HW_09_N1_p0"
config interface 8/2 description "IN DWDM / OUT TEST_HW_09_N0_p0"
config interface 8/3 description "IN DWDM / OUT TEST_HW_10_N1_p0"
config interface 8/4 description "IN DWDM / OUT TEST_HW_10_N0_p0"
config interface 9/1 description "IN DWDM / OUT TEST_HW_11_N0_p0"
config interface 9/2 description "IN DWDM / OUT TEST_HW_11_N0_p1"

View File

@@ -0,0 +1,58 @@
{
"system": {
"model": "Quasar-T-Q-0002",
"serial_number": "WEE1C1CC0004A",
"version": "0.2.17.2022-10-21"
},
"interfaces": [
{
"interface": "7/4",
"ip_address": null,
"mask": null,
"description": "IN DWDM / OUT TEST_HW_08_N0_p1"
},
{
"interface": "8/1",
"ip_address": null,
"mask": null,
"description": "IN DWDM / OUT TEST_HW_09_N1_p0"
},
{
"interface": "8/2",
"ip_address": null,
"mask": null,
"description": "IN DWDM / OUT TEST_HW_09_N0_p0"
},
{
"interface": "8/3",
"ip_address": null,
"mask": null,
"description": "IN DWDM / OUT TEST_HW_10_N1_p0"
},
{
"interface": "8/4",
"ip_address": null,
"mask": null,
"description": "IN DWDM / OUT TEST_HW_10_N0_p0"
},
{
"interface": "9/1",
"ip_address": null,
"mask": null,
"description": "IN DWDM / OUT TEST_HW_11_N0_p0"
},
{
"interface": "9/2",
"ip_address": null,
"mask": null,
"description": "IN DWDM / OUT TEST_HW_11_N0_p1"
},
{
"interface": "ethernet",
"ip_address": "25.25.1.221",
"mask": 24,
"description": null
}
],
"vlans": []
}

43
tests/fixtures/quasar/config_2.conf vendored Normal file
View File

@@ -0,0 +1,43 @@
# Copyright © 2021-2024, TechArgos LLC
# ----------- ------------------
# Component Version
# ----------- ------------------
# Assembly 0.2.23_9.9.1_GA
# Engine 2.23.15.2024-08-22
# DPlane 0.2.20.2023-10-17
# BfMonitor 1.4.0.2024-08-27
# CLI.core 1.0.3.2022-09-30
# CLI.engine 1.3.4.2024-08-22
# RConsole 1.0.1.2022-12-23
# RcAppParams 1.0.1.2022-12-23
# SNMP 0.3.0.2024-07-08
# Zabbix 0.2.13.2022-01-26
# WebUI 1.3.34.2024-08-30
# NMS.agent 1.2.12.2024-08-21
# SysLog 0.7.12.2024-02-02
# NTPd 0.1.5.2024-07-15
# AAA 0.0.8.2023-11-16
#
# ------------------------- --------------------
# Platform EEPROM field Value
# ------------------------- --------------------
# Product Name D5232C-T
# Part Number TA-PB-D5232C-T-AC-PI
# Local MAC N/A
# Product Serial Number WHF1C87123456A
# Product Version 0.0
# System Manufacturing Date 2026-06-07
config ethernet ipv4 address 25.25.18.19/24 gateway 25.25.18.254 enable
config interface 1/1 description "TEST_HW_1_1"
config interface 1/3 description "TEST_HW_1_3"
config interface 2/1 description "TEST_HW_2_1"
config interface 2/2 description "TEST_HW_2_2"
config interface 2/3 description "TEST_HW_2_3"
config interface 2/4 description "TEST_HW_2_4"
config interface 3/1 description "TEST_HW_3_1"
config interface 3/3 description "TEST_HW_3_3"
config interface 3/4 description "TEST_HW_3_4"
config interface 4/1 description "TEST_HW_4_1"
config interface 4/2 description "TEST_HW_4_2"
config interface 4/3 description "TEST_HW_4_3"
config interface 4/4 description "TEST_HW_4_4"

View File

@@ -0,0 +1,94 @@
{
"system": {
"model": "D5232C-T",
"serial_number": "WHF1C87123456A",
"version": "0.2.23_9.9.1_GA"
},
"interfaces": [
{
"interface": "1/1",
"ip_address": null,
"mask": null,
"description": "TEST_HW_1_1"
},
{
"interface": "1/3",
"ip_address": null,
"mask": null,
"description": "TEST_HW_1_3"
},
{
"interface": "2/1",
"ip_address": null,
"mask": null,
"description": "TEST_HW_2_1"
},
{
"interface": "2/2",
"ip_address": null,
"mask": null,
"description": "TEST_HW_2_2"
},
{
"interface": "2/3",
"ip_address": null,
"mask": null,
"description": "TEST_HW_2_3"
},
{
"interface": "2/4",
"ip_address": null,
"mask": null,
"description": "TEST_HW_2_4"
},
{
"interface": "3/1",
"ip_address": null,
"mask": null,
"description": "TEST_HW_3_1"
},
{
"interface": "3/3",
"ip_address": null,
"mask": null,
"description": "TEST_HW_3_3"
},
{
"interface": "3/4",
"ip_address": null,
"mask": null,
"description": "TEST_HW_3_4"
},
{
"interface": "4/1",
"ip_address": null,
"mask": null,
"description": "TEST_HW_4_1"
},
{
"interface": "4/2",
"ip_address": null,
"mask": null,
"description": "TEST_HW_4_2"
},
{
"interface": "4/3",
"ip_address": null,
"mask": null,
"description": "TEST_HW_4_3"
},
{
"interface": "4/4",
"ip_address": null,
"mask": null,
"description": "TEST_HW_4_4"
},
{
"interface": "ethernet",
"ip_address": "25.25.18.19",
"mask": 24,
"description": null
}
],
"vlans": []
}

39
tests/test_models.py Normal file
View File

@@ -0,0 +1,39 @@
import json
import pytest
from conftest import FIXTURES, load
from oxi.interfaces import device_registry
MODEL_CASES = [
("mikrotik", "config.conf", "config.expected.json"),
("keenetic", "config.conf", "config.expected.json"),
("qtech", "config_1.conf", "config_1.expected.json"),
("qtech", "config_2.conf", "config_2.expected.json"),
("huawei", "config.conf", "config.expected.json"),
("eltex", "config.conf", "config.expected.json"),
("h3c", "config.conf", "config.expected.json"),
("quasar", "config_1.conf", "config_1.expected.json"),
("quasar", "config_2.conf", "config_2.expected.json"),
]
@pytest.mark.parametrize("model_key, fixture, expected_file", MODEL_CASES)
def test_parse_matches_golden(model_key, fixture, expected_file):
cls = device_registry[model_key]
raw = load(model_key, fixture)
parsed = cls(raw).parse().model_dump(by_alias=True, mode="json")
expected = json.loads((FIXTURES / model_key / expected_file).read_text("utf-8"))
assert parsed == expected
@pytest.mark.parametrize("model_key, fixture, _expected", MODEL_CASES)
def test_parse_has_required_sections(model_key, fixture, _expected):
cls = device_registry[model_key]
device = cls(load(model_key, fixture)).parse()
assert device.system is not None
assert isinstance(device.interfaces, list)
assert isinstance(device.vlans, list)

87
tests/test_network.py Normal file
View File

@@ -0,0 +1,87 @@
import pytest
import responses
from conftest import load
from oxi import OxiAPI
from oxi.exception import OxiAPIError
BASE = "https://oxi.example.com"
NODE_DATA = {
"name": "HQ",
"full_name": "grp/HQ",
"model": "keenetic",
"ip": "192.168.1.1",
"group": "grp",
}
@responses.activate
def test_node_show_returns_view():
responses.get(f"{BASE}/node/show/HQ.json", json=NODE_DATA)
api = OxiAPI(url=BASE)
node = api.node("HQ")
assert node.ip == "192.168.1.1"
assert node.model == "keenetic"
assert node.full_name == "grp/HQ"
assert node.group == "grp"
@responses.activate
def test_node_config_fetches_and_parses():
responses.get(f"{BASE}/node/show/HQ.json", json=NODE_DATA)
responses.get(f"{BASE}/node/fetch/grp/HQ", body=load("keenetic"))
api = OxiAPI(url=BASE)
config = api.node("HQ").config
assert config.system.model == "Sprinter (KN-3710)"
assert len(config.interfaces) > 0
@responses.activate
def test_node_not_found_maps_to_404():
responses.get(f"{BASE}/node/show/missing.json", status=404)
api = OxiAPI(url=BASE)
with pytest.raises(OxiAPIError) as exc:
api.node("missing")
assert exc.value.status_code == 404
@responses.activate
def test_500_with_node_not_found_html_maps_to_404():
responses.get(
f"{BASE}/node/show/ghost.json",
status=500,
content_type="text/html",
body="<html><title>Oxidized::NodeNotFound</title></html>",
)
api = OxiAPI(url=BASE)
with pytest.raises(OxiAPIError) as exc:
api.node("ghost")
assert exc.value.status_code == 404
@responses.activate
def test_reload_returns_status_code():
responses.get(f"{BASE}/reload", status=200)
api = OxiAPI(url=BASE)
assert api.reload() == 200
@responses.activate
def test_unknown_model_raises_value_error():
data = {**NODE_DATA, "model": "unknown_vendor"}
responses.get(f"{BASE}/node/show/HQ.json", json=data)
responses.get(f"{BASE}/node/fetch/grp/HQ", body="whatever")
api = OxiAPI(url=BASE)
with pytest.raises(ValueError, match="not found in registry"):
_ = api.node("HQ").config

65
tests/test_units.py Normal file
View File

@@ -0,0 +1,65 @@
import pytest
from conftest import load
from oxi.exception import OxiAPIError
from oxi.interfaces import device_registry
from oxi.interfaces.base import BaseDevice
from oxi.interfaces.utils import decode_utf, expand_vlan_range
class TestExpandVlanRange:
@pytest.mark.parametrize("expand", [expand_vlan_range])
def test_simple_and_range(self, expand):
assert expand("1,7,14-15") == ["1", "7", "14", "15"]
@pytest.mark.parametrize("expand", [expand_vlan_range])
def test_reversed_range_is_normalized(self, expand):
assert expand("15-13") == ["13", "14", "15"]
@pytest.mark.parametrize("expand", [expand_vlan_range])
def test_non_numeric_range_kept_verbatim(self, expand):
assert expand("a-b") == ["a-b"]
@pytest.mark.parametrize("expand", [expand_vlan_range])
def test_empty(self, expand):
assert expand("") == []
@pytest.mark.parametrize("expand", [expand_vlan_range])
def test_list_input(self, expand):
assert expand(["1", "3-4"]) == ["1", "3", "4"]
class TestDecodeUtf:
def test_plain_text_passthrough(self):
assert decode_utf("Plain ASCII") == "Plain ASCII"
def test_escaped_utf8_is_decoded(self):
assert decode_utf(r'"\xd0\x94\xd0\xbe\xd0\xbc"') == "Дом"
class TestTemplateValidation:
def test_missing_required_group_raises(self):
class OnlySystem(BaseDevice):
template = "dummy.ttp"
def _load_template(self):
return '<group name="system"></group>'
with pytest.raises(ValueError, match="missing required groups"):
OnlySystem("data")
def test_missing_template_file_raises(self):
class NoTemplate(BaseDevice):
template = "does_not_exist.ttp"
with pytest.raises(FileNotFoundError):
NoTemplate("data")
class TestNodeNotFound:
def test_not_found_config_raises_on_parse(self):
device = device_registry["eltex"](load("eltex", "not_found.conf"), name="HQ")
assert device.raw is None
with pytest.raises(OxiAPIError) as exc:
device.parse()
assert exc.value.status_code == 404

72
tests/test_view.py Normal file
View File

@@ -0,0 +1,72 @@
import json
import pytest
from oxi.conf import ModelView
from oxi.interfaces.contract import Interfaces, System
@pytest.fixture
def system_view():
system = System(model="RB951", serial_number="ABC123", version="7.12")
return ModelView(system)
@pytest.fixture
def interfaces_view():
items = [
Interfaces(interface="eth0", ip_address="192.168.1.1", mask=24),
Interfaces(interface="eth1", description="uplink"),
]
return ModelView(items)
class TestSingleModelView:
def test_attribute_proxy(self, system_view):
assert system_view.model == "RB951"
assert system_view.serial_number == "ABC123"
def test_dump(self, system_view):
assert system_view.dump() == {
"model": "RB951",
"serial_number": "ABC123",
"version": "7.12",
}
def test_dump_json(self, system_view):
assert json.loads(system_view.dump_json())["model"] == "RB951"
def test_iter_raises(self, system_view):
with pytest.raises(TypeError):
iter(system_view)
def test_len_raises(self, system_view):
with pytest.raises(TypeError):
len(system_view)
def test_getitem_raises(self, system_view):
with pytest.raises(TypeError):
system_view[0]
class TestListModelView:
def test_len(self, interfaces_view):
assert len(interfaces_view) == 2
def test_iter(self, interfaces_view):
names = [iface.name for iface in interfaces_view]
assert names == ["eth0", "eth1"]
def test_getitem(self, interfaces_view):
assert interfaces_view[0].name == "eth0"
def test_slice(self, interfaces_view):
assert len(interfaces_view[:1]) == 1
def test_dump_uses_aliases(self, interfaces_view):
dumped = interfaces_view.dump()
assert dumped[0]["interface"] == "eth0"
def test_dump_json_keeps_unicode(self):
view = ModelView([Interfaces(interface="eth0", description="Дом")])
assert "Дом" in view.dump_json()

578
uv.lock generated Normal file
View File

@@ -0,0 +1,578 @@
version = 1
revision = 3
requires-python = ">=3.10"
[[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/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" },
{ url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" },
{ url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" },
{ url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" },
{ url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" },
{ url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" },
{ url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" },
{ url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" },
{ url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" },
{ url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" },
{ url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" },
{ url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" },
{ url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" },
{ url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" },
{ url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" },
{ url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" },
{ url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" },
{ url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" },
{ url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" },
{ 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 = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "exceptiongroup"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
]
[[package]]
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 = "iniconfig"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
[[package]]
name = "oxipy"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "pydantic" },
{ name = "requests" },
{ name = "ttp" },
]
[package.optional-dependencies]
dev = [
{ name = "pytest" },
{ name = "responses" },
]
[package.dev-dependencies]
dev = [
{ name = "pytest" },
{ name = "responses" },
{ name = "ruff" },
]
[package.metadata]
requires-dist = [
{ name = "pydantic", specifier = ">=2.12.5" },
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.3" },
{ name = "requests", specifier = ">=2.32.5" },
{ name = "responses", marker = "extra == 'dev'", specifier = ">=0.26.1" },
{ name = "ttp", specifier = ">=0.10.0" },
]
provides-extras = ["dev"]
[package.metadata.requires-dev]
dev = [
{ name = "pytest", specifier = ">=9.0.3" },
{ name = "responses", specifier = ">=0.26.1" },
{ name = "ruff", specifier = ">=0.15.17" },
]
[[package]]
name = "packaging"
version = "26.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
]
[[package]]
name = "pluggy"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]]
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/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" },
{ url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" },
{ url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" },
{ url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" },
{ url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" },
{ url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" },
{ url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" },
{ url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" },
{ url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" },
{ url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" },
{ url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" },
{ url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" },
{ url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" },
{ url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" },
{ url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" },
{ url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" },
{ 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/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" },
{ url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" },
{ url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" },
{ url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" },
{ url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" },
{ url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" },
{ url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" },
{ url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" },
{ url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" },
{ url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" },
{ url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" },
{ 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 = "pygments"
version = "2.20.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
]
[[package]]
name = "pytest"
version = "9.0.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "pygments" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
]
[[package]]
name = "pyyaml"
version = "6.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" },
{ url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" },
{ url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" },
{ url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" },
{ url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" },
{ url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" },
{ url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" },
{ url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" },
{ url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" },
{ url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
{ url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
{ url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
{ url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
{ url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
{ url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
{ url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
{ url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
{ url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
{ url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
{ url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
{ url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
{ url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
{ url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
{ url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
{ url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
{ url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
{ url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
{ url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
{ url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
{ url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
{ url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
{ url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
{ url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
{ url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
{ url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
{ url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
{ url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
{ url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
{ url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
{ url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
{ url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
{ url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
{ url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
{ url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
{ url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
]
[[package]]
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 = "responses"
version = "0.26.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyyaml" },
{ name = "requests" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c2/58/1fb6de3503428196df78638f991ec8095274f1ee9723e272ee4d9ff0092b/responses-0.26.1.tar.gz", hash = "sha256:2eb3218553cc8f79b57d257bac23af5e1bf381f5b9390b1767816f0843e01dc2", size = 83088, upload-time = "2026-05-21T19:56:39.747Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3a/31/6a620b4427d546b9e7cca8b3b8c5f0559d9cef2bb9eedcda7f73c1473c19/responses-0.26.1-py3-none-any.whl", hash = "sha256:8aacc4586eb08fb2208ef64a9eb4258d9b0c6e6f4260845f2f018ab847495345", size = 35502, upload-time = "2026-05-21T19:56:38.046Z" },
]
[[package]]
name = "ruff"
version = "0.15.17"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8c/a9/3abdf488f1bf3d24c699415e454ed554a6350d5d89ce183be1ee0a3361ac/ruff-0.15.17.tar.gz", hash = "sha256:2ec446937fd16c8c4de2674a209cc5af64d9c6f17d21fbf1151054fa0bcf5219", size = 4743346, upload-time = "2026-06-11T17:54:47.663Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/db/4d/e11259f5da07cb6afb2d074c31bf09da9671993f7329d4f15d2fdc458301/ruff-0.15.17-py3-none-linux_armv6l.whl", hash = "sha256:d9feddb927fc68bd295f5eebc587a7e42cfaf9b65f60ca4a2386febff575da8f", size = 10856677, upload-time = "2026-06-11T17:54:49.533Z" },
{ url = "https://files.pythonhosted.org/packages/29/3e/772d679e1a0dc058e58875bd2c0cb713a0530877b4a76fee3c7966df0d49/ruff-0.15.17-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:25805a226d741c47d274a35ad5c10a7dde175fcddfa511d7cf3da0a21eb3eab7", size = 11223443, upload-time = "2026-06-11T17:55:00.573Z" },
{ url = "https://files.pythonhosted.org/packages/68/58/bd41f7688b2fd5623012605130ed70e60aa7f2244baa3d5066bdd61530c8/ruff-0.15.17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f6ad73b14c2d18a3bf8ad7cb6974294d7f613a7898604826058e6ac64918ef4d", size = 10566458, upload-time = "2026-06-11T17:55:07.52Z" },
{ url = "https://files.pythonhosted.org/packages/d8/5b/733371013fcf1ec339e477ece6ab42bfe10bdd9bba8ee88a9516aa56bfc0/ruff-0.15.17-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ba0c1e4f95bcb3869d0d30cbd5917071ef2e28665abfec970cdab0492c713ed", size = 10914483, upload-time = "2026-06-11T17:55:05.501Z" },
{ url = "https://files.pythonhosted.org/packages/bd/cc/6f24251cc0252f7239391ccb85833f320efad14ebe5b443943f37ced6332/ruff-0.15.17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:81647960f10bff57d2e51cadd0c3950fe598400c852863a038720ef5b8cca91e", size = 10647497, upload-time = "2026-06-11T17:54:57.733Z" },
{ url = "https://files.pythonhosted.org/packages/68/dd/0d10c17ce1a1624d6fc3156309c3f834fdb5dfaad026ec90c85684f3990e/ruff-0.15.17-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e01a84ddbc8c16c23055ba3924476850f1bbc1917cebbb9376665a63e74260d", size = 11416967, upload-time = "2026-06-11T17:54:51.461Z" },
{ url = "https://files.pythonhosted.org/packages/2f/91/556bfb156f6144f355e831c23db00b2fc4120f86b3ce81cc5f7fd2df51f3/ruff-0.15.17-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fe9f653152f8f294f9f7e03bf3a453d8b4a27f7a59c78c8666167f2b17b96c", size = 12335770, upload-time = "2026-06-11T17:54:45.793Z" },
{ url = "https://files.pythonhosted.org/packages/88/82/8b5999aa13355e926f06d9f42a32dcca862f623bf0363785ff89d607dffd/ruff-0.15.17-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c0fe88a7676e7a05b73174d4d4a59cb2ac21ff8263583f87a81a6018475a978", size = 11575441, upload-time = "2026-06-11T17:54:32.661Z" },
{ url = "https://files.pythonhosted.org/packages/11/93/f10377bb04109ca0e8cbc483ff1982c54b6d418210041776f93e8cdc7fa9/ruff-0.15.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecfc3c7878fff94633ab0348524e093f9ce3243080416dd7d14f8ba400174719", size = 11557614, upload-time = "2026-06-11T17:54:34.698Z" },
{ url = "https://files.pythonhosted.org/packages/c7/a6/eeeae7f7d5493df41649ab3db92f086b2d0a30199e4efdf8e3dd7a033f24/ruff-0.15.17-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:b8461180b22420b1bdc289909410930761629fddf2a5aaf60fae1ab26cedc4c4", size = 11544450, upload-time = "2026-06-11T17:54:39.042Z" },
{ url = "https://files.pythonhosted.org/packages/32/88/5991ce565129a24dd4a00db1254b3b5db2e53018cbe4018ea5a89738e727/ruff-0.15.17-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6eccbe50a038b503e7140b441aa9c7fc8c1f36edf23ebef9f4165c2f28f568b7", size = 10892524, upload-time = "2026-06-11T17:55:09.432Z" },
{ url = "https://files.pythonhosted.org/packages/f5/1d/0fdd248313425f55223968af04b0a42125466a8d88d21c1d99c6af0a51e8/ruff-0.15.17-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:382fc0521025f5a8ad447d8bdd523545d0d7646adb718eb1c2dac5065ec27c0f", size = 10659573, upload-time = "2026-06-11T17:54:36.824Z" },
{ url = "https://files.pythonhosted.org/packages/9e/0e/072e8260deb9461062ce9311ced27a8e541229a6ffd483013dd37661e43e/ruff-0.15.17-py3-none-musllinux_1_2_i686.whl", hash = "sha256:456d41fcd1b2777ad63f09a6e7121d43f7b688bbc76a800c10f7f8fb1f912c3f", size = 11127818, upload-time = "2026-06-11T17:55:03.124Z" },
{ url = "https://files.pythonhosted.org/packages/ab/b4/55060a34163121498014696b5f656db5b8c6963768f227dbf0d76b311073/ruff-0.15.17-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b1a04bcc94ae6194e9db05d16ad31f298a7194bfbcb08258bbe589cee1d587b8", size = 11655901, upload-time = "2026-06-11T17:54:53.562Z" },
{ url = "https://files.pythonhosted.org/packages/49/71/9b29d6b87cef468d697f43c6a91e3fae4a80185779d7d5a4ef27d173439f/ruff-0.15.17-py3-none-win32.whl", hash = "sha256:596065960ab1ff593f744220c9fe6580eda00a95003cffa9f4048bb5b1bf0392", size = 10925574, upload-time = "2026-06-11T17:54:55.723Z" },
{ url = "https://files.pythonhosted.org/packages/3d/b2/8fc77f3723228836fa5d12497eb71c808f83782e10d058d2b15cfa14640b/ruff-0.15.17-py3-none-win_amd64.whl", hash = "sha256:6769e5fa1710b179b92e0bfa5a51735b35baea9013dadb06d5f44cbcf9547084", size = 12058788, upload-time = "2026-06-11T17:54:41.042Z" },
{ url = "https://files.pythonhosted.org/packages/2d/c7/c53e8dbff9c9dc4b7928773421ae294a5d28fcb8dcda1a089579d3a7e510/ruff-0.15.17-py3-none-win_arm64.whl", hash = "sha256:f3be1fbb34bcdfd146240d8fb92a709d4c2c8191348580a3c044ec60fa0b4456", size = 11355275, upload-time = "2026-06-11T17:54:43.635Z" },
]
[[package]]
name = "tomli"
version = "2.4.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" },
{ url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" },
{ url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" },
{ url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" },
{ url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" },
{ url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" },
{ url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" },
{ url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" },
{ url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" },
{ url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" },
{ url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" },
{ url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" },
{ url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" },
{ url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" },
{ url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" },
{ url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" },
{ url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" },
{ url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" },
{ url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" },
{ url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" },
{ url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" },
{ url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" },
{ url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" },
{ url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" },
{ url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" },
{ url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" },
{ url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" },
{ url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" },
{ url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" },
{ url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" },
{ url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" },
{ url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" },
{ url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" },
{ url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" },
{ url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" },
{ url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" },
{ url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" },
{ url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" },
{ url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" },
{ url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" },
{ url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" },
{ url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" },
{ url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" },
{ url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" },
{ url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" },
{ url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
]
[[package]]
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" },
]