Compare commits

...

30 Commits

Author SHA1 Message Date
John Lancaster
7ba1abec84 pyright config 2025-06-20 08:50:54 -05:00
John Lancaster
475bdb9dd9 simple fixes 2025-06-20 08:50:43 -05:00
John Lancaster
ba043554dc linting 2025-06-20 08:30:46 -05:00
John Lancaster
bd5c7ee339 updated ruff rules 2025-06-20 08:29:28 -05:00
John Lancaster
ce4ede51b1 removed hello stub 2025-06-20 08:21:28 -05:00
John Lancaster
edee038a9f added ruff.toml 2025-06-20 08:21:01 -05:00
John Lancaster
7025bcde99 changed to uv 2025-06-20 08:20:50 -05:00
John Lancaster
ad34a6e919 cleanup 2025-06-20 08:20:26 -05:00
John Lancaster
99936f2c85 cleaned up simple 2025-06-20 08:20:19 -05:00
John Lancaster
4655b93b64 added malformed 2025-06-05 22:38:16 -05:00
John Lancaster
cbd91c8873 added some more examples 2025-06-05 22:37:56 -05:00
John Lancaster
5fca821637 types 2025-05-26 16:37:33 -05:00
John Lancaster
c5cb766c73 updates 2025-05-26 16:34:55 -05:00
John Lancaster
4aa8a3a4a9 added kitchen sequence 2025-04-06 19:21:03 -05:00
John Lancaster
68d34a31b1 removed globals from import string 2025-02-03 23:11:48 -06:00
John Lancaster
e2e83ed579 added sequences 2025-02-03 23:11:26 -06:00
John Lancaster
2c7fd70945 submodule update 2025-02-03 23:10:33 -06:00
John Lancaster
2382e2c44b added another module 2025-02-03 23:05:51 -06:00
John Lancaster
3e82297db2 added submodule 2025-02-02 20:21:30 -06:00
John Lancaster
727bfbac1d rearranged a bit 2025-01-28 22:39:12 -06:00
John Lancaster
161219540c removed prev lockfiles 2025-01-28 17:53:53 -06:00
John Lancaster
3ae41ed4fb convert to uv lock file 2025-01-28 17:53:40 -06:00
John Lancaster
370ed2805b added some complexity 2025-01-28 17:49:44 -06:00
John Lancaster
fe9a713fe2 added apps to simulate problem 2025-01-28 17:28:03 -06:00
John Lancaster
7a8e6f383a test updates 2024-09-04 23:50:57 -05:00
John Lancaster
b673bda1f2 formatting 2024-09-04 23:50:39 -05:00
John Lancaster
6c3ea5084b reworked family dependencies 2024-09-04 21:02:44 -05:00
John Lancaster
346d0a95d8 added to gitignore 2024-09-04 20:05:54 -05:00
John Lancaster
14609ec8c4 added sibling 2024-09-04 20:05:00 -05:00
John Lancaster
6e793baed8 removed single use functions 2024-08-14 00:38:33 -05:00
40 changed files with 645 additions and 144 deletions

15
.gitignore vendored
View File

@@ -2,4 +2,17 @@ __pycache__
.pytest_cache
*.egg-info
.python-version
.venv
.venv
conf/compiled
conf/dashboards
conf/namespaces
conf/www
conf/appdaemon.yaml
conf/secrets.yaml
log
*.log
*.db
*.js
*cache*

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "conf/apps/appdir_test"]
path = conf/apps/appdir_test
url = https://github.com/AppDaemon/appdir_test

17
conf/ad_logging.yaml Normal file
View File

@@ -0,0 +1,17 @@
main_log:
# filename: /home/john/conf/ad-test/conf/log/appdaemon.log
log_generations: 5
log_size: 1024
level: DEBUG
# level: INFO
access_log:
filename: /home/john/conf/ad-test/conf/log/access.log
error_log:
filename: /home/john/conf/ad-test/conf/log/error.log
diag_log:
filename: /home/john/conf/ad-test/conf/log/diag.log
format: "{asctime} {levelname:<8} {appname:<10}: {message}"
test_log:
name: TestLog
filename: /home/john/conf/ad-test/conf/log/test.log

1
conf/apps/appdir_test Submodule

Submodule conf/apps/appdir_test added at bcefcfe7cc

View File

@@ -1,9 +0,0 @@
hello_world:
module: hello
class: HelloWorld
simple_app:
module: simple
class: SimpleApp
dependencies:
- hello_world

View File

