Compare commits

..

35 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
John Lancaster
81da8be655 added another test for the child being modified 2024-08-14 00:36:05 -05:00
John Lancaster
a0add54445 refactored to use new function 2024-08-14 00:35:55 -05:00
John Lancaster
6c50657e5b added some utils functions 2024-08-14 00:35:41 -05:00
John Lancaster
7bb5d1debe removed change 2024-08-14 00:12:33 -05:00
John Lancaster
4547668c2b added family folder 2024-08-14 00:11:53 -05:00
40 changed files with 696 additions and 115 deletions

13
.gitignore vendored
View File

@@ -3,3 +3,16 @@ __pycache__
*.egg-info *.egg-info
.python-version .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,7 +0,0 @@
hello_world:
module: hello
class: HelloWorld
simple_app:
module: simple
class: SimpleApp

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

@@ -0,0 +1,40 @@
child_app:
module: child
class: Child
dependencies:
- parent_app
sibling_app:
module: sibling
class: Sibling
dependencies:
- parent_app
parent_app:
module: parent
class: Parent
dependencies:
- grand-parent_app
grand-parent_app:
module: grand_parent
class: GrandParent
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

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

View File

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

View File

@@ -0,0 +1,6 @@
from appdaemon.adapi import ADAPI
class Parent(ADAPI):
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 dataclasses import dataclass, field
from typing import List
from .base import FoodItem from .base import FoodItem
from .eggs import Eggs from .eggs import Eggs
@@ -12,7 +11,7 @@ class GreenEggs(Eggs):
@dataclass @dataclass
class Menu: class Menu:
dishes: List[FoodItem] = field(init=False) dishes: list[type[FoodItem]] = field(init=False)
def __post_init__(self): def __post_init__(self):
self.dishes = [GreenEggs, Ham] 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): class Restaurant(ADAPI):
menu: Menu menu: Menu
def initialize(self): def initialize(self) -> None:
self.log(f'{self.__class__.__name__} initialized') self.log(f'{self.__class__.__name__} initialized')
self.menu = Menu() 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): class SimpleApp(Hass):
def initialize(self): def initialize(self) -> None:
self.log(f"{self.__class__.__name__} Initialized") 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 = [ dependencies = [
"pytest>=8.3.2", "pytest>=8.3.2",
"gitpython>=3.1.43", "gitpython>=3.1.43",
"appdaemon",
] ]
readme = "README.md" readme = "README.md"
requires-python = ">= 3.8" requires-python = ">= 3.10"
[build-system] [build-system]
requires = ["hatchling"] requires = ["hatchling"]
build-backend = "hatchling.build" build-backend = "hatchling.build"
[tool.rye] [tool.ruff.format]
managed = true quote-style = "single"
dev-dependencies = []
[tool.hatch.metadata] [tool.hatch.metadata]
allow-direct-references = true allow-direct-references = true
[tool.hatch.build.targets.wheel] [tool.hatch.build.targets.wheel]
packages = ["src/ad_test"] 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 import pytest
from appdaemon.appdaemon import AppDaemon from appdaemon.appdaemon import AppDaemon
from appdaemon.logging import Logging from appdaemon.logs import Logging
from appdaemon.models.config import AppDaemonConfig from appdaemon.models.config import AppDaemonConfig
from git import Repo from git import Repo
from pytest import Function from pytest import Function
@@ -38,7 +38,7 @@ def base_config() -> AppDaemonConfig:
time_zone='America/Chicago', time_zone='America/Chicago',
config_dir=CONFIG_DIR, config_dir=CONFIG_DIR,
config_file='appdaemon.yaml', 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) 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(): for cfg in ad.logging.config.values():
logger = logging.getLogger(cfg['name']) logger = logging.getLogger(cfg['name'])
logger.propagate = True logger.propagate = True

View File

@@ -3,38 +3,29 @@ import logging
import re import re
from pathlib import Path from pathlib import Path
import food.menu
import pytest import pytest
from appdaemon.appdaemon import AppDaemon from appdaemon.appdaemon import AppDaemon
from git import Repo from git import Repo
from .utils import get_load_order 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): def test_file(ad: AppDaemon, caplog: pytest.LogCaptureFixture, config_repo: Repo):
assert isinstance(ad, AppDaemon) import food.menu
module_file = Path(food.menu.__file__) 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: try:
with caplog.at_level(logging.DEBUG, logger='AppDaemon._app_management'): with caplog.at_level(logging.DEBUG, logger='AppDaemon._app_management'):
@@ -49,11 +40,16 @@ def test_file(ad: AppDaemon, caplog: pytest.LogCaptureFixture, config_repo: Repo
reset_file(config_repo, module_file) reset_file(config_repo, module_file)
def test_file_with_error(ad: AppDaemon, caplog: pytest.LogCaptureFixture, config_repo: Repo): def test_file_with_error(
assert isinstance(ad, AppDaemon) ad: AppDaemon, caplog: pytest.LogCaptureFixture, config_repo: Repo
):
import food.menu
module_file = Path(food.menu.__file__) 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: try:
with caplog.at_level(logging.DEBUG): with caplog.at_level(logging.DEBUG):
@@ -67,3 +63,40 @@ def test_file_with_error(ad: AppDaemon, caplog: pytest.LogCaptureFixture, config
assert "Started 'my_restaurant'" in caplog.text assert "Started 'my_restaurant'" in caplog.text
finally: finally:
reset_file(config_repo, module_file) reset_file(config_repo, module_file)
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'
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 "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 import pytest
from appdaemon.appdaemon import AppDaemon from appdaemon.appdaemon import AppDaemon
from .utils import get_load_order from .utils import count_error_lines, get_load_order
def validate_app_dependencies(ad: AppDaemon): def validate_app_dependencies(ad: AppDaemon):
@@ -41,8 +41,7 @@ def test_startup(ad: AppDaemon, caplog: pytest.LogCaptureFixture):
with caplog.at_level(logging.DEBUG): with caplog.at_level(logging.DEBUG):
ad.loop.run_until_complete(asyncio.sleep(2.0)) ad.loop.run_until_complete(asyncio.sleep(2.0))
error_log_lines = [msg for name, lvl, msg in caplog.record_tuples if name.startswith('Error')] assert count_error_lines(caplog) == 0
assert len(error_log_lines) == 0
assert "Started 'hello_world'" in caplog.text assert "Started 'hello_world'" in caplog.text
assert 'App initialization complete' in caplog.text assert 'App initialization complete' in caplog.text

View File

@@ -1,8 +1,11 @@
import asyncio import asyncio
from typing import List import re
from pathlib import Path
from typing import Generator, List, Literal
import pytest import pytest
from appdaemon.appdaemon import AppDaemon from appdaemon.appdaemon import AppDaemon
from git import Repo
async def delayed_stop(ad: AppDaemon, delay: float): async def delayed_stop(ad: AppDaemon, delay: float):
@@ -10,8 +13,71 @@ async def delayed_stop(ad: AppDaemon, delay: float):
ad.stop() ad.stop()
def get_load_order(caplog: pytest.LogCaptureFixture) -> List[str]: 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, logger_name: str):
for record in caplog.records: for record in caplog.records:
if record.name == 'AppDaemon._app_management': if record.name == logger_name:
if 'Determined module load order' in record.msg: yield record
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):
for record in get_logger_records(caplog, 'AppDaemon._app_management'):
if re.search('Apps to be loaded', record.msg):
return record.args[0]
def count_error_lines(caplog: pytest.LogCaptureFixture) -> int:
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" },
]