5 Commits

Author SHA1 Message Date
53b79ce04f 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
2026-06-12 15:59:57 +03:00
IluaAir
074f2e9340 Update .gitignore and remove .python-version file
All checks were successful
CI / lint (pull_request) Successful in 45s
CI / test (3.10) (pull_request) Successful in 28s
CI / test (3.11) (pull_request) Successful in 42s
CI / test (3.12) (pull_request) Successful in 47s
CI / test (3.13) (pull_request) Successful in 39s
- Added .python-version to .gitignore to prevent tracking of Python version files.
- Deleted the .python-version file as it is no longer needed for the project.
2026-06-12 00:03:55 +03:00
IluaAir
dd7f117380 Add CI workflows for Gitea and GitHub
- Created a CI workflow for Gitea with linting and testing jobs using Python versions 3.10 to 3.13.
- Established a CI workflow for GitHub with similar linting and testing jobs, ensuring consistency across platforms.
- Both workflows utilize the `uv` tool for dependency management and testing, enhancing the project's CI/CD capabilities.
2026-06-11 23:57:11 +03:00
IluaAir
3c0e70b320 Update project configuration and dependencies
- Added `.vscode` to `.gitignore` to exclude Visual Studio Code settings.
- Updated `pyproject.toml` to include `ruff` for linting and configured its settings.
- Modified `uv.lock` to include `ruff` in both optional and development dependencies.
- Refactored type hints in several files to use `str | None` for optional parameters.
- Cleaned up unused imports and whitespace in various modules for improved code clarity.
2026-06-11 23:47:39 +03:00
IluaAir
c40cae1561 Update dependencies and clean up Huawei model code
- Updated `pytest` to version 9.0.3 and `responses` to version 0.26.1 in `pyproject.toml`.
- Removed the main execution block from the Huawei model for cleaner code structure.
2026-06-11 23:22:45 +03:00
23 changed files with 157 additions and 47 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

3
.gitignore vendored
View File

@@ -8,8 +8,9 @@ wheels/
main.py main.py
# Virtual environments # Virtual environments
.venv .venv
.python-version
.idea .idea
.DS_Store .DS_Store
.vscode
# etc files # etc files
*.txt *.txt

View File

@@ -1 +0,0 @@
3.13

View File

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

View File

