Compare commits

...

29 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
40 changed files with 625 additions and 121 deletions

13
.gitignore vendored
View File

@@ -3,3 +3,16 @@ __pycache__
*.egg-info
.python-version
.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'},
)
@@ -48,6 +48,10 @@ def ad(base_config: AppDaemonConfig):
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,16 +3,14 @@ import logging
import re
from pathlib import Path
import food.menu
import pytest
from appdaemon.appdaemon import AppDaemon
from git import Repo
from . import utils
from .utils import (
count_error_lines,
get_app_orders,
get_load_order,
get_loaded_apps,
reset_file,
)
@@ -20,6 +18,8 @@ INDENT = ' ' * 4
def test_file(ad: AppDaemon, caplog: pytest.LogCaptureFixture, config_repo: Repo):
import food.menu
module_file = Path(food.menu.__file__)
file_content = module_file.read_text()
@@ -40,7 +40,11 @@ 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):
def test_file_with_error(
ad: AppDaemon, caplog: pytest.LogCaptureFixture, config_repo: Repo
):
import food.menu
module_file = Path(food.menu.__file__)
file_content = module_file.read_text().splitlines()
@@ -61,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,7 +1,7 @@
import asyncio
import re
from pathlib import Path
from typing import List
from typing import Generator, List, Literal
import pytest
from appdaemon.appdaemon import AppDaemon
@@ -13,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):
@@ -39,7 +63,9 @@ 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)
@@ -47,3 +73,11 @@ 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" },
]