@@ -0,0 +1,11 @@
rules:
module: rules
global: true
statemachine:
module: statemachine
global: true
hal:
module: hal
global: true

View File

@@ -0,0 +1,7 @@
import logging
logger = logging.getLogger('AppDaemon.Perimeter')
logger.info('Imported statemachine')
class StateMachine: ...

View File

@@ -0,0 +1,12 @@
from dataclasses import dataclass
@dataclass
class Rule1:
state: str
@dataclass
class Rule2:
value: int
other: str = 'default' # test changing this

View File

@@ -1,15 +1,40 @@
child:
child_app:
module: child
class: Child
dependencies:
- parent_app
parent:
sibling_app:
module: sibling
class: Sibling
dependencies:
- parent_app
parent_app:
module: parent
class: Parent
dependencies:
- child
- grand-parent_app
grand-parent:
grand-parent_app:
module: grand_parent
class: GrandParent
dependencies:
- parent
sequence:
setup_tv:
name: Setup TV
namespace: hass
steps:
- homeassistant/turn_on:
entity_id: switch.living_room_tv
- sleep: 30
- remote/send_command:
entity_id: roku.living_room
loop_step:
times: 5
interval: 0.5

View File

@@ -1,5 +1,6 @@
from appdaemon.adapi import ADAPI
class Child(ADAPI):
def initialize(self):
self.log(f"{self.__class__.__name__} Initialized")
self.log(f'{self.__class__.__name__} Initialized')

View File

@@ -1,5 +1,6 @@
from appdaemon.adapi import ADAPI
class GrandParent(ADAPI):
def initialize(self):
self.log(f"{self.__class__.__name__} Initialized")
def initialize(self) -> None:
self.log(f'{self.__class__.__name__} Initialized')

View File

@@ -1,5 +1,6 @@
from appdaemon.adapi import ADAPI
class Parent(ADAPI):
def initialize(self):
self.log(f"{self.__class__.__name__} Initialized")
def initialize(self) -> None:
self.log(f'{self.__class__.__name__} Initialized')

View File

@@ -0,0 +1,6 @@
from child import Child
class Sibling(Child):
def initialize(self) -> None:
self.log(f'{self.__class__.__name__} Initialized')

View File

View File

@@ -1,5 +1,4 @@
from dataclasses import dataclass, field
from typing import List
from .base import FoodItem
from .eggs import Eggs
@@ -12,7 +11,7 @@ class GreenEggs(Eggs):
@dataclass
class Menu:
dishes: List[FoodItem] = field(init=False)
dishes: list[type[FoodItem]] = field(init=False)
def __post_init__(self):
self.dishes = [GreenEggs, Ham]

View File

@@ -0,0 +1,17 @@
from enum import Enum, auto
class Utensil(Enum):
KNIFE = auto()
FORK = auto()
SPOON = auto()
PLATE = auto()
BOWL = auto()
CUP = auto()
GLASS = auto()
MUG = auto()
POT = auto()
PAN = auto()
GLOBAL_VAR = Utensil.FORK

View File

@@ -0,0 +1,6 @@
from enum import Enum, auto
class Food(Enum):
PIZZA = auto()
BURGER = auto()

View File

@@ -5,7 +5,7 @@ from food.menu import Menu
class Restaurant(ADAPI):
menu: Menu
def initialize(self):
def initialize(self) -> None:
self.log(f'{self.__class__.__name__} initialized')
self.menu = Menu()

28
conf/apps/globals.py Normal file
View File

@@ -0,0 +1,28 @@
import logging
from enum import Enum
LOGGER = logging.getLogger('AppDaemon._globals')
GLOBAL_VAR = 'Hello, World!'
def global_function() -> None:
LOGGER.info('This is a global function.')
class GlobalClass:
def __init__(self) -> None:
self.value = 'This is a global class instance.'
def display(self) -> None:
LOGGER.info(self.value)
class ModeSelect(Enum):
MODE_A = 'mode_a'
MODE_B = 'mode_b'
MODE_C = 'mode_c'
GLOBAL_MODE = ModeSelect.MODE_C

11
conf/apps/globals/hal.py Normal file
View File

@@ -0,0 +1,11 @@
import logging
from typing import Any
logger = logging.getLogger('AppDaemon.Perimeter')
class HAL:
def __init__(self, *args: Any, **kwargs: Any) -> None:
self.args = args
self.kwargs = kwargs
logger.info('Logging from HAL')

View File

@@ -0,0 +1,9 @@
CONSTANTS = {
'A': 1,
'B': 2,
'C': 3,
}
def utility_function():
return 123, 456