@@ -1,4 +1,3 @@
from typing import Optional
from requests.adapters import HTTPAdapter from requests.adapters import HTTPAdapter
from urllib3.util import Retry from urllib3.util import Retry
@@ -6,7 +5,7 @@ from urllib3.util import Retry
class OxiAdapter(HTTPAdapter): class OxiAdapter(HTTPAdapter):
def __init__( def __init__(
self, self,
timeout: Optional[int] = None, timeout: int | None = None,
max_retries: int = 3, max_retries: int = 3,
*args, *args,
**kwargs, **kwargs,

View File

@@ -1,6 +1,7 @@
from functools import cached_property
import json import json
from typing import TYPE_CHECKING, Generic, Iterator, TypeVar from collections.abc import Iterator
from functools import cached_property
from typing import TYPE_CHECKING, Generic, TypeVar
from pydantic import BaseModel from pydantic import BaseModel

View File

@@ -1,8 +1,8 @@
from typing import Optional
from requests import HTTPError, Session from requests import HTTPError, Session
from oxi.adapter import OxiAdapter from oxi.adapter import OxiAdapter
from oxi.exception import OxiAPIError from oxi.exception import OxiAPIError
from .node import Node from .node import Node
@@ -10,8 +10,8 @@ class OxiAPI:
def __init__( def __init__(
self, self,
url: str, url: str,
username: Optional[str] = None, username: str | None = None,
password: Optional[str] = None, password: str | None = None,
verify: bool = True, verify: bool = True,
): ):
self.base_url = url.rstrip("/") self.base_url = url.rstrip("/")
@@ -20,8 +20,8 @@ class OxiAPI:
def __create_session( def __create_session(
self, self,
username: Optional[str] = None, username: str | None = None,
password: Optional[str] = None, password: str | None = None,
verify: bool = True, verify: bool = True,
) -> Session: ) -> Session:
session = Session() session = Session()

View File

@@ -1,4 +1,4 @@
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from requests import HTTPError from requests import HTTPError
@@ -35,7 +35,7 @@ def _looks_like_node_not_found_html(e: "HTTPError") -> bool:
class OxiAPIError(Exception): class OxiAPIError(Exception):
def __init__(self, message: str, status_code: Optional[int] = None): def __init__(self, message: str, status_code: int | None = None):
super().__init__(message) super().__init__(message)
self.status_code = status_code self.status_code = status_code
self.message = message self.message = message

View File

@@ -1,4 +1,4 @@
from typing import Callable, Type from collections.abc import Callable
from .base import BaseDevice from .base import BaseDevice
@@ -7,7 +7,7 @@ device_registry = {}
def register_parser( def register_parser(
name: list[str] | str, name: list[str] | str,
) -> Callable[[Type[BaseDevice]], Type[BaseDevice]]: ) -> Callable[[type[BaseDevice]], type[BaseDevice]]:
def wrapper(cls): def wrapper(cls):
name_list = [] name_list = []
if isinstance(name, str): if isinstance(name, str):

View File

@@ -1,10 +1,11 @@
import xml.etree.ElementTree as ET
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from pathlib import Path from pathlib import Path
from ttp import ttp from ttp import ttp
from oxi.exception import OxiAPIError from oxi.exception import OxiAPIError
from oxi.interfaces.contract import Device from oxi.interfaces.contract import Device, Interfaces, System, Vlans
import xml.etree.ElementTree as ET
from oxi.interfaces.contract import Interfaces, System, Vlans
class BaseDevice(ABC): class BaseDevice(ABC):
@@ -40,7 +41,7 @@ class BaseDevice(ABC):
Raises: Raises:
ValueError: if raw data cannot be validated by the contract. ValueError: if raw data cannot be validated by the contract.
""" """ # noqa: E501
return self.raw.get("vlans", []) return self.raw.get("vlans", [])
def interfaces(self) -> list[dict]: def interfaces(self) -> list[dict]:
@@ -52,7 +53,7 @@ class BaseDevice(ABC):
Raises: Raises:
ValueError: if raw data cannot be validated by the contract. ValueError: if raw data cannot be validated by the contract.
""" """ # noqa: E501
return self.raw.get("interfaces", []) return self.raw.get("interfaces", [])
def system(self) -> dict: def system(self) -> dict:
@@ -82,11 +83,7 @@ class BaseDevice(ABC):
def _validate_contract(self) -> dict: def _validate_contract(self) -> dict:
if self.raw is None: if self.raw is None:
msg = ( msg = f"Node {self.name} not found" if self.name else "Node not found"
f"Node {self.name} not found"
if self.name
else "Node not found"
)
raise OxiAPIError(msg, status_code=404) raise OxiAPIError(msg, status_code=404)
system_data = self.system() system_data = self.system()
interfaces_data = self._as_list(self.interfaces()) interfaces_data = self._as_list(self.interfaces())
@@ -99,8 +96,8 @@ class BaseDevice(ABC):
if "vlans" in self._declared_sections: if "vlans" in self._declared_sections:
if "vlans" not in self.raw: if "vlans" not in self.raw:
raise ValueError( raise ValueError(
f"{self.__class__.__name__}: template '{self.template}' declares optional group " f"{self.__class__.__name__}: template '{self.template}' "
f"'vlans', but TTP did not return it." f"declares optional group 'vlans', but TTP did not return it."
) )
vlans_data = self._as_list(self.vlans()) vlans_data = self._as_list(self.vlans())
result["vlans"] = [Vlans(**item) for item in vlans_data] result["vlans"] = [Vlans(**item) for item in vlans_data]

View File

@@ -1,4 +1,5 @@
from ipaddress import IPv4Address from ipaddress import IPv4Address
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field

View File

@@ -3,5 +3,5 @@ import pkgutil
package = __package__ package = __package__
for loader, module_name, is_pkg in pkgutil.iter_modules(__path__): for _, module_name, _ in pkgutil.iter_modules(__path__):
importlib.import_module(f"{package}.{module_name}") importlib.import_module(f"{package}.{module_name}")

View File

@@ -14,4 +14,4 @@ class H3C(BaseDevice):
vlans.append(item) vlans.append(item)
continue continue
vlans.extend({"vlan_id": vlan_id} for vlan_id in vlan_ids) vlans.extend({"vlan_id": vlan_id} for vlan_id in vlan_ids)
return vlans return vlans

View File

@@ -9,10 +9,3 @@ class Huawei(BaseDevice):
def vlans(self) -> list[dict]: def vlans(self) -> list[dict]:
vlan_ids = self.raw.get("vlans", {}).get("vlan_ids", []) vlan_ids = self.raw.get("vlans", {}).get("vlan_ids", [])
return [{"vlan_id": vlan} for vlan in vlan_ids] return [{"vlan_id": vlan} for vlan in vlan_ids]
if __name__ == "__main__":
with open("./test4.txt") as file:
data = file.read()
huawei = Huawei(data)
print(huawei.parse())

View File

@@ -1,4 +1,5 @@
from ipaddress import ip_interface from ipaddress import ip_interface
from oxi.interfaces import register_parser from oxi.interfaces import register_parser
from oxi.interfaces.base import BaseDevice from oxi.interfaces.base import BaseDevice
from oxi.interfaces.utils import decode_utf from oxi.interfaces.utils import decode_utf

View File

@@ -6,7 +6,6 @@ from oxi.exception import OxiAPIError
from .view import NodeView from .view import NodeView
if TYPE_CHECKING: if TYPE_CHECKING:
from requests import Session from requests import Session

View File

@@ -3,7 +3,6 @@ from typing import TYPE_CHECKING
from .conf import NodeConfig from .conf import NodeConfig
if TYPE_CHECKING: if TYPE_CHECKING:
from requests import Session from requests import Session

View File

@@ -22,8 +22,8 @@ dependencies = [
[project.optional-dependencies] [project.optional-dependencies]
dev = [ dev = [
"pytest>=8", "pytest>=9.0.3",
"responses>=0.25", "responses>=0.26.1",
] ]
[project.urls] [project.urls]
@@ -47,4 +47,12 @@ include-package-data = true
dev = [ dev = [
"pytest>=9.0.3", "pytest>=9.0.3",
"responses>=0.26.1", "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"]

View File

@@ -1,8 +1,8 @@
import json import json
import pytest import pytest
from conftest import FIXTURES, load from conftest import FIXTURES, load
from oxi.interfaces import device_registry from oxi.interfaces import device_registry
MODEL_CASES = [ MODEL_CASES = [

View File

@@ -1,7 +1,7 @@
import pytest import pytest
import responses import responses
from conftest import load from conftest import load
from oxi import OxiAPI from oxi import OxiAPI
from oxi.exception import OxiAPIError from oxi.exception import OxiAPIError
@@ -84,4 +84,4 @@ def test_unknown_model_raises_value_error():
api = OxiAPI(url=BASE) api = OxiAPI(url=BASE)
with pytest.raises(ValueError, match="not found in registry"): with pytest.raises(ValueError, match="not found in registry"):
api.node("HQ").config _ = api.node("HQ").config

View File

@@ -1,10 +1,9 @@
import pytest import pytest
from conftest import load from conftest import load
from oxi.exception import OxiAPIError from oxi.exception import OxiAPIError
from oxi.interfaces import device_registry from oxi.interfaces import device_registry
from oxi.interfaces.base import BaseDevice from oxi.interfaces.base import BaseDevice
from oxi.interfaces.contract import Interfaces, System
from oxi.interfaces.utils import decode_utf, expand_vlan_range from oxi.interfaces.utils import decode_utf, expand_vlan_range

38
uv.lock generated
View File

@@ -158,23 +158,34 @@ dependencies = [
{ name = "ttp" }, { name = "ttp" },
] ]
[package.dev-dependencies] [package.optional-dependencies]
dev = [ dev = [
{ name = "pytest" }, { name = "pytest" },
{ name = "responses" }, { name = "responses" },
] ]
[package.dev-dependencies]
dev = [
{ name = "pytest" },
{ name = "responses" },
{ name = "ruff" },
]
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "pydantic", specifier = ">=2.12.5" }, { name = "pydantic", specifier = ">=2.12.5" },
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.3" },
{ name = "requests", specifier = ">=2.32.5" }, { name = "requests", specifier = ">=2.32.5" },
{ name = "responses", marker = "extra == 'dev'", specifier = ">=0.26.1" },
{ name = "ttp", specifier = ">=0.10.0" }, { name = "ttp", specifier = ">=0.10.0" },
] ]
provides-extras = ["dev"]
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [ dev = [
{ name = "pytest", specifier = ">=9.0.3" }, { name = "pytest", specifier = ">=9.0.3" },
{ name = "responses", specifier = ">=0.26.1" }, { name = "responses", specifier = ">=0.26.1" },
{ name = "ruff", specifier = ">=0.15.17" },
] ]
[[package]] [[package]]
@@ -448,6 +459,31 @@ 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" }, { 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]] [[package]]
name = "tomli" name = "tomli"
version = "2.4.1" version = "2.4.1"