View File

@@ -1,6 +0,0 @@
from appdaemon.adapi import ADAPI
class HelloWorld(ADAPI):
def initialize(self):
self.log(f"{self.__class__.__name__} Initialized")

View File

@@ -0,0 +1,12 @@
from appdaemon.adapi import ADAPI
from globals import GLOBAL_MODE, GLOBAL_VAR
class AppA(ADAPI):
def initialize(self) -> None:
self.log(f'{self.__class__.__name__} Initialized')
self.log(GLOBAL_VAR)
self.log(f'Global mode is set to: {GLOBAL_MODE.value}')
def terminate(self) -> None: ...

View File

@@ -0,0 +1,11 @@
from appdaemon.adapi import ADAPI
from globals import GLOBAL_MODE, GLOBAL_VAR
class AppB(ADAPI):
def initialize(self) -> None:
self.log(f'{self.__class__.__name__} Initialized')
self.log(GLOBAL_VAR)
self.log(f'Global mode is set to: {GLOBAL_MODE.value}')
def terminate(self) -> None: ...

View File

@@ -0,0 +1,27 @@
hello_world:
module: hello
class: HelloWorld
extra: abc123
# disable: true
simple_app:
module: simple
class: SimpleApp
extra: 1234
priority: 10
# dependencies:
# - hello_world
base_app:
module: simple
class: BaseApp
# AppA:
# module: app_a
# class: AppA
# dependencies:
# - AppB # This is only set to demonstrate forcing it to load after AppB
# AppB:
# module: app_b
# class: AppB

View File

@@ -0,0 +1,24 @@
from typing import Any
from appdaemon.adapi import ADAPI
# fake_name.get()
# if:
# OUtisde&sdf'asdfasdf'<txF> asdfasdfom some html</.dstd>
# fake/
# SimpleApp
class HelloWorld(ADAPI):
def initialize(self) -> None:
self.log(f'{self.__class__.__name__} Initialized')
self.log('+' * 50)
# fake
self.register_service('my_domain/my_exciting_service', self.my_exciting_cb)
def my_exciting_cb(self, *args: str, my_arg: int = 0, **kwargs: Any) -> Any:
namespace, domain, service = args
self.log(f'Service {domain}/{service} in the {namespace} namepsace called with {kwargs}')
return 999 + my_arg

View File

@@ -0,0 +1,9 @@
from appdaemon.adapi import ADAPI
class SimpleApp(ADAPI):
def initialize(self):
self.log(f'{self.__class__.__name__} Initialized')
if:
OUtisde&sdf'asdfasdf'<txF> asdfasdfom some html</.dstd>

View File

@@ -1,6 +1,21 @@
from appdaemon.adapi import ADAPI
from appdaemon import utils
from appdaemon.adbase import ADBase
from appdaemon.plugins.hass import Hass
class SimpleApp(ADAPI):
def initialize(self):
self.log(f"{self.__class__.__name__} Initialized")
class SimpleApp(Hass):
def initialize(self) -> None:
match self.ping():
case float() as ping:
ping = utils.format_timedelta(ping)
self.log(f'{self.__class__.__name__} Initialized: {ping}')
case _:
pass
class BaseApp(ADBase):
def initialize(self) -> None:
self.adapi = self.get_ad_api()
self.log = self.adapi.log
self.hassapi = self.get_plugin_api('HASS')
assert isinstance(self.hassapi, Hass), 'HASS API not available'

29
conf/pyrightconfig.json Normal file
View File

@@ -0,0 +1,29 @@
{
// Specify which paths to check
"include": [
"./ad-test/conf/apps/**"
],
// "typeCheckingMode": "standard",
"typeCheckingMode": "basic",
// Be somewhat tolerant with regards to imperfect dependencies or types
"reportUnknownMemberType": "none",
"reportUnusedImport": "warning",
"reportMissingTypeStubs": "none",
"reportIncompleteStub": "none",
"reportUnknownVariableType": "none",
"reportUnknownParameterType": "warning",
"reportMissingTypeArgument": "warning",
"useLibraryCodeForTypes": true, // Homogeneous with pylance's default
// Add warnings for probable programming errors
"reportUnnecessaryTypeIgnoreComment": "warning",
"reportMissingParameterType": "warning",
"reportUnnecessaryComparison": "warning",
"reportUnnecessaryIsInstance": "warning",
"reportUnnecessaryCast": "warning",
"reportUnnecessaryContains": "warning",
"reportMatchNotExhaustive": "error",
"reportUnusedVariable": "warning",
"reportUnusedCoroutine": "warning",
"reportUnusedExpression": "warning",
"reportUnusedFunction": "warning"
}

View File

@@ -8,20 +8,28 @@ authors = [
dependencies = [
"pytest>=8.3.2",
"gitpython>=3.1.43",
"appdaemon",
]
readme = "README.md"
requires-python = ">= 3.8"
requires-python = ">= 3.10"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.rye]
managed = true
dev-dependencies = []
[tool.ruff.format]
quote-style = "single"
[tool.hatch.metadata]
allow-direct-references = true
[tool.hatch.build.targets.wheel]
packages = ["src/ad_test"]
[tool.uv.sources]
appdaemon = {path = "/home/john/Documents/appdaemon" }
[dependency-groups]
dev = [
"ruff>=0.11.13",
]

View File

@@ -1,26 +0,0 @@
# generated by rye
# use `rye lock` or `rye sync` to update this lockfile
#
# last locked with the following flags:
# pre: false
# features: []
# all-features: false
# with-sources: false
# generate-hashes: false
# universal: false
-e file:.
gitdb==4.0.11
# via gitpython
gitpython==3.1.43
# via ad-test
iniconfig==2.0.0
# via pytest
packaging==24.1
# via pytest
pluggy==1.5.0
# via pytest
pytest==8.3.2
# via ad-test
smmap==5.0.1
# via gitdb

View File

@@ -1,26 +0,0 @@
# generated by rye
# use `rye lock` or `rye sync` to update this lockfile
#
# last locked with the following flags:
# pre: false
# features: []
# all-features: false
# with-sources: false
# generate-hashes: false
# universal: false
-e file:.
gitdb==4.0.11
# via gitpython
gitpython==3.1.43
# via ad-test
iniconfig==2.0.0
# via pytest
packaging==24.1
# via pytest
pluggy==1.5.0
# via pytest
pytest==8.3.2
# via ad-test
smmap==5.0.1
# via gitdb

29
ruff.toml Normal file
View File

@@ -0,0 +1,29 @@
line-length = 100
target-version = "py312"
[format]
quote-style = "single"
indent-style = "space"
[lint]
exclude = [
"conf/apps/simple_app/malformed.py"
]
select = ["ALL"]
extend-ignore = [
"ANN",
"BLE001",
"COM812",
"D",
"E501",
"ERA001",
"INP001",
"S101",
]
[lint.flake8-quotes]
inline-quotes = "single"
docstring-quotes = "double"
multiline-quotes = "double"

View File

@@ -1,2 +0,0 @@
def hello() -> str:
return "Hello from ad-test!"

View File

@@ -5,7 +5,7 @@ from typing import List
import pytest
from appdaemon.appdaemon import AppDaemon
from appdaemon.logging import Logging
from appdaemon.logs import Logging
from appdaemon.models.config import AppDaemonConfig
from git import Repo
from pytest import Function
@@ -38,7 +38,7 @@ def base_config() -> AppDaemonConfig:
time_zone='America/Chicago',
config_dir=CONFIG_DIR,
config_file='appdaemon.yaml',
module_debug={'_app_management': 'DEBUG'}
module_debug={'_app_management': 'DEBUG'},
)
@@ -47,7 +47,11 @@ def ad(base_config: AppDaemonConfig):
loop = asyncio.new_event_loop()
ad = AppDaemon(Logging(), loop, base_config)
import sys
sys.path.insert(0, (CONFIG_DIR / 'apps/food-repo/src').as_posix())
for cfg in ad.logging.config.values():
logger = logging.getLogger(cfg['name'])
logger.propagate = True

View File

@@ -3,41 +3,29 @@ import logging
import re
from pathlib import Path
# import child
import food.menu
import pytest
from appdaemon.appdaemon import AppDaemon
from git import Repo
from .utils import count_error_lines, get_app_orders, get_load_order, get_loaded_apps
from . import utils
from .utils import (
get_app_orders,
get_load_order,
reset_file,
)
INDENT = ' ' * 4
def reset_file(repo: Repo, changed: Path):
if not changed.is_absolute():
changed = Path(repo.working_tree_dir) / changed
repo.git.checkout('HEAD', '--', changed)
def modify_file(path: Path):
file_content = path.read_text()
modified_content = re.sub(r'Ham', r'Spam', file_content, flags=re.MULTILINE)
modified_content = re.sub(r'ham', r'spam', modified_content, flags=re.MULTILINE)
path.write_text(modified_content)
def insert_import_error(path: Path):
file_content = path.read_text().splitlines()
file_content.insert(0, 'raise ImportError')
path.write_text('\n'.join(file_content))
def test_file(ad: AppDaemon, caplog: pytest.LogCaptureFixture, config_repo: Repo):
assert isinstance(ad, AppDaemon)
import food.menu
module_file = Path(food.menu.__file__)
modify_file(module_file)
file_content = module_file.read_text()
modified_content = re.sub(r'Ham', r'Spam', file_content, flags=re.MULTILINE)
modified_content = re.sub(r'ham', r'spam', modified_content, flags=re.MULTILINE)
module_file.write_text(modified_content)
try:
with caplog.at_level(logging.DEBUG, logger='AppDaemon._app_management'):
@@ -52,11 +40,16 @@ def test_file(ad: AppDaemon, caplog: pytest.LogCaptureFixture, config_repo: Repo
reset_file(config_repo, module_file)
def test_file_with_error(ad: AppDaemon, caplog: pytest.LogCaptureFixture, config_repo: Repo):
assert isinstance(ad, AppDaemon)
def test_file_with_error(
ad: AppDaemon, caplog: pytest.LogCaptureFixture, config_repo: Repo
):
import food.menu
module_file = Path(food.menu.__file__)
insert_import_error(module_file)
file_content = module_file.read_text().splitlines()
file_content.insert(0, 'raise ImportError')
module_file.write_text('\n'.join(file_content))
try:
with caplog.at_level(logging.DEBUG):
@@ -72,23 +65,38 @@ def test_file_with_error(ad: AppDaemon, caplog: pytest.LogCaptureFixture, config
reset_file(config_repo, module_file)
def test_modification_child(ad: AppDaemon, caplog: pytest.LogCaptureFixture, config_repo: Repo):
def test_modification_child(
ad: AppDaemon, caplog: pytest.LogCaptureFixture, config_repo: Repo
):
assert isinstance(ad, AppDaemon)
ad.loop.run_until_complete(asyncio.sleep(2.5))
module_file = Path(__file__).parents[2] / 'conf/apps/family/child.py'
file_content = module_file.read_text()
file_content += (INDENT * 2) + "self.log(f'Modified {self.__class__.__name__}')\n"
module_file.write_text(file_content)
utils.inject_into_file(module_file, 2, 'raise ImportError')
try:
with caplog.at_level(logging.DEBUG):
ad.loop.run_until_complete(asyncio.sleep(1.0))
assert count_error_lines(caplog) == 0
assert get_loaded_apps(caplog) == {'child'}
assert get_app_orders(caplog, 'stop') == ['child', 'parent', 'grand-parent']
assert get_app_orders(caplog, 'start') == ['child', 'parent', 'grand-parent']
assert "Error importing 'child'" in caplog.text
load_order = utils.get_load_order(caplog)
assert load_order == ['child', 'sibling'] # sibling is a variant of child
stopping = get_app_orders(caplog, 'stop', ordered=False)
assert stopping is not None, 'Failed to get app stop order'
assert stopping == {'child_app', 'sibling_app'}
# start_order = get_app_orders(caplog, 'start')
# assert start_order is None
# assert start_order == ['child', 'parent', 'grand-parent'], f'Wrong app start order: {start_order}'
finally:
reset_file(config_repo, module_file)
ad.loop.run_until_complete(asyncio.sleep(1.0))
start_order = get_app_orders(caplog, 'start', ordered=False)
assert start_order == {
'child_app',
'sibling_app',
}, f'Wrong app start order: {start_order}'

View File

@@ -5,7 +5,7 @@ from typing import List
import pytest
from appdaemon.appdaemon import AppDaemon
from .utils import get_load_order, count_error_lines
from .utils import count_error_lines, get_load_order
def validate_app_dependencies(ad: AppDaemon):

View File

@@ -1,9 +1,11 @@
import asyncio
import re
from typing import List
from pathlib import Path
from typing import Generator, List, Literal
import pytest
from appdaemon.appdaemon import AppDaemon
from git import Repo
async def delayed_stop(ad: AppDaemon, delay: float):
@@ -11,23 +13,47 @@ async def delayed_stop(ad: AppDaemon, delay: float):
ad.stop()
def get_load_order(caplog: pytest.LogCaptureFixture) -> List[str]:
for record in caplog.records:
if record.name == 'AppDaemon._app_management':
if 'Determined module load order' in record.msg:
return record.args[0]
def get_load_order(
caplog: pytest.LogCaptureFixture, ordered: bool = True
) -> Generator[str, None, None]:
records = (
record.args[0]
for record in get_logger_records(caplog, 'AppDaemon._app_management')
if 'Determined module load order' in record.msg
)
try:
result = list(records)[-1]
if not ordered:
result = set(result)
return result
except Exception:
return
def get_logger_records(caplog: pytest.LogCaptureFixture, name: str):
def get_logger_records(caplog: pytest.LogCaptureFixture, logger_name: str):
for record in caplog.records:
if record.name == name:
if record.name == logger_name:
yield record
def get_app_orders(caplog: pytest.LogCaptureFixture, phase: str) -> List[str]:
for record in get_logger_records(caplog, 'AppDaemon._app_management'):
if re.search(f'App {phase} order', record.msg):
return record.args[0]
def get_app_orders(
caplog: pytest.LogCaptureFixture,
phase: Literal['start', 'stop'],
ordered: bool = True,
) -> list[str]:
"""Extracts the app start/stop order from the captured log lines"""
records = (
record.args[0]
for record in get_logger_records(caplog, 'AppDaemon._app_management')
if re.search(f'App {phase} order', record.msg)
)
try:
result = list(records)[-1]
if not ordered:
result = set(result)
return result
except Exception:
return
def get_loaded_apps(caplog: pytest.LogCaptureFixture):
@@ -37,5 +63,21 @@ def get_loaded_apps(caplog: pytest.LogCaptureFixture):
def count_error_lines(caplog: pytest.LogCaptureFixture) -> int:
error_log_lines = [msg for name, lvl, msg in caplog.record_tuples if name.startswith('Error')]
error_log_lines = [
msg for name, lvl, msg in caplog.record_tuples if name.startswith('Error')
]
return len(error_log_lines)
def reset_file(repo: Repo, changed: Path):
if not changed.is_absolute():
changed = Path(repo.working_tree_dir) / changed
repo.git.checkout('HEAD', '--', changed)
def inject_into_file(file: Path, pos: int, line: str):
content = file.read_text()
lines = content.splitlines()
lines.insert(pos, line)
new_content = '\n'.join(lines)
file.write_text(new_content)

184
uv.lock generated Normal file
View File

@@ -0,0 +1,184 @@
version = 1
revision = 2
requires-python = ">=3.8"
resolution-markers = [
"python_full_version >= '3.9'",
"python_full_version < '3.9'",
]
[[package]]
name = "ad-test"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "gitpython" },
{ name = "pytest" },
]
[package.metadata]
requires-dist = [
{ name = "gitpython", specifier = ">=3.1.43" },
{ name = "pytest", specifier = ">=8.3.2" },
]
[[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.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" },
]
[[package]]
name = "gitdb"
version = "4.0.12"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "smmap" },
]
sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" },
]
[[package]]
name = "gitpython"
version = "3.1.44"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "gitdb" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c0/89/37df0b71473153574a5cdef8f242de422a0f5d26d7a9e231e6f169b4ad14/gitpython-3.1.44.tar.gz", hash = "sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269", size = 214196, upload-time = "2025-01-02T07:32:43.59Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/9a/4114a9057db2f1462d5c8f8390ab7383925fe1ac012eaa42402ad65c2963/GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110", size = 207599, upload-time = "2025-01-02T07:32:40.731Z" },
]
[[package]]
name = "iniconfig"
version = "2.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
]
[[package]]
name = "packaging"
version = "25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
]
[[package]]
name = "pluggy"
version = "1.5.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.9'",
]
sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" },
]
[[package]]
name = "pluggy"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.9'",
]
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 = "pytest"
version = "8.3.5"
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", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
{ name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" },
]
[[package]]
name = "smmap"
version = "5.0.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" },
]
[[package]]
name = "tomli"
version = "2.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" },
{ url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" },
{ url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" },
{ url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" },
{ url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" },
{ url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" },
{ url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" },
{ url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" },
{ url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" },
{ url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" },
{ url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" },
{ url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" },
{ url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" },
{ url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" },
{ url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" },
{ url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" },
{ url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" },
{ url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" },
{ url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" },
{ url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" },
{ url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" },
{ url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" },
{ url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" },
{ url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" },
{ url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" },
{ url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" },
{ url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" },
{ url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" },
{ url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" },
{ url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" },
{ url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" },
]
[[package]]
name = "typing-extensions"
version = "4.13.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" },
]