22 Commits

Author SHA1 Message Date
kolo de7972c14f update 2026-03-17 19:48:33 +03:00
kolo 688dec6591 new command info 2026-03-17 10:28:26 +03:00
kolo 8d68cdc40d update 2026-03-16 14:23:09 +03:00
kolo 2785779583 new command routes 2026-03-15 19:56:28 +03:00
kolo 0d8871a719 support module paths and __main__.py in entrypoint resolver 2026-03-15 19:17:16 +03:00
kolo 5eece75c40 benchs 2026-03-15 19:09:02 +03:00
kolo a3d7630219 cli module better 2026-03-15 18:06:45 +03:00
kolo 7ffc6cd987 new changelog fragment 2026-03-13 17:42:54 +03:00
kolo db94cc8c9e complete entrypoint resolver refactor 2026-03-13 17:29:14 +03:00
kolo b9b83540e2 perf boooooooooooooost 2026-03-13 16:50:38 +03:00
kolo 1cd5c3759e update 2026-03-13 12:17:32 +03:00
kolo 44f7b42302 update 2026-03-12 15:32:10 +03:00
kolo b2f5a1b163 new diagrams in benchmarks 2026-02-13 14:42:59 +03:00
kolo 1023d05419 Merge branch 'main' into cli 2026-02-12 14:19:50 +03:00
kolo 732a4456b7 ruff format 2026-02-12 14:18:53 +03:00
kolo de6d35205c rename orchestrator method start_polling to run_repl 2026-02-10 14:03:37 +03:00
kolo 6ed1d35e8a new command new) 2026-02-10 13:41:51 +03:00
kolo 18a8376469 add new init command for generate boilerplate in flat or src layout 2026-02-09 14:50:03 +03:00
kolo 1211518c40 fix ovelapping with argparser 2026-02-08 22:47:32 +03:00
kolo 70f1327a0d complete creating run command 2026-02-08 22:37:42 +03:00
kolo b732036e87 cli module creating 2026-02-08 19:23:15 +03:00
kolo e9dd7af905 start creating cli module 2026-02-07 13:31:23 +03:00
57 changed files with 1810 additions and 786 deletions
+4
View File
@@ -1,6 +1,9 @@
#### joe made this: http://goel.io/joe #### joe made this: http://goel.io/joe
metrics/reports/diagrams metrics/reports/diagrams
*.dist
*build
*.exe
#### python #### #### python ####
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
@@ -322,3 +325,4 @@ http-client.private.env.json
.idea/ApifoxUploaderProjectSetting.xml .idea/ApifoxUploaderProjectSetting.xml
.zed .zed
test.py
@@ -0,0 +1,35 @@
<!--
A new scriv changelog fragment.
Uncomment the section that is right (remove the HTML comment wrapper).
For top level release notes, leave all the headers commented out.
-->
### Added
- A cli module that implements the ability to launch applications on Argenta, run application benchmarks on Argenta, create a boilerplate for new projects, and much more.
- A new `info` command has been added to the Argenta CLI, providing a quick overview of the installed package and runtime environment.
### Changed
- Refactoring the initialization order of some modules; heavy imports are now imported only when necessary, which resulted in a boost to importtime.
<!--
### Deprecated
- A bullet item for the Deprecated category.
-->
<!--
### Removed
- A bullet item for the Removed category.
-->
<!--
### Fixed
- A bullet item for the Fixed category.
-->
+2 -2
View File
@@ -12,6 +12,6 @@ orchestrator = Orchestrator(
if __name__ == "__main__": if __name__ == "__main__":
if arg_parser.parsed_argspace.get_by_name("dev"): if arg_parser.parsed_argspace.get_by_name("dev"):
orchestrator.start_polling(App(initial_message="ArgentaDev")) orchestrator.run_repl(App(initial_message="ArgentaDev"))
else: else:
orchestrator.start_polling(App()) orchestrator.run_repl(App())
+1 -1
View File
@@ -22,7 +22,7 @@ def main():
print(f" Host: {host.value}") print(f" Host: {host.value}")
print(f" Port: {port.value}") print(f" Port: {port.value}")
orchestrator.start_polling(app) orchestrator.run_repl(app)
if __name__ == "__main__": if __name__ == "__main__":
+1 -1
View File
@@ -24,4 +24,4 @@ orchestrator = Orchestrator(custom_providers=[ConnectionProvider()])
# 4. Start the application # 4. Start the application
if __name__ == "__main__": if __name__ == "__main__":
orchestrator.start_polling(app) orchestrator.run_repl(app)
@@ -60,7 +60,7 @@ orchestrator = Orchestrator()
def main(): def main():
app.include_router(router) app.include_router(router)
orchestrator.start_polling(app) orchestrator.run_repl(app)
if __name__ == "__main__": if __name__ == "__main__":
+1 -1
View File
@@ -9,7 +9,7 @@ orchestrator: Orchestrator = Orchestrator()
def main() -> None: def main() -> None:
app.include_router(router) app.include_router(router)
orchestrator.start_polling(app) orchestrator.run_repl(app)
if __name__ == "__main__": if __name__ == "__main__":
+1 -1
View File
@@ -30,4 +30,4 @@ app.include_router(main_router)
# 5. Start application # 5. Start application
if __name__ == "__main__": if __name__ == "__main__":
orchestrator.start_polling(app) orchestrator.run_repl(app)
@@ -15,4 +15,4 @@ app.include_router(router)
# 3. Start polling via orchestrator # 3. Start polling via orchestrator
if __name__ == "__main__": if __name__ == "__main__":
orchestrator.start_polling(app) orchestrator.run_repl(app)
+1 -1
View File
@@ -25,7 +25,7 @@ def test_input_incorrect_command(capsys: CaptureFixture[str]):
app.set_unknown_command_handler(lambda command: print(f"Unknown command: {command.trigger}")) app.set_unknown_command_handler(lambda command: print(f"Unknown command: {command.trigger}"))
with patch("builtins.input", side_effect=["help", "q"]): with patch("builtins.input", side_effect=["help", "q"]):
orchestrator.start_polling(app) orchestrator.run_repl(app)
output = capsys.readouterr().out output = capsys.readouterr().out
assert "\nUnknown command: help\n" in output assert "\nUnknown command: help\n" in output
+1 -1
View File
@@ -159,7 +159,7 @@ PredefinedMessages
app.add_message_on_startup(PredefinedMessages.AUTOCOMPLETE) app.add_message_on_startup(PredefinedMessages.AUTOCOMPLETE)
app.add_message_on_startup(PredefinedMessages.HELP) app.add_message_on_startup(PredefinedMessages.HELP)
orchestrator.start_polling(app) orchestrator.run_repl(app)
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+1 -1
View File
@@ -36,7 +36,7 @@ Orchestrator
Основные методы Основные методы
---------------- ----------------
.. py:method:: start_polling(self, app: App) -> None .. py:method:: run_repl(self, app: App) -> None
Это главный метод, который запускает приложение. Он запускает бесконечный цикл ввода -> вывода. Это главный метод, который запускает приложение. Он запускает бесконечный цикл ввода -> вывода.
+1 -1
View File
@@ -45,7 +45,7 @@
E2E-тестирование цикла E2E-тестирование цикла
---------------------- ----------------------
Полный запуск цикла ``start_polling`` можно покрывать через подпроцесс с передачей строк в ``stdin``. Это тяжелее и обычно не требуется. Если всё же необходимо — пример ниже. Полный запуск цикла ``run_repl`` можно покрывать через подпроцесс с передачей строк в ``stdin``. Это тяжелее и обычно не требуется. Если всё же необходимо — пример ниже.
.. danger:: .. danger::
**Важно:** Обязательно передавайте строковый триггер команды выхода последним элементом в списке ``side_effects`` при патче ``input``. **Важно:** Обязательно передавайте строковый триггер команды выхода последним элементом в списке ``side_effects`` при патче ``input``.
+29 -8
View File
@@ -1,36 +1,57 @@
set windows-shell := ["powershell.exe", "-NoLogo", "-Command"] set windows-shell := ["powershell.exe", "-NoLogo", "-Command"]
set shell := ["bash", "-c"] set shell := ["bash", "-c"]
# Вывести список всех рецептов # List all available recipes
default: default:
@just --list @just --list
# Запустить тесты через pytest # ── Testing ───────────────────────────────────────────────────────────────────
# Run tests via pytest
tests: tests:
python -m pytest tests python -m pytest tests
# Запустить тесты с отчетом о покрытии # Run tests with coverage report
tests-cov: tests-cov:
python -m pytest --cov=argenta tests python -m pytest --cov=argenta tests
# Запустить тесты с отчетом о покрытии с html репортом # Run tests with coverage HTML report
tests-cov-html: tests-cov-html:
python -m pytest --cov=argenta tests --cov-report=html python -m pytest --cov=argenta tests --cov-report=html
# Отформатировать код (Ruff + isort) # ── Code quality ──────────────────────────────────────────────────────────────
# Format code (Ruff + isort)
format: format:
python -m ruff format ./src python -m ruff format ./src
python -m isort ./src python -m isort ./src
# Проверить типы через mypy (strict) # Check types via mypy (strict)
mypy: mypy:
python -m mypy -p argenta --strict python -m mypy -p argenta --strict
# Проверить стиль через wemake-python-styleguide # Check style via wemake-python-styleguide
wps: wps:
python -m flake8 --format=wemake ./src python -m flake8 --format=wemake ./src
# Запустить линтер Ruff # Run Ruff linter
ruff: ruff:
python -m ruff check ./src python -m ruff check ./src
# Run all checks (format, mypy, ruff, wps)
check-format: format mypy ruff wps
# ── Changelog (scriv) ─────────────────────────────────────────────────────────
# Create a new changelog fragment and open it in $EDITOR
frag:
if (-not (Test-Path "./changelog.d")) { New-Item -ItemType Directory -Path "./changelog.d" }
scriv create --add
# Preview collected changelog without writing anything
changelog-preview:
scriv collect --dry-run
# Collect fragments into CHANGELOG.md for release (usage: just release 1.2.3)
release version:
scriv collect --version {{ version }} --add
+6 -6
View File
@@ -1,18 +1,18 @@
from argenta import App, Orchestrator, Command from argenta import App, Command, Orchestrator
from argenta.app import DynamicDividingLine
from .handlers import router from .handlers import router
app = App(initial_message="metrics", exit_command=Command("exit", aliases=["quit"]))
app = App(initial_message="metrics", exit_command=Command('exit', aliases=['quit'])) app.include_router(router)
orchestrator = Orchestrator() orchestrator = Orchestrator()
def main() -> None: def main() -> None:
app.include_router(router)
app.set_description_message_pattern( app.set_description_message_pattern(
lambda command, description: f'[bold cyan]▸[/bold cyan] [bold white]{command}[/bold white] [dim]│[/dim] [yellow italic]{description}[/yellow italic]' lambda command, description: f"[bold cyan]▸[/bold cyan] [bold white]{command}[/bold white] [dim]│[/dim] [yellow italic]{description}[/yellow italic]"
) )
orchestrator.start_polling(app) orchestrator.run_repl(app)
if __name__ == "__main__": if __name__ == "__main__":
+2
View File
@@ -13,8 +13,10 @@ class BenchmarksNotFound(Exception):
def __str__(self) -> str: def __str__(self) -> str:
return f"Benchmarks with type '{self.type_}' not found" return f"Benchmarks with type '{self.type_}' not found"
class BenchmarksWithSameNameAlreadyExists(Exception): class BenchmarksWithSameNameAlreadyExists(Exception):
def __init__(self, benchmark_name: str): def __init__(self, benchmark_name: str):
self.benchmark_name = benchmark_name self.benchmark_name = benchmark_name
def __str__(self) -> str: def __str__(self) -> str:
return f"Benchmarks with name '{self.benchmark_name}' already exists" return f"Benchmarks with name '{self.benchmark_name}' already exists"
+25 -30
View File
@@ -1,16 +1,11 @@
__all__ = [ __all__ = ["Benchmark", "Benchmarks", "BenchmarkResult", "BenchmarkGroupResult"]
"Benchmark",
"Benchmarks",
"BenchmarkResult",
"BenchmarkGroupResult"
]
import gc
import io import io
import statistics
import time
from contextlib import redirect_stdout from contextlib import redirect_stdout
from dataclasses import dataclass from dataclasses import dataclass
import time
import gc
import statistics
from typing import Callable, override from typing import Callable, override
from .exceptions import BenchmarkNotFound, BenchmarksNotFound, BenchmarksWithSameNameAlreadyExists from .exceptions import BenchmarkNotFound, BenchmarksNotFound, BenchmarksWithSameNameAlreadyExists
@@ -40,14 +35,7 @@ class BenchmarkGroupResult:
class Benchmark: class Benchmark:
def __init__( def __init__(self, func: FuncForBenchmark, *, type_: str, name: str, description: str) -> None:
self,
func: FuncForBenchmark,
*,
type_: str,
name: str,
description: str
) -> None:
self.func = func self.func = func
self.type_ = type_ self.type_ = type_
self.name = name self.name = name
@@ -78,11 +66,11 @@ class Benchmark:
@override @override
def __repr__(self) -> str: def __repr__(self) -> str:
return f'Benchmark<{self.type_=}, {self.name=}, {self.description=}>' return f"Benchmark<{self.type_=}, {self.name=}, {self.description=}>"
@override @override
def __str__(self) -> str: def __str__(self) -> str:
return f'benchmark {self.name} with type {self.type_}' return f"benchmark {self.name} with type {self.type_}"
class Benchmarks: class Benchmarks:
@@ -92,16 +80,14 @@ class Benchmarks:
self._benchmarks_paired_by_name: dict[str, Benchmark] = {} self._benchmarks_paired_by_name: dict[str, Benchmark] = {}
def register( def register(
self, self, type_: str, description: str = ""
type_: str,
description: str = ""
) -> Callable[[FuncForBenchmark], FuncForBenchmark]: ) -> Callable[[FuncForBenchmark], FuncForBenchmark]:
def decorator(func: FuncForBenchmark) -> FuncForBenchmark: def decorator(func: FuncForBenchmark) -> FuncForBenchmark:
benchmark = Benchmark( benchmark = Benchmark(
func, func,
type_=type_, type_=type_,
name=func.__name__, name=func.__name__,
description=description or f'description for {func.__name__} with type {type_}', description=description or f"description for {func.__name__} with type {type_}",
) )
if self._benchmarks_paired_by_name.get(func.__name__): if self._benchmarks_paired_by_name.get(func.__name__):
raise BenchmarksWithSameNameAlreadyExists(func.__name__) raise BenchmarksWithSameNameAlreadyExists(func.__name__)
@@ -110,9 +96,12 @@ class Benchmarks:
self._benchmarks.append(benchmark) self._benchmarks.append(benchmark)
self._benchmarks_grouped_by_type.setdefault(type_, []).append(benchmark) self._benchmarks_grouped_by_type.setdefault(type_, []).append(benchmark)
return func return func
return decorator return decorator
def run_benchmark_by_name(self, name: str, iterations: int = 100, is_gc_disables: bool = False) -> BenchmarkResult: def run_benchmark_by_name(
self, name: str, iterations: int = 100, is_gc_disables: bool = False
) -> BenchmarkResult:
benchmark = self.get_benchmark_by_name(name) benchmark = self.get_benchmark_by_name(name)
if not benchmark: if not benchmark:
raise BenchmarkNotFound(name) raise BenchmarkNotFound(name)
@@ -130,28 +119,34 @@ class Benchmarks:
is_gc_disabled=is_gc_disables, is_gc_disabled=is_gc_disables,
avg_time=avg, avg_time=avg,
median_time=median, median_time=median,
std_dev=std_dev std_dev=std_dev,
) )
def run_benchmarks_by_type(self, type_: str, iterations: int = 100, is_gc_disabled: bool = False) -> BenchmarkGroupResult: def run_benchmarks_by_type(
self, type_: str, iterations: int = 100, is_gc_disabled: bool = False
) -> BenchmarkGroupResult:
benchmarks = self.get_benchmarks_by_type(type_) benchmarks = self.get_benchmarks_by_type(type_)
if not benchmarks: if not benchmarks:
raise BenchmarksNotFound(type_) raise BenchmarksNotFound(type_)
benchmark_results: list[BenchmarkResult] = [] benchmark_results: list[BenchmarkResult] = []
for benchmark in benchmarks: for benchmark in benchmarks:
benchmark_results.append(self.run_benchmark_by_name(benchmark.name, iterations, is_gc_disabled)) benchmark_results.append(
self.run_benchmark_by_name(benchmark.name, iterations, is_gc_disabled)
)
return BenchmarkGroupResult( return BenchmarkGroupResult(
type_=type_, type_=type_,
iterations=iterations, iterations=iterations,
is_gc_disabled=is_gc_disabled, is_gc_disabled=is_gc_disabled,
benchmark_results=benchmark_results benchmark_results=benchmark_results,
) )
def run_benchmarks_grouped_by_type(self, iterations: int = 100, is_gc_disabled: bool = False) -> list[BenchmarkGroupResult]: def run_benchmarks_grouped_by_type(
self, iterations: int = 100, is_gc_disabled: bool = False
) -> list[BenchmarkGroupResult]:
results: list[BenchmarkGroupResult] = [] results: list[BenchmarkGroupResult] = []
for type_, benchmarks in self._benchmarks_grouped_by_type.items(): for type_, _ in self._benchmarks_grouped_by_type.items():
results.append(self.run_benchmarks_by_type(type_, iterations, is_gc_disabled)) results.append(self.run_benchmarks_by_type(type_, iterations, is_gc_disabled))
return results return results
+21 -16
View File
@@ -3,11 +3,11 @@ __all__ = [
"benchmark_command_with_flags", "benchmark_command_with_flags",
"benchmark_many_commands", "benchmark_many_commands",
"benchmark_command_with_many_flags", "benchmark_command_with_many_flags",
"benchmark_extreme_router" "benchmark_extreme_router",
] ]
from argenta.command.models import Command, InputCommand
from argenta.command import Flag, Flags from argenta.command import Flag, Flags
from argenta.command.models import Command, InputCommand
from argenta.response import Response from argenta.response import Response
from argenta.router import Router from argenta.router import Router
@@ -18,11 +18,11 @@ from .entity import benchmarks
def benchmark_simple_command() -> None: def benchmark_simple_command() -> None:
router = Router() router = Router()
@router.command(Command('test')) @router.command(Command("test"))
def handler(_res: Response) -> None: def handler(_res: Response) -> None:
pass pass
input_cmd = InputCommand.parse('test') input_cmd = InputCommand.parse("test")
router.finds_appropriate_handler(input_cmd) router.finds_appropriate_handler(input_cmd)
@@ -30,11 +30,11 @@ def benchmark_simple_command() -> None:
def benchmark_command_with_flags() -> None: def benchmark_command_with_flags() -> None:
router = Router() router = Router()
@router.command(Command('test', flags=Flags([Flag('a'), Flag('b'), Flag('c')]))) @router.command(Command("test", flags=Flags([Flag("a"), Flag("b"), Flag("c")])))
def handler(_res: Response) -> None: def handler(_res: Response) -> None:
pass pass
input_cmd = InputCommand.parse('test -a -b -c') input_cmd = InputCommand.parse("test -a -b -c")
router.finds_appropriate_handler(input_cmd) router.finds_appropriate_handler(input_cmd)
@@ -43,38 +43,43 @@ def benchmark_many_commands() -> None:
router = Router() router = Router()
for i in range(50): for i in range(50):
@router.command(Command(f'cmd{i}'))
@router.command(Command(f"cmd{i}"))
def handler(_res: Response) -> None: def handler(_res: Response) -> None:
pass pass
input_cmd = InputCommand.parse('cmd25') input_cmd = InputCommand.parse("cmd25")
router.finds_appropriate_handler(input_cmd) router.finds_appropriate_handler(input_cmd)
@benchmarks.register(type_="finds_appropriate_handler", description="Command with many flags (20 flags)") @benchmarks.register(
type_="finds_appropriate_handler", description="Command with many flags (20 flags)"
)
def benchmark_command_with_many_flags() -> None: def benchmark_command_with_many_flags() -> None:
router = Router() router = Router()
flags = Flags([Flag(f'flag{i}') for i in range(20)]) flags = Flags([Flag(f"flag{i}") for i in range(20)])
@router.command(Command('test', flags=flags)) @router.command(Command("test", flags=flags))
def handler(_res: Response) -> None: def handler(_res: Response) -> None:
pass pass
input_cmd = InputCommand.parse('test ' + ' '.join(f'-flag{i}' for i in range(10))) input_cmd = InputCommand.parse("test " + " ".join(f"-flag{i}" for i in range(10)))
router.finds_appropriate_handler(input_cmd) router.finds_appropriate_handler(input_cmd)
@benchmarks.register(type_="finds_appropriate_handler", description="Extreme (100 commands, 10 flags each)") @benchmarks.register(
type_="finds_appropriate_handler", description="Extreme (100 commands, 10 flags each)"
)
def benchmark_extreme_router() -> None: def benchmark_extreme_router() -> None:
router = Router() router = Router()
for i in range(100): for i in range(100):
flags = Flags([Flag(f'f{i}_{j}') for j in range(10)]) flags = Flags([Flag(f"f{i}_{j}") for j in range(10)])
@router.command(Command(f'cmd{i}', flags=flags)) @router.command(Command(f"cmd{i}", flags=flags))
def handler(_res: Response) -> None: def handler(_res: Response) -> None:
pass pass
input_cmd = InputCommand.parse('cmd50 -f50_0 -f50_1 -f50_2') input_cmd = InputCommand.parse("cmd50 -f50_0 -f50_1 -f50_2")
router.finds_appropriate_handler(input_cmd) router.finds_appropriate_handler(input_cmd)
+10 -26
View File
@@ -7,7 +7,7 @@ __all__ = [
"benchmark_validate_regex_complex", "benchmark_validate_regex_complex",
"benchmark_validate_multiple_flags_10", "benchmark_validate_multiple_flags_10",
"benchmark_validate_multiple_flags_50", "benchmark_validate_multiple_flags_50",
"benchmark_validate_extreme_100_flags" "benchmark_validate_extreme_100_flags",
] ]
import re import re
@@ -58,14 +58,8 @@ def benchmark_validate_regex_complex() -> None:
@benchmarks.register(type_="flag_validation", description="Multiple flags validation (10 flags)") @benchmarks.register(type_="flag_validation", description="Multiple flags validation (10 flags)")
def benchmark_validate_multiple_flags_10() -> None: def benchmark_validate_multiple_flags_10() -> None:
flags = [ flags = [Flag(f"flag{i}", possible_values=PossibleValues.ALL) for i in range(10)]
Flag(f"flag{i}", possible_values=PossibleValues.ALL) input_flags = [InputFlag(f"flag{i}", input_value=f"value{i}") for i in range(10)]
for i in range(10)
]
input_flags = [
InputFlag(f"flag{i}", input_value=f"value{i}")
for i in range(10)
]
for flag, input_flag in zip(flags, input_flags): for flag, input_flag in zip(flags, input_flags):
flag.validate_input_flag_value(input_flag.input_value) flag.validate_input_flag_value(input_flag.input_value)
@@ -73,30 +67,20 @@ def benchmark_validate_multiple_flags_10() -> None:
@benchmarks.register(type_="flag_validation", description="Multiple flags validation (50 flags)") @benchmarks.register(type_="flag_validation", description="Multiple flags validation (50 flags)")
def benchmark_validate_multiple_flags_50() -> None: def benchmark_validate_multiple_flags_50() -> None:
flags = [ flags = [Flag(f"flag{i}", possible_values=PossibleValues.ALL) for i in range(50)]
Flag(f"flag{i}", possible_values=PossibleValues.ALL) input_flags = [InputFlag(f"flag{i}", input_value=f"value{i}") for i in range(50)]
for i in range(50)
]
input_flags = [
InputFlag(f"flag{i}", input_value=f"value{i}")
for i in range(50)
]
for flag, input_flag in zip(flags, input_flags): for flag, input_flag in zip(flags, input_flags):
flag.validate_input_flag_value(input_flag.input_value) flag.validate_input_flag_value(input_flag.input_value)
@benchmarks.register(type_="flag_validation", description="Extreme (100 flags with regex validation)") @benchmarks.register(
type_="flag_validation", description="Extreme (100 flags with regex validation)"
)
def benchmark_validate_extreme_100_flags() -> None: def benchmark_validate_extreme_100_flags() -> None:
pattern = re.compile(r"^[a-zA-Z0-9_-]+$") pattern = re.compile(r"^[a-zA-Z0-9_-]+$")
flags = [ flags = [Flag(f"flag{i}", possible_values=pattern) for i in range(100)]
Flag(f"flag{i}", possible_values=pattern) input_flags = [InputFlag(f"flag{i}", input_value=f"valid_value_{i}") for i in range(100)]
for i in range(100)
]
input_flags = [
InputFlag(f"flag{i}", input_value=f"valid_value_{i}")
for i in range(100)
]
for flag, input_flag in zip(flags, input_flags): for flag, input_flag in zip(flags, input_flags):
flag.validate_input_flag_value(input_flag.input_value) flag.validate_input_flag_value(input_flag.input_value)
+10 -4
View File
@@ -5,7 +5,7 @@ __all__ = [
"benchmark_command_with_mixed_prefixes", "benchmark_command_with_mixed_prefixes",
"benchmark_command_with_long_values", "benchmark_command_with_long_values",
"benchmark_command_with_quoted_values", "benchmark_command_with_quoted_values",
"benchmark_extreme_many_flags" "benchmark_extreme_many_flags",
] ]
from argenta.command.models import InputCommand from argenta.command.models import InputCommand
@@ -23,12 +23,16 @@ def benchmark_command_with_few_flags() -> None:
InputCommand.parse("start -a -b -c") InputCommand.parse("start -a -b -c")
@benchmarks.register(type_="input_command_parse", description="Command with flags and values (5 flags)") @benchmarks.register(
type_="input_command_parse", description="Command with flags and values (5 flags)"
)
def benchmark_command_with_flags_and_values() -> None: def benchmark_command_with_flags_and_values() -> None:
InputCommand.parse("start --host localhost --port 8080 --debug --verbose -c config.json") InputCommand.parse("start --host localhost --port 8080 --debug --verbose -c config.json")
@benchmarks.register(type_="input_command_parse", description="Command with mixed prefixes (-, --, ---)") @benchmarks.register(
type_="input_command_parse", description="Command with mixed prefixes (-, --, ---)"
)
def benchmark_command_with_mixed_prefixes() -> None: def benchmark_command_with_mixed_prefixes() -> None:
InputCommand.parse("cmd -a --bb ---ccc -d value --ee value2 ---fff value3") InputCommand.parse("cmd -a --bb ---ccc -d value --ee value2 ---fff value3")
@@ -40,7 +44,9 @@ def benchmark_command_with_long_values() -> None:
InputCommand.parse(cmd) InputCommand.parse(cmd)
@benchmarks.register(type_="input_command_parse", description="Command with quoted values (5 flags)") @benchmarks.register(
type_="input_command_parse", description="Command with quoted values (5 flags)"
)
def benchmark_command_with_quoted_values() -> None: def benchmark_command_with_quoted_values() -> None:
InputCommand.parse("cmd --text 'hello world' --path '/usr/local/bin' --msg \"test message\"") InputCommand.parse("cmd --text 'hello world' --path '/usr/local/bin' --msg \"test message\"")
+20 -8
View File
@@ -3,7 +3,7 @@ __all__ = [
"benchmark_many_commands_most_similar", "benchmark_many_commands_most_similar",
"benchmark_many_aliases", "benchmark_many_aliases",
"benchmark_partial_match", "benchmark_partial_match",
"benchmark_extreme_commands" "benchmark_extreme_commands",
] ]
from argenta import App from argenta import App
@@ -19,9 +19,11 @@ def setup_app_with_commands(command_count: int, aliases_per_command: int = 0) ->
router = Router() router = Router()
for i in range(command_count): for i in range(command_count):
aliases = {f'alias{i}_{j}' for j in range(aliases_per_command)} if aliases_per_command else set() aliases = (
{f"alias{i}_{j}" for j in range(aliases_per_command)} if aliases_per_command else set()
)
@router.command(Command(f'command{i}', aliases=aliases)) @router.command(Command(f"command{i}", aliases=aliases))
def handler(_res: Response) -> None: def handler(_res: Response) -> None:
pass pass
@@ -29,31 +31,41 @@ def setup_app_with_commands(command_count: int, aliases_per_command: int = 0) ->
return app return app
@benchmarks.register(type_="most_similar_command", description="Few commands (10 commands, no match)") @benchmarks.register(
type_="most_similar_command", description="Few commands (10 commands, no match)"
)
def benchmark_few_commands() -> None: def benchmark_few_commands() -> None:
app = setup_app_with_commands(10) app = setup_app_with_commands(10)
app._most_similar_command("unknown") app._most_similar_command("unknown")
@benchmarks.register(type_="most_similar_command", description="Many commands (50 commands, no match)") @benchmarks.register(
type_="most_similar_command", description="Many commands (50 commands, no match)"
)
def benchmark_many_commands_most_similar() -> None: def benchmark_many_commands_most_similar() -> None:
app = setup_app_with_commands(50) app = setup_app_with_commands(50)
app._most_similar_command("unknown") app._most_similar_command("unknown")
@benchmarks.register(type_="most_similar_command", description="Many aliases (20 commands, 10 aliases each)") @benchmarks.register(
type_="most_similar_command", description="Many aliases (20 commands, 10 aliases each)"
)
def benchmark_many_aliases() -> None: def benchmark_many_aliases() -> None:
app = setup_app_with_commands(20, aliases_per_command=10) app = setup_app_with_commands(20, aliases_per_command=10)
app._most_similar_command("unknown") app._most_similar_command("unknown")
@benchmarks.register(type_="most_similar_command", description="Partial match (50 commands, prefix match)") @benchmarks.register(
type_="most_similar_command", description="Partial match (50 commands, prefix match)"
)
def benchmark_partial_match() -> None: def benchmark_partial_match() -> None:
app = setup_app_with_commands(50) app = setup_app_with_commands(50)
app._most_similar_command("comm") app._most_similar_command("comm")
@benchmarks.register(type_="most_similar_command", description="Extreme (100 commands, 20 aliases each)") @benchmarks.register(
type_="most_similar_command", description="Extreme (100 commands, 20 aliases each)"
)
def benchmark_extreme_commands() -> None: def benchmark_extreme_commands() -> None:
app = setup_app_with_commands(100, aliases_per_command=20) app = setup_app_with_commands(100, aliases_per_command=20)
app._most_similar_command("comm") app._most_similar_command("comm")
+16 -16
View File
@@ -3,7 +3,7 @@ __all__ = [
"benchmark_with_many_aliases", "benchmark_with_many_aliases",
"benchmark_few_aliases", "benchmark_few_aliases",
"benchmark_extreme_aliases", "benchmark_extreme_aliases",
"benchmark_very_many_aliases" "benchmark_very_many_aliases",
] ]
from argenta import App from argenta import App
@@ -19,15 +19,15 @@ def benchmark_no_aliases() -> None:
app = App(override_system_messages=True) app = App(override_system_messages=True)
router = Router() router = Router()
@router.command(Command('command1')) @router.command(Command("command1"))
def handler1(_res: Response) -> None: def handler1(_res: Response) -> None:
pass pass
@router.command(Command('command2')) @router.command(Command("command2"))
def handler2(_res: Response) -> None: def handler2(_res: Response) -> None:
pass pass
@router.command(Command('command3')) @router.command(Command("command3"))
def handler3(_res: Response) -> None: def handler3(_res: Response) -> None:
pass pass
@@ -40,15 +40,15 @@ def benchmark_few_aliases() -> None:
app = App(override_system_messages=True) app = App(override_system_messages=True)
router = Router() router = Router()
@router.command(Command('command1', aliases={'c1', 'cmd1'})) @router.command(Command("command1", aliases={"c1", "cmd1"}))
def handler1(_res: Response) -> None: def handler1(_res: Response) -> None:
pass pass
@router.command(Command('command2', aliases={'c2', 'cmd2'})) @router.command(Command("command2", aliases={"c2", "cmd2"}))
def handler2(_res: Response) -> None: def handler2(_res: Response) -> None:
pass pass
@router.command(Command('command3', aliases={'c3', 'cmd3'})) @router.command(Command("command3", aliases={"c3", "cmd3"}))
def handler3(_res: Response) -> None: def handler3(_res: Response) -> None:
pass pass
@@ -61,15 +61,15 @@ def benchmark_with_many_aliases() -> None:
app = App(override_system_messages=True) app = App(override_system_messages=True)
router = Router() router = Router()
@router.command(Command('command1', aliases={'c1', 'cmd1', 'com1', 'first', 'one'})) @router.command(Command("command1", aliases={"c1", "cmd1", "com1", "first", "one"}))
def handler1(_res: Response) -> None: def handler1(_res: Response) -> None:
pass pass
@router.command(Command('command2', aliases={'c2', 'cmd2', 'com2', 'second', 'two'})) @router.command(Command("command2", aliases={"c2", "cmd2", "com2", "second", "two"}))
def handler2(_res: Response) -> None: def handler2(_res: Response) -> None:
pass pass
@router.command(Command('command3', aliases={'c3', 'cmd3', 'com3', 'third', 'three'})) @router.command(Command("command3", aliases={"c3", "cmd3", "com3", "third", "three"}))
def handler3(_res: Response) -> None: def handler3(_res: Response) -> None:
pass pass
@@ -82,15 +82,15 @@ def benchmark_very_many_aliases() -> None:
app = App(override_system_messages=True) app = App(override_system_messages=True)
router = Router() router = Router()
@router.command(Command('command1', aliases={f'alias1_{i}' for i in range(20)})) @router.command(Command("command1", aliases={f"alias1_{i}" for i in range(20)}))
def handler1(_res: Response) -> None: def handler1(_res: Response) -> None:
pass pass
@router.command(Command('command2', aliases={f'alias2_{i}' for i in range(20)})) @router.command(Command("command2", aliases={f"alias2_{i}" for i in range(20)}))
def handler2(_res: Response) -> None: def handler2(_res: Response) -> None:
pass pass
@router.command(Command('command3', aliases={f'alias3_{i}' for i in range(20)})) @router.command(Command("command3", aliases={f"alias3_{i}" for i in range(20)}))
def handler3(_res: Response) -> None: def handler3(_res: Response) -> None:
pass pass
@@ -103,15 +103,15 @@ def benchmark_extreme_aliases() -> None:
app = App(override_system_messages=True) app = App(override_system_messages=True)
router = Router() router = Router()
@router.command(Command('command1', aliases={f'alias1_{i}' for i in range(100)})) @router.command(Command("command1", aliases={f"alias1_{i}" for i in range(100)}))
def handler1(_res: Response) -> None: def handler1(_res: Response) -> None:
pass pass
@router.command(Command('command2', aliases={f'alias2_{i}' for i in range(100)})) @router.command(Command("command2", aliases={f"alias2_{i}" for i in range(100)}))
def handler2(_res: Response) -> None: def handler2(_res: Response) -> None:
pass pass
@router.command(Command('command3', aliases={f'alias3_{i}' for i in range(100)})) @router.command(Command("command3", aliases={f"alias3_{i}" for i in range(100)}))
def handler3(_res: Response) -> None: def handler3(_res: Response) -> None:
pass pass
@@ -3,7 +3,7 @@ __all__ = [
"benchmark_many_routers", "benchmark_many_routers",
"benchmark_many_commands_per_router", "benchmark_many_commands_per_router",
"benchmark_many_aliases_per_command", "benchmark_many_aliases_per_command",
"benchmark_extreme_routers" "benchmark_extreme_routers",
] ]
from argenta import App from argenta import App
@@ -14,14 +14,17 @@ from argenta.router import Router
from .entity import benchmarks from .entity import benchmarks
@benchmarks.register(type_="validate_routers_for_collisions", description="With few routers (3 routers, 1 command each)") @benchmarks.register(
type_="validate_routers_for_collisions",
description="With few routers (3 routers, 1 command each)",
)
def benchmark_few_routers() -> None: def benchmark_few_routers() -> None:
app = App(override_system_messages=True) app = App(override_system_messages=True)
for i in range(3): for i in range(3):
router = Router() router = Router()
@router.command(Command(f'cmd{i}')) @router.command(Command(f"cmd{i}"))
def handler(_res: Response) -> None: def handler(_res: Response) -> None:
pass pass
@@ -31,14 +34,17 @@ def benchmark_few_routers() -> None:
app._validate_routers_for_collisions() app._validate_routers_for_collisions()
@benchmarks.register(type_="validate_routers_for_collisions", description="With many routers (10 routers, 1 command each)") @benchmarks.register(
type_="validate_routers_for_collisions",
description="With many routers (10 routers, 1 command each)",
)
def benchmark_many_routers() -> None: def benchmark_many_routers() -> None:
app = App(override_system_messages=True) app = App(override_system_messages=True)
for i in range(10): for i in range(10):
router = Router() router = Router()
@router.command(Command(f'cmd{i}')) @router.command(Command(f"cmd{i}"))
def handler(_res: Response) -> None: def handler(_res: Response) -> None:
pass pass
@@ -48,7 +54,10 @@ def benchmark_many_routers() -> None:
app._validate_routers_for_collisions() app._validate_routers_for_collisions()
@benchmarks.register(type_="validate_routers_for_collisions", description="With many commands per router (3 routers, 10 commands each)") @benchmarks.register(
type_="validate_routers_for_collisions",
description="With many commands per router (3 routers, 10 commands each)",
)
def benchmark_many_commands_per_router() -> None: def benchmark_many_commands_per_router() -> None:
app = App(override_system_messages=True) app = App(override_system_messages=True)
@@ -56,7 +65,8 @@ def benchmark_many_commands_per_router() -> None:
router = Router() router = Router()
for j in range(10): for j in range(10):
@router.command(Command(f'cmd{i}_{j}'))
@router.command(Command(f"cmd{i}_{j}"))
def handler(_res: Response) -> None: def handler(_res: Response) -> None:
pass pass
@@ -66,7 +76,10 @@ def benchmark_many_commands_per_router() -> None:
app._validate_routers_for_collisions() app._validate_routers_for_collisions()
@benchmarks.register(type_="validate_routers_for_collisions", description="With many aliases (3 routers, 5 commands, 10 aliases each)") @benchmarks.register(
type_="validate_routers_for_collisions",
description="With many aliases (3 routers, 5 commands, 10 aliases each)",
)
def benchmark_many_aliases_per_command() -> None: def benchmark_many_aliases_per_command() -> None:
app = App(override_system_messages=True) app = App(override_system_messages=True)
@@ -74,7 +87,10 @@ def benchmark_many_aliases_per_command() -> None:
router = Router() router = Router()
for j in range(5): for j in range(5):
@router.command(Command(f'cmd{i}_{j}', aliases={f'alias{i}_{j}_{k}' for k in range(10)}))
@router.command(
Command(f"cmd{i}_{j}", aliases={f"alias{i}_{j}_{k}" for k in range(10)})
)
def handler(_res: Response) -> None: def handler(_res: Response) -> None:
pass pass
@@ -84,7 +100,10 @@ def benchmark_many_aliases_per_command() -> None:
app._validate_routers_for_collisions() app._validate_routers_for_collisions()
@benchmarks.register(type_="validate_routers_for_collisions", description="Extreme (20 routers, 10 commands, 20 aliases each)") @benchmarks.register(
type_="validate_routers_for_collisions",
description="Extreme (20 routers, 10 commands, 20 aliases each)",
)
def benchmark_extreme_routers() -> None: def benchmark_extreme_routers() -> None:
app = App(override_system_messages=True) app = App(override_system_messages=True)
@@ -92,7 +111,10 @@ def benchmark_extreme_routers() -> None:
router = Router() router = Router()
for j in range(10): for j in range(10):
@router.command(Command(f'cmd{i}_{j}', aliases={f'alias{i}_{j}_{k}' for k in range(20)}))
@router.command(
Command(f"cmd{i}_{j}", aliases={f"alias{i}_{j}_{k}" for k in range(20)})
)
def handler(_res: Response) -> None: def handler(_res: Response) -> None:
pass pass
+56 -33
View File
@@ -5,17 +5,18 @@ from pathlib import Path
from rich.console import Console from rich.console import Console
from argenta.command import Flag, PossibleValues, Flags from argenta.command import Flag, Flags, PossibleValues
from argenta.command.flag import ValidationStatus from argenta.command.flag import ValidationStatus
from argenta.command.models import Command from argenta.command.models import Command
from argenta.response import Response from argenta.response import Response
from argenta.router import Router from argenta.router import Router
from .benchmarks.core.models import BenchmarkGroupResult from .benchmarks.core.models import BenchmarkGroupResult
from .benchmarks.entity import benchmarks as registered_benchmarks from .benchmarks.entity import benchmarks as registered_benchmarks
from .services.report_table_generator import ReportTableGenerator
from .services.system_info_reader import get_system_info
from .services.diagram_generator import DiagramGenerator from .services.diagram_generator import DiagramGenerator
from .services.release_generator import ReleaseGenerator from .services.release_generator import ReleaseGenerator
from .services.report_table_generator import ReportTableGenerator
from .services.system_info_reader import get_system_info
console = Console() console = Console()
router = Router(title="Metrics commands:", disable_redirect_stdout=True) router = Router(title="Metrics commands:", disable_redirect_stdout=True)
@@ -27,22 +28,30 @@ POSITIVE_INTEGER_PATTERN = re.compile(r"^[1-9]\d*$")
Command( Command(
"run-all", "run-all",
description="Print all benchmarks results", description="Print all benchmarks results",
flags=Flags([ flags=Flags(
Flag('without-gc', possible_values=PossibleValues.NEITHER), [
Flag('without-system-info', possible_values=PossibleValues.NEITHER) Flag("without-gc", possible_values=PossibleValues.NEITHER),
]) Flag("without-system-info", possible_values=PossibleValues.NEITHER),
]
),
) )
) )
def all_print_handler(response: Response) -> None: def all_print_handler(response: Response) -> None:
report_generator = ReportTableGenerator(get_system_info()) report_generator = ReportTableGenerator(get_system_info())
without_system_info = response.input_flags.get_flag_by_name("without-system-info", with_status=ValidationStatus.VALID) without_system_info = response.input_flags.get_flag_by_name(
"without-system-info", with_status=ValidationStatus.VALID
)
if not without_system_info: if not without_system_info:
console.print(report_generator.generate_system_info_header()) console.print(report_generator.generate_system_info_header())
console.print(report_generator.generate_system_info_table()) console.print(report_generator.generate_system_info_table())
is_gc_disabled = response.input_flags.get_flag_by_name("without-gc", with_status=ValidationStatus.VALID) is_gc_disabled = response.input_flags.get_flag_by_name(
type_grouped_benchmarks: list[BenchmarkGroupResult] = registered_benchmarks.run_benchmarks_grouped_by_type(is_gc_disabled=bool(is_gc_disabled)) "without-gc", with_status=ValidationStatus.VALID
)
type_grouped_benchmarks: list[BenchmarkGroupResult] = (
registered_benchmarks.run_benchmarks_grouped_by_type(is_gc_disabled=bool(is_gc_disabled))
)
for benchmark_group_result in type_grouped_benchmarks: for benchmark_group_result in type_grouped_benchmarks:
console.print(report_generator.generate_benchmark_table_header(benchmark_group_result)) console.print(report_generator.generate_benchmark_table_header(benchmark_group_result))
@@ -67,11 +76,13 @@ def list_types_handler(_: Response) -> None:
Command( Command(
"run-type", "run-type",
description="Run benchmarks by specific type", description="Run benchmarks by specific type",
flags=Flags([ flags=Flags(
Flag('type', possible_values=registered_benchmarks.get_types()), [
Flag('without-gc', possible_values=PossibleValues.NEITHER), Flag("type", possible_values=registered_benchmarks.get_types()),
Flag('without-system-info', possible_values=PossibleValues.NEITHER) Flag("without-gc", possible_values=PossibleValues.NEITHER),
]) Flag("without-system-info", possible_values=PossibleValues.NEITHER),
]
),
) )
) )
def run_type_handler(response: Response) -> None: def run_type_handler(response: Response) -> None:
@@ -94,13 +105,19 @@ def run_type_handler(response: Response) -> None:
report_generator = ReportTableGenerator(get_system_info()) report_generator = ReportTableGenerator(get_system_info())
without_system_info = response.input_flags.get_flag_by_name("without-system-info", with_status=ValidationStatus.VALID) without_system_info = response.input_flags.get_flag_by_name(
"without-system-info", with_status=ValidationStatus.VALID
)
if not without_system_info: if not without_system_info:
console.print(report_generator.generate_system_info_header()) console.print(report_generator.generate_system_info_header())
console.print(report_generator.generate_system_info_table()) console.print(report_generator.generate_system_info_table())
is_gc_disabled = response.input_flags.get_flag_by_name("without-gc", with_status=ValidationStatus.VALID, default=False) is_gc_disabled = response.input_flags.get_flag_by_name(
benchmark_group_result = registered_benchmarks.run_benchmarks_by_type(benchmark_type, is_gc_disabled=bool(is_gc_disabled)) "without-gc", with_status=ValidationStatus.VALID, default=False
)
benchmark_group_result = registered_benchmarks.run_benchmarks_by_type(
benchmark_type, is_gc_disabled=bool(is_gc_disabled)
)
console.print(report_generator.generate_benchmark_table_header(benchmark_group_result)) console.print(report_generator.generate_benchmark_table_header(benchmark_group_result))
console.print(report_generator.generate_benchmark_report_table(benchmark_group_result)) console.print(report_generator.generate_benchmark_report_table(benchmark_group_result))
@@ -113,22 +130,21 @@ def release_generate_handler(_: Response) -> None:
console.print(f"[cyan]Generating release report for version:[/cyan] [bold]{lib_version}[/bold]") console.print(f"[cyan]Generating release report for version:[/cyan] [bold]{lib_version}[/bold]")
console.print("[dim]Running benchmarks (1000 iterations, GC disabled)...[/dim]\n") console.print("[dim]Running benchmarks (1000 iterations, GC disabled)...[/dim]\n")
type_grouped_benchmarks: list[BenchmarkGroupResult] = registered_benchmarks.run_benchmarks_grouped_by_type( type_grouped_benchmarks: list[BenchmarkGroupResult] = (
iterations=1000, registered_benchmarks.run_benchmarks_grouped_by_type(iterations=1000, is_gc_disabled=True)
is_gc_disabled=True
) )
release_generator = ReleaseGenerator(lib_version) release_generator = ReleaseGenerator(lib_version)
output_dir = release_generator.generate_release(type_grouped_benchmarks) output_dir = release_generator.generate_release(type_grouped_benchmarks)
console.print(f"[green]✓[/green] Benchmarks completed. Generating release report...\n") console.print("[green]✓[/green] Benchmarks completed. Generating release report...\n")
for benchmark_group in type_grouped_benchmarks: for benchmark_group in type_grouped_benchmarks:
console.print(f"[cyan]Generated for:[/cyan] [bold]{benchmark_group.type_}[/bold]") console.print(f"[cyan]Generated for:[/cyan] [bold]{benchmark_group.type_}[/bold]")
console.print(f" [green]✓[/green] {benchmark_group.type_}_comparison.png") console.print(f" [green]✓[/green] {benchmark_group.type_}_comparison.png")
console.print(f" [green]✓[/green] {benchmark_group.type_}.json\n") console.print(f" [green]✓[/green] {benchmark_group.type_}.json\n")
console.print(f"[bold green]✓ Release report generated successfully[/bold green]") console.print("[bold green]✓ Release report generated successfully[/bold green]")
console.print(f"[cyan]Output directory:[/cyan] [bold]{output_dir}[/bold]") console.print(f"[cyan]Output directory:[/cyan] [bold]{output_dir}[/bold]")
@@ -136,26 +152,33 @@ def release_generate_handler(_: Response) -> None:
Command( Command(
"diagrams-generate", "diagrams-generate",
description="Generate diagrams for all benchmarks", description="Generate diagrams for all benchmarks",
flags=Flags([ flags=Flags(
Flag('without-gc', possible_values=PossibleValues.NEITHER), [
Flag('iterations', possible_values=POSITIVE_INTEGER_PATTERN) Flag("without-gc", possible_values=PossibleValues.NEITHER),
]) Flag("iterations", possible_values=POSITIVE_INTEGER_PATTERN),
]
),
) )
) )
def diagrams_generate_handler(response: Response) -> None: def diagrams_generate_handler(response: Response) -> None:
iterations = 100 iterations = 100
iterations_flag = response.input_flags.get_flag_by_name("iterations", with_status=ValidationStatus.VALID) iterations_flag = response.input_flags.get_flag_by_name(
"iterations", with_status=ValidationStatus.VALID
)
if iterations_flag: if iterations_flag:
iterations = int(iterations_flag.input_value) iterations = int(iterations_flag.input_value)
is_gc_disabled = bool(response.input_flags.get_flag_by_name("without-gc", with_status=ValidationStatus.VALID)) is_gc_disabled = bool(
response.input_flags.get_flag_by_name("without-gc", with_status=ValidationStatus.VALID)
)
console.print("[cyan]Running all benchmarks...[/cyan]") console.print("[cyan]Running all benchmarks...[/cyan]")
console.print(f"[dim]Iterations: {iterations}, GC Disabled: {is_gc_disabled}[/dim]\n") console.print(f"[dim]Iterations: {iterations}, GC Disabled: {is_gc_disabled}[/dim]\n")
type_grouped_benchmarks: list[BenchmarkGroupResult] = registered_benchmarks.run_benchmarks_grouped_by_type( type_grouped_benchmarks: list[BenchmarkGroupResult] = (
iterations=iterations, registered_benchmarks.run_benchmarks_grouped_by_type(
is_gc_disabled=is_gc_disabled iterations=iterations, is_gc_disabled=is_gc_disabled
)
) )
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
@@ -164,7 +187,7 @@ def diagrams_generate_handler(response: Response) -> None:
diagram_generator = DiagramGenerator(output_dir) diagram_generator = DiagramGenerator(output_dir)
console.print(f"[green]✓[/green] Benchmarks completed. Generating diagrams...\n") console.print("[green]✓[/green] Benchmarks completed. Generating diagrams...\n")
generated_count = 0 generated_count = 0
+1 -1
View File
@@ -1,6 +1,6 @@
from .diagram_generator import DiagramGenerator from .diagram_generator import DiagramGenerator
from .release_generator import ReleaseGenerator
from .report_table_generator import ReportTableGenerator from .report_table_generator import ReportTableGenerator
from .system_info_reader import get_system_info from .system_info_reader import get_system_info
from .release_generator import ReleaseGenerator
__all__ = ["DiagramGenerator", "ReportTableGenerator", "get_system_info", "ReleaseGenerator"] __all__ = ["DiagramGenerator", "ReportTableGenerator", "get_system_info", "ReleaseGenerator"]
+58 -75
View File
@@ -2,8 +2,9 @@ __all__ = ["DiagramGenerator"]
from pathlib import Path from pathlib import Path
import matplotlib import cairosvg
import matplotlib.pyplot as plt import pygal
from pygal.style import Style
from ..benchmarks.core.models import BenchmarkGroupResult from ..benchmarks.core.models import BenchmarkGroupResult
@@ -12,8 +13,26 @@ class DiagramGenerator:
def __init__(self, output_dir: Path | str) -> None: def __init__(self, output_dir: Path | str) -> None:
self.output_dir: Path = Path(output_dir) if isinstance(output_dir, str) else output_dir self.output_dir: Path = Path(output_dir) if isinstance(output_dir, str) else output_dir
matplotlib.use('Agg') self._style = Style(
plt.style.use('seaborn-v0_8-whitegrid') background="white",
plot_background="white",
foreground="#2c3e50",
foreground_strong="#000000",
foreground_subtle="#7f8c8d",
opacity=".9",
opacity_hover=".95",
transition="150ms ease-in",
colors=("#2ecc71", "#3498db", "#e74c3c"),
title_font_size=40,
legend_font_size=34,
label_font_size=32, #
major_label_font_size=32,
value_font_size=28,
value_label_font_size=28,
tooltip_font_size=24,
no_data_font_size=28,
font_family="Consolas, 'Courier New', monospace",
)
def generate_comparison_diagram(self, benchmark_group: BenchmarkGroupResult) -> Path: def generate_comparison_diagram(self, benchmark_group: BenchmarkGroupResult) -> Path:
results = benchmark_group.benchmark_results results = benchmark_group.benchmark_results
@@ -27,84 +46,48 @@ class DiagramGenerator:
max_value = max( max_value = max(
max(avg_times) if avg_times else 0, max(avg_times) if avg_times else 0,
max(median_times) if median_times else 0, max(median_times) if median_times else 0,
max(std_devs) if std_devs else 0 max(std_devs) if std_devs else 0,
) )
y_limit = max_value / 0.85 if max_value > 0 else 1.0 y_limit = max_value / 0.85 if max_value > 0 else 1.0
items_count = len(descriptions) title_text = f"{benchmark_group.type_.replace('_', ' ').title()}"
x_positions: list[int] = list(range(items_count)) metadata_text = (
f"Iterations: {benchmark_group.iterations} | GC: "
bar_width = 0.25 f"{'Disabled' if benchmark_group.is_gc_disabled else 'Enabled'}"
)
x_std_dev = [x - bar_width for x in x_positions]
x_avg = [x for x in x_positions]
x_median = [x + bar_width for x in x_positions]
fig, ax = plt.subplots(figsize=(16, 8))
fig.patch.set_facecolor('white')
bars_std = ax.bar(x_std_dev, std_devs, bar_width, label='Std Deviation',
color='#2ecc71', alpha=0.9, edgecolor='#27ae60', linewidth=1.5)
bars_avg = ax.bar(x_avg, avg_times, bar_width, label='Average Time',
color='#3498db', alpha=0.9, edgecolor='#2980b9', linewidth=1.5)
bars_median = ax.bar(x_median, median_times, bar_width, label='Median Time',
color='#e74c3c', alpha=0.9, edgecolor='#c0392b', linewidth=1.5)
for bar_group in [bars_std, bars_avg, bars_median]:
for bar in bar_group:
height = bar.get_height()
ax.text(
bar.get_x() + bar.get_width() / 2.,
height,
f'{height:.3f}',
ha='center', va='bottom', fontsize=9, fontweight='bold'
)
ax.set_ylabel('Time (ms)', fontsize=14, fontweight='bold', labelpad=10)
title_text = f'{benchmark_group.type_.replace("_", " ").title()}'
metadata_text = f'Iterations: {benchmark_group.iterations} | GC: {"Disabled" if benchmark_group.is_gc_disabled else "Enabled"}'
ax.text(0.5, 1.08, title_text, transform=ax.transAxes,
fontsize=18, fontweight='bold', ha='center', color='#2c3e50')
ax.text(0.5, 1.03, metadata_text, transform=ax.transAxes,
fontsize=12, ha='center', color='#7f8c8d', style='italic')
ax.set_xticks(x_positions)
ax.set_xticklabels([])
for i, (pos, desc) in enumerate(zip(x_positions, descriptions)):
text_x_pos = pos - bar_width - (bar_width / 2)
ax.text(
text_x_pos,
y_limit * 0.02,
desc,
rotation=90, va='bottom', ha='right', fontsize=10,
color='#2c3e50'
)
ax.set_ylim(0, y_limit)
legend = ax.legend(loc='upper left', fontsize=12, framealpha=0.95,
edgecolor='#34495e', fancybox=True, shadow=True)
legend.get_frame().set_facecolor('#ecf0f1')
ax.grid(axis='y', alpha=0.4, linestyle='--', linewidth=0.8)
ax.set_axisbelow(True)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_color('#7f8c8d')
ax.spines['bottom'].set_color('#7f8c8d')
plt.tight_layout()
filename = f"{benchmark_group.type_}_comparison.png" filename = f"{benchmark_group.type_}_comparison.png"
output_path = self.output_dir / filename output_path = self.output_dir / filename
self.output_dir.mkdir(parents=True, exist_ok=True) self.output_dir.mkdir(parents=True, exist_ok=True)
plt.savefig(output_path, dpi=200, bbox_inches='tight', facecolor='white') dynamic_height = 600 + (len(descriptions) * 150)
plt.close(fig)
chart = pygal.HorizontalBar(
style=self._style,
width=3100,
height=dynamic_height,
explicit_size=True,
show_legend=True,
legend_at_bottom=True,
print_values=True,
print_values_position="top",
legend_at_bottom_columns=3,
range=(0, y_limit),
zero=0,
)
chart.title = f"{title_text}\n{metadata_text}"
chart.x_title = "Time (ms)"
chart.no_data_text = "No data"
chart.value_formatter = lambda x: f"{x:.3f}"
chart.x_labels = descriptions
chart.add("Std Deviation", std_devs)
chart.add("Average Time", avg_times)
chart.add("Median Time", median_times)
svg_bytes = chart.render()
cairosvg.svg2png(bytestring=svg_bytes, write_to=str(output_path))
return output_path return output_path
+3 -3
View File
@@ -36,14 +36,14 @@ class ReleaseGenerator:
"description": br.description, "description": br.description,
"avg_time": br.avg_time, "avg_time": br.avg_time,
"median_time": br.median_time, "median_time": br.median_time,
"std_dev": br.std_dev "std_dev": br.std_dev,
} }
for br in benchmark_group.benchmark_results for br in benchmark_group.benchmark_results
] ],
} }
json_path = type_dir / f"{benchmark_group.type_}.json" json_path = type_dir / f"{benchmark_group.type_}.json"
with open(json_path, 'w', encoding='utf-8') as f: with open(json_path, "w", encoding="utf-8") as f:
json.dump(json_data, f, indent=2, ensure_ascii=False) json.dump(json_data, f, indent=2, ensure_ascii=False)
return self.output_dir return self.output_dir
+21 -13
View File
@@ -3,7 +3,7 @@ from rich.table import Table
from rich.text import Text from rich.text import Text
from ..benchmarks.core.models import BenchmarkGroupResult from ..benchmarks.core.models import BenchmarkGroupResult
from metrics.services.system_info_reader import SystemInfo from .system_info_reader import SystemInfo
class ReportTableGenerator: class ReportTableGenerator:
@@ -12,11 +12,15 @@ class ReportTableGenerator:
self._cached_benchmark_tables: dict[int, Table] = {} self._cached_benchmark_tables: dict[int, Table] = {}
self._cached_system_info_table: Table | None = None self._cached_system_info_table: Table | None = None
def generate_benchmark_report_table(self, benchmark_group_result: BenchmarkGroupResult) -> Table: def generate_benchmark_report_table(
self, benchmark_group_result: BenchmarkGroupResult
) -> Table:
if cached_result := self._cached_benchmark_tables.get(id(benchmark_group_result)): if cached_result := self._cached_benchmark_tables.get(id(benchmark_group_result)):
return cached_result return cached_result
table = Table(show_header=True, header_style="bold cyan", border_style="blue", show_lines=True) table = Table(
show_header=True, header_style="bold cyan", border_style="blue", show_lines=True
)
table.add_column("Description", style="dim") table.add_column("Description", style="dim")
table.add_column("Avg Time", justify="right", style="bold yellow") table.add_column("Avg Time", justify="right", style="bold yellow")
table.add_column("Median Time", justify="right", style="bold yellow") table.add_column("Median Time", justify="right", style="bold yellow")
@@ -34,18 +38,22 @@ class ReportTableGenerator:
@staticmethod @staticmethod
def generate_benchmark_table_header(benchmark_group_result: BenchmarkGroupResult) -> Panel: def generate_benchmark_table_header(benchmark_group_result: BenchmarkGroupResult) -> Panel:
header_text = Text(f"TYPE: {benchmark_group_result.type_.upper()} ; " header_text = Text(
f"ITERATIONS: {benchmark_group_result.iterations} ; " f"TYPE: {benchmark_group_result.type_.upper()} ; "
f"GC {"DISABLED" if benchmark_group_result.is_gc_disabled else "ENABLED"} ; " f"ITERATIONS: {benchmark_group_result.iterations} ; "
f"ALL TIME IN MS", f"GC {'DISABLED' if benchmark_group_result.is_gc_disabled else 'ENABLED'} ; "
style="bold magenta") f"ALL TIME IN MS",
style="bold magenta",
)
return Panel(header_text, expand=False, border_style="magenta") return Panel(header_text, expand=False, border_style="magenta")
def generate_system_info_table(self) -> Table: def generate_system_info_table(self) -> Table:
if self._cached_system_info_table is not None: if self._cached_system_info_table is not None:
return self._cached_system_info_table return self._cached_system_info_table
table = Table(show_header=True, header_style="bold cyan", border_style="blue", show_lines=True) table = Table(
show_header=True, header_style="bold cyan", border_style="blue", show_lines=True
)
table.add_column("Parameter", style="green") table.add_column("Parameter", style="green")
table.add_column("Value", style="yellow") table.add_column("Value", style="yellow")
@@ -55,10 +63,10 @@ class ReportTableGenerator:
table.add_row("CPU", self.system_info.cpu_info.name) table.add_row("CPU", self.system_info.cpu_info.name)
table.add_row("CPU Physical Cores", str(self.system_info.cpu_info.physical_cores)) table.add_row("CPU Physical Cores", str(self.system_info.cpu_info.physical_cores))
table.add_row("CPU Logical Cores", str(self.system_info.cpu_info.logical_cores)) table.add_row("CPU Logical Cores", str(self.system_info.cpu_info.logical_cores))
table.add_row("CPU Max Frequency", str(self.system_info.cpu_info.max_frequency) + ' GHz') table.add_row("CPU Max Frequency", str(self.system_info.cpu_info.max_frequency) + " GHz")
table.add_row("Total RAM", str(self.system_info.memory_info.total_ram) + ' GB') table.add_row("Total RAM", str(self.system_info.memory_info.total_ram) + " GB")
table.add_row("Used RAM", str(self.system_info.memory_info.used_ram) + ' GB') table.add_row("Used RAM", str(self.system_info.memory_info.used_ram) + " GB")
table.add_row("Available RAM", str(self.system_info.memory_info.available_ram) + ' GB') table.add_row("Available RAM", str(self.system_info.memory_info.available_ram) + " GB")
table.add_row("Python Version", self.system_info.python_info.version) table.add_row("Python Version", self.system_info.python_info.version)
table.add_row("Python Implementation", self.system_info.python_info.implementation) table.add_row("Python Implementation", self.system_info.python_info.implementation)
table.add_row("Python Compiler", self.system_info.python_info.compiler) table.add_row("Python Compiler", self.system_info.python_info.compiler)
+36 -41
View File
@@ -1,28 +1,19 @@
__all__ = [ __all__ = ["SystemInfo", "get_system_info"]
"SystemInfo",
"get_system_info"
]
from dataclasses import dataclass
import platform import platform
import sys import sys
from dataclasses import dataclass
import cpuinfo import cpuinfo
import psutil import psutil
@dataclass(frozen=True, slots=True)
class SystemInfo:
os_info: OSInfo
cpu_info: CPUInfo
memory_info: MemoryInfo
python_info: PythonInfo
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class OSInfo: class OSInfo:
name: str name: str
kernel_version: str kernel_version: str
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class CPUInfo: class CPUInfo:
name: str name: str
@@ -31,11 +22,13 @@ class CPUInfo:
logical_cores: int logical_cores: int
max_frequency: float max_frequency: float
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class MemoryInfo: class MemoryInfo:
total_ram: float # in GB total_ram: float # in GB
used_ram: float # in GB used_ram: float # in GB
available_ram: float # in GB available_ram: float # in GB
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class PythonInfo: class PythonInfo:
@@ -44,18 +37,6 @@ class PythonInfo:
compiler: str compiler: str
def get_system_info() -> SystemInfo:
os_info = get_os_info()
cpu_info = get_cpu_info()
memory_info = get_memory_info()
python_info = get_python_info()
return SystemInfo(
os_info=os_info,
cpu_info=cpu_info,
memory_info=memory_info,
python_info=python_info,
)
def get_os_info() -> OSInfo: def get_os_info() -> OSInfo:
system = platform.system() system = platform.system()
@@ -73,22 +54,17 @@ def get_os_info() -> OSInfo:
kernel_version=kernel_version, kernel_version=kernel_version,
) )
elif system == "Darwin": elif system == "Darwin":
return OSInfo( return OSInfo(kernel_version=platform.release(), name=f"macOS {platform.mac_ver()[0]}")
kernel_version=platform.release(),
name=f"macOS {platform.mac_ver()[0]}"
)
else: else:
return OSInfo( return OSInfo(kernel_version=platform.release(), name=platform.system())
kernel_version=platform.release(),
name=platform.system()
)
def get_cpu_info() -> CPUInfo: def get_cpu_info() -> CPUInfo:
cpu_info = cpuinfo.get_cpu_info() cpu_info = cpuinfo.get_cpu_info()
cpu_name = cpu_info["brand_raw"] cpu_name = cpu_info["brand_raw"]
cpu_architecture = cpu_info["arch"] cpu_architecture = cpu_info["arch"]
cpu_physical_cores = psutil.cpu_count(logical=False) cpu_physical_cores = psutil.cpu_count(logical=False) or 0
cpu_logical_cores = psutil.cpu_count(logical=True) cpu_logical_cores = psutil.cpu_count(logical=True) or 0
cpu_freq = psutil.cpu_freq() cpu_freq = psutil.cpu_freq()
cpu_max_frequency = cpu_freq.max cpu_max_frequency = cpu_freq.max
@@ -98,9 +74,10 @@ def get_cpu_info() -> CPUInfo:
architecture=cpu_architecture, architecture=cpu_architecture,
physical_cores=cpu_physical_cores, physical_cores=cpu_physical_cores,
logical_cores=cpu_logical_cores, logical_cores=cpu_logical_cores,
max_frequency=cpu_max_frequency max_frequency=cpu_max_frequency,
) )
def get_memory_info() -> MemoryInfo: def get_memory_info() -> MemoryInfo:
mem = psutil.virtual_memory() mem = psutil.virtual_memory()
total_ram = round(mem.total / (1024**3), 2) total_ram = round(mem.total / (1024**3), 2)
@@ -113,14 +90,32 @@ def get_memory_info() -> MemoryInfo:
available_ram=available_ram, available_ram=available_ram,
) )
def get_python_info() -> PythonInfo: def get_python_info() -> PythonInfo:
python_version = platform.python_version() python_version = platform.python_version()
python_implementation = platform.python_implementation() python_implementation = platform.python_implementation()
python_compiler = platform.python_compiler() python_compiler = platform.python_compiler()
return PythonInfo( return PythonInfo(
version=python_version, version=python_version, implementation=python_implementation, compiler=python_compiler
implementation=python_implementation,
compiler=python_compiler
) )
@dataclass(frozen=True, slots=True)
class SystemInfo:
os_info: OSInfo
cpu_info: CPUInfo
memory_info: MemoryInfo
python_info: PythonInfo
def get_system_info() -> SystemInfo:
os_info = get_os_info()
cpu_info = get_cpu_info()
memory_info = get_memory_info()
python_info = get_python_info()
return SystemInfo(
os_info=os_info,
cpu_info=cpu_info,
memory_info=memory_info,
python_info=python_info,
)
-18
View File
@@ -1,18 +0,0 @@
from argenta import App, Command, Response, Router
app = App(override_system_messages=True)
router = Router()
@router.command(Command('command'))
def handler(_res: Response) -> None:
pass
@router.command(Command('command_other'))
def handler2(_res: Response) -> None:
pass
app.include_routers(router)
app._pre_cycle_setup()
assert app._most_similar_command('command_') == 'command'
+1 -1
View File
@@ -9,7 +9,7 @@ orchestrator: Orchestrator = Orchestrator()
def main() -> None: def main() -> None:
app.include_router(router) app.include_router(router)
orchestrator.start_polling(app) orchestrator.run_repl(app)
if __name__ == '__main__': if __name__ == '__main__':
main() main()
+3 -2
View File
@@ -3,12 +3,13 @@ from prompt_toolkit import HTML
from argenta import App, Orchestrator from argenta import App, Orchestrator
from argenta.app import PredefinedMessages, StaticDividingLine, AutoCompleter from argenta.app import PredefinedMessages, StaticDividingLine, AutoCompleter
from argenta.app.dividing_line.models import DynamicDividingLine from argenta.app.dividing_line.models import DynamicDividingLine
from argenta.orchestrator import ArgParser
from mock.mock_app.routers import work_router from mock.mock_app.routers import work_router
app: App = App( app: App = App(
dividing_line=StaticDividingLine('~') dividing_line=StaticDividingLine('~')
) )
orchestrator: Orchestrator = Orchestrator() orchestrator: Orchestrator = Orchestrator(arg_parser=ArgParser(processed_args=[]))
def main(): def main():
@@ -18,7 +19,7 @@ def main():
app.add_message_on_startup(PredefinedMessages.AUTOCOMPLETE) app.add_message_on_startup(PredefinedMessages.AUTOCOMPLETE)
app.add_message_on_startup(PredefinedMessages.HELP) app.add_message_on_startup(PredefinedMessages.HELP)
orchestrator.start_polling(app) orchestrator.run_repl(app)
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+13
View File
@@ -13,12 +13,19 @@ dependencies = [
"prompt-toolkit>=3.0.52", "prompt-toolkit>=3.0.52",
] ]
[project.optional-dependencies]
cli = [
"nuitka[onefile]>=4.0.5",
"typer>=0.9,!=0.12,<=0.21.1",
]
[dependency-groups] [dependency-groups]
dev = [ dev = [
{include-group = "linters"}, {include-group = "linters"},
{include-group = "typecheckers"}, {include-group = "typecheckers"},
{include-group = "docs"}, {include-group = "docs"},
{include-group = "tests"}, {include-group = "tests"},
{include-group = "metrics"},
"scriv>=1.8.0", "scriv>=1.8.0",
] ]
linters = [ linters = [
@@ -46,13 +53,19 @@ metrics = [
"matplotlib>=3.10.8", "matplotlib>=3.10.8",
"psutil>=7.2.1", "psutil>=7.2.1",
"py-cpuinfo>=9.0.0", "py-cpuinfo>=9.0.0",
"cairosvg>=2.8.2",
"pygal>=3.1.0",
] ]
[project.scripts]
argenta = "argenta._cli.__main__:main"
[tool.ruff] [tool.ruff]
line-length=100 line-length=100
[tool.pyright] [tool.pyright]
typeCheckingMode = "strict" typeCheckingMode = "strict"
reportMissingTypeStubs = false
[[tool.pyright.executionEnvironments]] [[tool.pyright.executionEnvironments]]
root = "tests/" root = "tests/"
View File
+60
View File
@@ -0,0 +1,60 @@
from typer import Typer
from .commands import (
build_handler,
info_handler,
init_handler,
new_handler,
routes_handler,
run_handler,
)
def main() -> None:
app = Typer()
app.command(
"run",
help="Command to start the orchestrator repl; the path to the callable object is required",
short_help="Start the orchestrator REPL",
epilog="Example: run app/main.py:main",
)(run_handler)
app.command(
"init",
help="Creates a flat/src boilerplate architecture in an existing project",
short_help="Initialize architecture in existing project",
epilog="Make sure you are in the project root before running this command.",
)(init_handler)
app.command(
"new",
help="Creates a project and in it flat/src boilerplate architecture",
short_help="Create a new project with boilerplate",
epilog="This will create a new directory with the project structure.",
)(new_handler)
app.command(
"routes",
help="Creates a project and in it flat/src boilerplate architecture",
short_help="Create a new project with boilerplate",
epilog="This will create a new directory with the project structure.",
)(routes_handler)
app.command(
name="info",
help="Displays information about the installed Argenta package and environment",
short_help="Show Argenta version and environment info",
epilog="Uses metadata to retrieve the installed package version.",
)(info_handler)
app.command(
name="build",
help="Compiles the project into a standalone binary using Nuitka",
short_help="Build a standalone binary",
)(build_handler)
app()
if __name__ == "__main__":
main()
+6
View File
@@ -0,0 +1,6 @@
from .run import run_handler as run_handler
from .init import init_handler as init_handler
from .new import new_handler as new_handler
from .routes import routes_handler as routes_handler
from .info import info_handler as info_handler
from .build import build_handler as build_handler
+58
View File
@@ -0,0 +1,58 @@
__all__ = ["build_handler"]
import os
import subprocess
import sys
from pathlib import Path
from rich.console import Console
def build_handler(entry_point: str, output_name: str | None = None) -> None:
console = Console()
file_path, _, callable_name = entry_point.partition(":")
if not file_path or not callable_name:
console.print(
f'[bold red]Error:[/bold red] "{entry_point}" must be in format "<path/to/file.py>:<callable>"'
)
raise SystemExit(1)
path = Path(file_path).resolve()
if not path.exists():
console.print(f'[bold red]Error:[/bold red] File "{file_path}" not found')
raise SystemExit(1)
is_main_module = path.name == "__main__.py"
target = str(path.parent) if is_main_module else str(path)
name = output_name or (path.parent.name if is_main_module else path.stem)
console.print(
f"[bold green]Building[/bold green] [cyan]{entry_point}[/cyan] → [cyan]{name}[/cyan]"
)
args = [
sys.executable,
"-m",
"nuitka",
"--standalone",
"--onefile",
f"--output-filename={name}",
f"--jobs={os.cpu_count()}",
"--lto=no",
"--include-windows-runtime-dlls=no",
]
if is_main_module:
args.append("--python-flag=-m")
args.append(target)
result = subprocess.run(args, check=False)
if result.returncode != 0:
console.print("[bold red]Build failed.[/bold red]")
raise SystemExit(result.returncode)
console.print(f"[bold green]Done![/bold green] Binary: [cyan]{name}[/cyan]")
+36
View File
@@ -0,0 +1,36 @@
__all__ = ["info_handler"]
import sys
import platform
from importlib.metadata import version
from art import text2art # pyright: ignore[reportUnknownVariableType]
from rich.console import Console
from rich.padding import Padding
from rich.table import Table
from rich import box
console = Console()
def info_handler() -> None:
table = Table(
box=box.SIMPLE,
show_header=False,
pad_edge=False,
show_edge=False,
expand=False,
)
table.add_column(style="bold cyan")
table.add_column(style="white", justify="right")
table.add_row("Argenta version", f'[bold red]{version("argenta")}[/bold red]')
table.add_row("Python version", sys.version.split()[0])
table.add_row("Platform", f"{platform.system()} {platform.release()} ({platform.machine()})")
table.add_row("Docs", "https://argenta.readthedocs.io")
console.print(f"[bold red]{text2art("Argenta", font='tarty1')}[/bold red]")
console.print(Padding(table, pad=(2, 5)))
console.print(Padding("[i]made with ❤ by [b]kolo[/b][/i]", pad=(0, 17)))
+107
View File
@@ -0,0 +1,107 @@
__all__ = ["init_handler"]
from pathlib import Path
from typing import Literal
GITIGNORE_CONTENT = """
__pycache__/
*.py[cod]
.env
.venv/
env/
"""
FLAT_MAIN_TEMPLATE = """
from argenta import Orchestrator, App
from handlers import router
def main():
app = App()
app.include_router(router)
orchestrator = Orchestrator()
orchestrator.run_repl(app)
if __name__ == "__main__":
main()
"""
FLAT_HANDLERS_TEMPLATE = """
from argenta import Router, Response
router = Router("Hello command")
@router.command("hello")
def start_handler(response: Response):
print("Hello world!")
"""
SRC_MAIN_TEMPLATE = """
from argenta import Orchestrator, App
from .routers import router
def main():
app = App()
app.include_router(router)
orchestrator = Orchestrator()
orchestrator.run_repl(app)
if __name__ == "__main__":
main()
"""
SRC_ROUTERS_TEMPLATE = """
from argenta import Router
from .handlers.hello_world_handler import hello_handler
router = Router()
router.command('hello')(hello_handler)
"""
SRC_HANDLER_TEMPLATE = """
from argenta import Response
def hello_handler(response: Response) -> None:
print("Hello world!")
"""
def create_file(path: Path, content: str) -> None:
if not path.exists():
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content.strip(), encoding="utf-8")
else:
print(f"Skipped: {path} (already exists)")
def init_handler(with_arch: Literal["flat", "src"] = "flat") -> None:
cwd = Path.cwd()
project_name = cwd.name.lower().replace(" ", "_")
create_file(cwd / ".gitignore", GITIGNORE_CONTENT)
if with_arch == "flat":
create_file(cwd / "main.py", FLAT_MAIN_TEMPLATE)
create_file(cwd / "handlers.py", FLAT_HANDLERS_TEMPLATE)
elif with_arch == "src":
base_pkg = cwd / "src" / project_name / "application"
create_file(base_pkg / "__main__.py", SRC_MAIN_TEMPLATE)
create_file(base_pkg / "routers.py", SRC_ROUTERS_TEMPLATE)
create_file(base_pkg / "handlers" / "hello_world_handler.py", SRC_HANDLER_TEMPLATE)
create_file(cwd / "src" / "__init__.py", "")
create_file(cwd / "src" / project_name / "__init__.py", "")
create_file(base_pkg / "__init__.py", "")
create_file(base_pkg / "handlers" / "__init__.py", "")
print("\nInitialization complete.")
+115
View File
@@ -0,0 +1,115 @@
__all__ = ["new_handler"]
import sys
from pathlib import Path
from typing import Literal
GITIGNORE_CONTENT = """
__pycache__/
*.py[cod]
.env
.venv/
env/
"""
FLAT_MAIN_TEMPLATE = """
from argenta import Orchestrator, App
from handlers import router
def main():
app = App()
app.include_router(router)
orchestrator = Orchestrator()
orchestrator.run_repl(app)
if __name__ == "__main__":
main()
"""
FLAT_HANDLERS_TEMPLATE = """
from argenta import Router, Response
router = Router("Hello command")
@router.command("hello")
def start_handler(response: Response):
print("Hello world!")
"""
SRC_MAIN_TEMPLATE = """
from argenta import Orchestrator, App
from .routers import router
def main():
app = App()
app.include_router(router)
orchestrator = Orchestrator()
orchestrator.run_repl(app)
if __name__ == "__main__":
main()
"""
SRC_ROUTERS_TEMPLATE = """
from argenta import Router
from .handlers.hello_world_handler import hello_handler
router = Router()
router.command('hello')(hello_handler)
"""
SRC_HANDLER_TEMPLATE = """
from argenta import Response
def hello_handler(response: Response) -> None:
print("Hello world!")
"""
def create_file(path: Path, content: str) -> None:
if not path.exists():
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content.strip(), encoding="utf-8")
else:
print(f"Skipped: {path} (already exists)")
def new_handler(project_name: str, with_arch: Literal["flat", "src"] = "flat") -> None:
base_dir = Path.cwd() / project_name
if base_dir.exists():
print(f"Error: Directory '{project_name}' already exists.")
sys.exit(1)
base_dir.mkdir(parents=True)
print(f"Initialized project directory: {base_dir}")
create_file(base_dir / ".gitignore", GITIGNORE_CONTENT)
if with_arch == "flat":
create_file(base_dir / "main.py", FLAT_MAIN_TEMPLATE)
create_file(base_dir / "handlers.py", FLAT_HANDLERS_TEMPLATE)
elif with_arch == "src":
pkg_name = project_name.lower().replace(" ", "_").replace("-", "_")
app_pkg = base_dir / "src" / pkg_name / "application"
create_file(app_pkg / "__main__.py", SRC_MAIN_TEMPLATE)
create_file(app_pkg / "routers.py", SRC_ROUTERS_TEMPLATE)
create_file(app_pkg / "handlers" / "hello_world_handler.py", SRC_HANDLER_TEMPLATE)
create_file(base_dir / "src" / "__init__.py", "")
create_file(base_dir / "src" / pkg_name / "__init__.py", "")
create_file(app_pkg / "__init__.py", "")
create_file(app_pkg / "handlers" / "__init__.py", "")
print(f"\nProject '{project_name}' created successfully! 🚀")
+82
View File
@@ -0,0 +1,82 @@
__all__ = ["routes_handler"]
from collections import defaultdict
from rich.console import Console
from rich.panel import Panel
from rich.tree import Tree
from ..infrastructure.entrypoint_resolver.entity import (
EntryPointAsApp,
EntrypointResolver,
ResolveFromStringError,
)
def routes_handler(entrypoint_path: str) -> None:
entrypoint_path, _, entrypoint_callable_name = entrypoint_path.partition(":")
if not entrypoint_callable_name:
raise ResolveFromStringError(
"Path to callable object that run orchestrator repl must be in the format <path/to/file.py>:<object_name>"
)
app_instance = EntrypointResolver[EntryPointAsApp](entrypoint_path).parse_entrypoint_with_type(
entrypoint_callable_name
)
app = app_instance.instance_object
routers = app.registered_routers
console = Console()
stats: dict[str, int] = defaultdict(int)
tree = Tree(f"📦 [bold blue]App object:[/bold blue] {app!r}")
for router in routers:
stats["routers"] += 1
router_node = tree.add(f"📁 [bold green]Router:[/bold green] {router.title}")
for command in router.command_handlers:
stats["commands"] += 1
trigger = command.handled_command.trigger
description = command.handled_command.description
aliases = list(command.handled_command.aliases)
flags = list(command.handled_command.registered_flags)
cmd_node = router_node.add(f"⚡ [bold cyan]{trigger}[/bold cyan]")
if description:
cmd_node.add(f"📝 [dim]description:[/dim] {description}")
if aliases:
aliases_str = ", ".join(f"[yellow]{a}[/yellow]" for a in aliases)
cmd_node.add(f"🔀 [dim]aliases:[/dim] {aliases_str}")
stats["aliases"] += len(aliases)
if flags:
flags_node = cmd_node.add(f"🚩 [dim]flags:[/dim] ({len(flags)})")
for flag in flags:
possible = flag.possible_values
flags_node.add(
f"[magenta]{flag.prefix}{flag.name}[/magenta]"
f" [dim]possible_values:[/dim] [italic]{possible!r}[/italic]"
)
stats["flags"] += len(flags)
stats_text = (
f"📁 [bold]Total Routers:[/bold] {stats['routers']}\n"
f"⚡ [bold]Total Commands:[/bold] {stats['commands']}\n"
f"🔀 [bold]Total Aliases:[/bold] {stats['aliases']}\n"
f"🚩 [bold]Total Flags:[/bold] {stats['flags']}"
)
console.print(
Panel(
stats_text,
title="[bold blue]App Stats[/bold blue]",
expand=False,
border_style="blue",
)
)
console.print()
console.print(tree)
+24
View File
@@ -0,0 +1,24 @@
__all__ = ["run_handler"]
import os
from ..infrastructure.entrypoint_resolver.entity import (
CallableEntryPoint,
EntrypointResolver,
ResolveFromStringError,
)
def run_handler(entrypoint_path: str) -> None:
os.environ["RUN_FROM_ARGENTA_RUNNER"] = "1"
entrypoint_path, _, entrypoint_callable_name = entrypoint_path.partition(":")
if not entrypoint_callable_name:
raise ResolveFromStringError(
"Path to callable object that run orchestrator repl must be in the format <path/to/file.py>:<object_name> or <path.to.module>:<object_name>"
)
runner = EntrypointResolver[CallableEntryPoint](entrypoint_path).parse_entrypoint_with_type(
entrypoint_callable_name
)
runner.instance_object()
@@ -0,0 +1,7 @@
from .entity import CallableEntryPoint as CallableEntryPoint
from .entity import EntryPointAsApp as EntryPointAsApp
from .entity import EntrypointResolver as EntrypointResolver
from .exceptions import EntrypointNotCallableError as EntrypointNotCallableError
from .exceptions import ResolveFromStringError as ResolveFromStringError
from .exceptions import CallableEntrypointNotMatchRequiredSignatureError as CallableEntrypointNotMatchRequiredSignatureError
from .exceptions import EntrypointNotAppInstanceError as EntrypointNotAppInstanceError
@@ -0,0 +1,136 @@
__all__ = ["EntrypointResolver", "EntryPointAsApp", "CallableEntryPoint"]
import importlib
import inspect
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Callable, Protocol, cast, get_args
from argenta.app.models import App
from .exceptions import (
CallableEntrypointNotMatchRequiredSignatureError,
EntrypointNotAppInstanceError,
EntrypointNotCallableError,
ResolveFromStringError,
)
class EntryPoint[T](Protocol):
@property
def raw_path(self) -> str: ...
@property
def instance_object(self) -> T: ...
@dataclass(frozen=True, slots=True)
class CallableEntryPoint:
raw_path: str
instance_object: Callable[[], None]
@dataclass(frozen=True, slots=True)
class EntryPointAsApp:
raw_path: str
instance_object: App
@dataclass(frozen=True, slots=True)
class ResolvedEntrypoint:
resolved_source_path: str
instance: Callable[[], None] | App
class EntrypointResolver[T: (CallableEntryPoint, EntryPointAsApp)]:
def __init__(self, path_to_entrypoint: str):
self._path_to_entrypoint = path_to_entrypoint
def parse_entrypoint_with_type(
self,
entrypoint_object_name: str,
) -> T:
entrypoint_type: type[T] = get_args(self.__orig_class__)[0] # pyright: ignore[reportAttributeAccessIssue, reportUnknownMemberType]
if entrypoint_type is CallableEntryPoint:
return cast(T, self._parse_callable_entrypoint(entrypoint_object_name))
elif entrypoint_type is EntryPointAsApp:
return cast(T, self._parse_entrypoint_as_app(entrypoint_object_name))
raise NotImplementedError
def _parse_callable_entrypoint(self, entrypoint_object_name: str) -> CallableEntryPoint:
resolved_entrypoint = self._resolve_from_string(entrypoint_object_name)
instance_object = resolved_entrypoint.instance
if not callable(instance_object):
raise EntrypointNotCallableError(repr(instance_object))
instance_object_signature = inspect.signature(instance_object)
required_params = instance_object_signature.parameters
if required_params:
raise CallableEntrypointNotMatchRequiredSignatureError(repr(instance_object))
return CallableEntryPoint(raw_path=resolved_entrypoint.resolved_source_path, instance_object=instance_object)
def _parse_entrypoint_as_app(self, entrypoint_object_name: str) -> EntryPointAsApp:
resolved_entrypoint = self._resolve_from_string(entrypoint_object_name)
instance_object = resolved_entrypoint.instance
if not isinstance(instance_object, App):
raise EntrypointNotAppInstanceError(repr(instance_object))
return EntryPointAsApp(raw_path=resolved_entrypoint.resolved_source_path, instance_object=instance_object)
def _resolve_from_string(self, entrypoint_object_name: str) -> ResolvedEntrypoint:
raw_path = self._path_to_entrypoint
raw_path_as_dir = Path(raw_path).resolve()
if raw_path_as_dir.is_dir() and (raw_path_as_dir / "__main__.py").exists():
raw_path = str(raw_path_as_dir / "__main__.py")
is_file_path = bool(re.search(r"[\/\\]|\.py$", raw_path))
if is_file_path:
abs_path = Path(raw_path).resolve()
if not abs_path.exists():
raise ResolveFromStringError(f'File "{raw_path}" not found')
package_root = abs_path.parent
while (package_root / "__init__.py").exists():
package_root = package_root.parent
pkg_root_str = str(package_root)
if pkg_root_str not in sys.path:
sys.path.insert(0, pkg_root_str)
module_name = ".".join(abs_path.relative_to(package_root).with_suffix("").parts)
resolved_source_path = str(abs_path)
else:
module_name = raw_path
cwd_str = str(Path.cwd())
if cwd_str not in sys.path:
sys.path.insert(0, cwd_str)
resolved_source_path = module_name
try:
module = importlib.import_module(module_name)
except ImportError as e:
if not is_file_path and not module_name.endswith(".__main__"):
try:
main_module_name = f"{module_name}.__main__"
module = importlib.import_module(main_module_name)
module_name = main_module_name
except ImportError:
raise ResolveFromStringError(f'Cannot import module "{module_name}": {e}')
else:
raise ResolveFromStringError(f'Cannot import module "{module_name}": {e}')
if not is_file_path:
resolved_source_path = getattr(module, "__file__", resolved_source_path)
try:
instance = getattr(module, entrypoint_object_name)
except AttributeError:
raise ResolveFromStringError(f'"{entrypoint_object_name}" not found in "{raw_path}"')
return ResolvedEntrypoint(resolved_source_path, instance)
@@ -0,0 +1,26 @@
class ResolverError(Exception):
pass
class ResolveFromStringError(ResolverError):
pass
class EntrypointError(Exception):
def __init__(self, entrypoint_as_repr: str) -> None:
self.entrypoint_as_repr = entrypoint_as_repr
class EntrypointNotCallableError(EntrypointError):
def __str__(self):
return f"Entrypoint {self.entrypoint_as_repr} is not callable"
class CallableEntrypointNotMatchRequiredSignatureError(EntrypointError):
def __str__(self) -> str:
return f"Callable entrypoint {self.entrypoint_as_repr} not match with required signature Callable[[], ...]"
class EntrypointNotAppInstanceError(EntrypointError):
def __str__(self):
return f"Entrypoint {self.entrypoint_as_repr} is not instance of App"
@@ -0,0 +1,122 @@
__all__ = ['build_session', 'do_prompt']
from typing import Callable, Iterable
from prompt_toolkit import HTML, PromptSession
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
from prompt_toolkit.completion import CompleteEvent, Completer, Completion, ThreadedCompleter
from prompt_toolkit.cursor_shapes import CursorShape
from prompt_toolkit.document import Document
from prompt_toolkit.formatted_text import StyleAndTextTuples
from prompt_toolkit.history import FileHistory, History, InMemoryHistory, ThreadedHistory
from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent
from prompt_toolkit.lexers import Lexer
from prompt_toolkit.styles import Style
class CommandLexer(Lexer):
def __init__(self, valid_commands: set[str]) -> None:
self.valid_commands: set[str] = valid_commands
def lex_document(self, document: Document) -> Callable[[int], StyleAndTextTuples]:
def get_line_tokens(lineno: int) -> StyleAndTextTuples:
if lineno >= len(document.lines):
return []
line_text: str = document.lines[lineno]
if not line_text.strip():
return [("", line_text)]
first_word: str = line_text.split()[0] if line_text.split() else ""
if first_word in self.valid_commands:
return [("class:valid", line_text)]
else:
return [("class:invalid", line_text)]
return get_line_tokens
class HistoryCompleter(Completer):
def __init__(self, history_container: History, static_commands: set[str]) -> None:
self.history_container: History = history_container
self.static_commands: set[str] = static_commands
def get_completions(
self, document: Document, complete_event: CompleteEvent
) -> Iterable[Completion]:
text: str = document.text_before_cursor
history_items: set[str] = set(self.history_container.load_history_strings())
all_candidates: set[str] = history_items.union(self.static_commands)
matches: list[str] = sorted(cmd for cmd in all_candidates if cmd.startswith(text))
if not matches:
return
for match in matches:
yield Completion(match, start_position=-len(text), display=match)
@staticmethod
def _find_common_prefix(matches: list[str]) -> str:
if not matches:
return ""
common: str = matches[0]
for match in matches[1:]:
i: int = 0
while i < len(common) and i < len(match) and common[i] == match[i]:
i += 1
common = common[:i]
return common
def build_session(
history_filename: str | None,
autocomplete_button: str,
command_highlighting: bool,
auto_suggestions: bool,
all_commands: set[str],
) -> PromptSession[str]:
kb = KeyBindings()
def _(event: KeyPressEvent) -> None:
buff = event.app.current_buffer
if buff.complete_state:
buff.complete_next()
return
comps_gen = iter(buff.completer.get_completions(buff.document, CompleteEvent()))
try:
first = next(comps_gen)
except StopIteration:
return
try:
_ = next(comps_gen)
buff.start_completion(select_first=False)
except StopIteration:
buff.apply_completion(first)
kb.add(autocomplete_button)(_)
history: InMemoryHistory | ThreadedHistory
if history_filename:
history = ThreadedHistory(FileHistory(history_filename))
else:
history = InMemoryHistory()
style = Style.from_dict({"valid": "#00ff00", "invalid": "#ff0000"})
return PromptSession(
history=history,
completer=ThreadedCompleter(HistoryCompleter(history, all_commands)),
complete_while_typing=False,
key_bindings=kb,
auto_suggest=AutoSuggestFromHistory() if auto_suggestions else None,
style=style if command_highlighting else None,
lexer=CommandLexer(all_commands) if command_highlighting else None,
)
def do_prompt(session: PromptSession[str], prompt_text: str | HTML) -> str:
return session.prompt(
HTML(prompt_text) if isinstance(prompt_text, str) else prompt_text,
cursor=CursorShape.BLINKING_BEAM,
)
+21 -113
View File
@@ -1,86 +1,21 @@
from __future__ import annotations
__all__ = ["AutoCompleter"] __all__ = ["AutoCompleter"]
import sys import sys
from typing import Callable, Iterable from typing import TYPE_CHECKING
from prompt_toolkit import HTML, PromptSession if TYPE_CHECKING:
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory from prompt_toolkit import PromptSession, HTML
from prompt_toolkit.completion import (CompleteEvent, Completer, Completion,
ThreadedCompleter)
from prompt_toolkit.cursor_shapes import CursorShape
from prompt_toolkit.document import Document
from prompt_toolkit.formatted_text import StyleAndTextTuples
from prompt_toolkit.history import FileHistory, History, InMemoryHistory, ThreadedHistory
from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent
from prompt_toolkit.lexers import Lexer
from prompt_toolkit.styles import Style
class CommandLexer(Lexer):
def __init__(self, valid_commands: set[str]) -> None:
self.valid_commands: set[str] = valid_commands
def lex_document(self, document: Document) -> Callable[[int], StyleAndTextTuples]:
def get_line_tokens(lineno: int) -> StyleAndTextTuples:
if lineno >= len(document.lines):
return []
line_text: str = document.lines[lineno]
if not line_text.strip():
return [("", line_text)]
first_word: str = line_text.split()[0] if line_text.split() else ""
if first_word in self.valid_commands:
return [("class:valid", line_text)]
else:
return [("class:invalid", line_text)]
return get_line_tokens
class HistoryCompleter(Completer):
def __init__(self, history_container: History, static_commands: set[str]) -> None:
self.history_container: History = history_container
self.static_commands: set[str] = static_commands
def get_completions(self, document: Document, complete_event: CompleteEvent) -> Iterable[Completion]:
text: str = document.text_before_cursor
history_items: set[str] = set(self.history_container.load_history_strings())
all_candidates: set[str] = history_items.union(self.static_commands)
matches: list[str] = sorted(cmd for cmd in all_candidates if cmd.startswith(text))
if not matches:
return
for match in matches:
yield Completion(
match,
start_position=-len(text),
display=match
)
@staticmethod
def _find_common_prefix(matches: list[str]) -> str:
if not matches:
return ""
common: str = matches[0]
for match in matches[1:]:
i: int = 0
while i < len(common) and i < len(match) and common[i] == match[i]:
i += 1
common = common[:i]
return common
class AutoCompleter: class AutoCompleter:
def __init__( def __init__(
self, self,
history_filename: str | None = None, history_filename: str | None = None,
autocomplete_button: str = "tab", autocomplete_button: str = "tab",
command_highlighting: bool = True, command_highlighting: bool = True,
auto_suggestions: bool = True, auto_suggestions: bool = True,
) -> None: ) -> None:
self.history_filename: str | None = history_filename self.history_filename: str | None = history_filename
self.autocomplete_button: str = autocomplete_button self.autocomplete_button: str = autocomplete_button
@@ -95,41 +30,14 @@ class AutoCompleter:
self._fallback_mode = True self._fallback_mode = True
return return
kb = KeyBindings() from ._ext_features_impl import build_session
def _(event: KeyPressEvent) -> None: self._session = build_session(
buff = event.app.current_buffer self.history_filename,
if buff.complete_state: self.autocomplete_button,
buff.complete_next() self.command_highlighting,
return self.auto_suggestions,
comps_gen = iter(buff.completer.get_completions(buff.document, CompleteEvent())) all_commands
try:
first = next(comps_gen)
except StopIteration:
return
try:
_ = next(comps_gen)
buff.start_completion(select_first=False)
except StopIteration:
buff.apply_completion(first)
kb.add(self.autocomplete_button)(_)
history: InMemoryHistory | ThreadedHistory
if self.history_filename:
history = ThreadedHistory(FileHistory(self.history_filename))
else:
history = InMemoryHistory()
style = Style.from_dict({'valid': '#00ff00', 'invalid': '#ff0000'})
self._session = PromptSession(
history=history,
completer=ThreadedCompleter(HistoryCompleter(history, all_commands)),
complete_while_typing=False,
key_bindings=kb,
auto_suggest=AutoSuggestFromHistory() if self.auto_suggestions else None,
style=style if self.command_highlighting else None,
lexer=CommandLexer(all_commands) if self.command_highlighting else None,
) )
def prompt(self, prompt_text: str | HTML = ">>> ") -> str: def prompt(self, prompt_text: str | HTML = ">>> ") -> str:
@@ -137,7 +45,7 @@ class AutoCompleter:
return input(prompt_text if isinstance(prompt_text, str) else ">>> ") return input(prompt_text if isinstance(prompt_text, str) else ">>> ")
if self._session is None: if self._session is None:
raise RuntimeError("Call initial_setup() before using prompt()") raise RuntimeError("Call initial_setup() before using prompt()")
return self._session.prompt(
HTML(prompt_text) if isinstance(prompt_text, str) else prompt_text, from ._ext_features_impl import do_prompt
cursor=CursorShape.BLINKING_BEAM
) return do_prompt(self._session, prompt_text)
+4 -5
View File
@@ -3,8 +3,6 @@ __all__ = ["App"]
import difflib import difflib
from typing import Never, TypeAlias from typing import Never, TypeAlias
from rich.console import Console
from argenta.app.autocompleter import AutoCompleter from argenta.app.autocompleter import AutoCompleter
from argenta.app.behavior_handlers.models import (BehaviorHandlersFabric, from argenta.app.behavior_handlers.models import (BehaviorHandlersFabric,
BehaviorHandlersSettersMixin) BehaviorHandlersSettersMixin)
@@ -144,7 +142,7 @@ class BaseApp(BehaviorHandlersSettersMixin):
is_stdout_redirected_by_router=processing_router.is_redirect_stdout_disabled is_stdout_redirected_by_router=processing_router.is_redirect_stdout_disabled
) )
def _run_polling(self) -> None: def _run_repl(self) -> None:
self._viewer.view_initial_message(self._initial_message) self._viewer.view_initial_message(self._initial_message)
self._pre_cycle_setup() self._pre_cycle_setup()
while True: while True:
@@ -189,7 +187,7 @@ class App(BaseApp):
repeat_command_groups_printing: bool = False, repeat_command_groups_printing: bool = False,
override_system_messages: bool = False, override_system_messages: bool = False,
autocompleter: AutoCompleter | None = None, autocompleter: AutoCompleter | None = None,
printer: Printer = Console().print, printer: Printer | None = None,
) -> None: ) -> None:
""" """
Public. The essence of the application itself. Public. The essence of the application itself.
@@ -206,6 +204,7 @@ class App(BaseApp):
:param printer: system messages text output function :param printer: system messages text output function
:return: None :return: None
""" """
from rich.console import Console
super().__init__( super().__init__(
prompt=prompt, prompt=prompt,
initial_message=initial_message, initial_message=initial_message,
@@ -216,7 +215,7 @@ class App(BaseApp):
repeat_command_groups_printing=repeat_command_groups_printing, repeat_command_groups_printing=repeat_command_groups_printing,
override_system_messages=override_system_messages, override_system_messages=override_system_messages,
autocompleter=autocompleter or AutoCompleter(), autocompleter=autocompleter or AutoCompleter(),
printer=printer, printer=printer or Console().print,
) )
def include_router(self, router: Router) -> None: def include_router(self, router: Router) -> None:
+2 -5
View File
@@ -7,9 +7,8 @@ __all__ = [
"HandlerFunc", "HandlerFunc",
] ]
from typing import Any, Protocol, TypeVar from typing import Any, Protocol, TypeVar, Callable
from argenta.response import Response
T = TypeVar("T", contravariant=True) T = TypeVar("T", contravariant=True)
@@ -39,6 +38,4 @@ class DescriptionMessageGenerator(Protocol):
raise NotImplementedError raise NotImplementedError
class HandlerFunc(Protocol): type HandlerFunc = Callable[..., Any]
def __call__(self, response: Response, /, *args: Any, **kwargs: Any) -> None:
raise NotImplementedError
+8 -6
View File
@@ -1,5 +1,7 @@
__all__ = ["Orchestrator"] __all__ = ["Orchestrator"]
import os
from dishka import Provider, make_container from dishka import Provider, make_container
from argenta.app import App from argenta.app import App
@@ -7,13 +9,12 @@ from argenta.di.integration import setup_dishka
from argenta.di.providers import SystemProvider from argenta.di.providers import SystemProvider
from argenta.orchestrator.argparser import ArgParser from argenta.orchestrator.argparser import ArgParser
DEFAULT_ARGPARSER: ArgParser = ArgParser(processed_args=[])
class Orchestrator: class Orchestrator:
def __init__( def __init__(
self, self,
arg_parser: ArgParser = DEFAULT_ARGPARSER, arg_parser: ArgParser | None = None,
custom_providers: list[Provider] | None = None, custom_providers: list[Provider] | None = None,
auto_inject_handlers: bool = True, auto_inject_handlers: bool = True,
): ):
@@ -22,13 +23,14 @@ class Orchestrator:
:param arg_parser: Cmd argument parser and configurator at startup :param arg_parser: Cmd argument parser and configurator at startup
:return: None :return: None
""" """
self._arg_parser: ArgParser = arg_parser self._arg_parser: ArgParser | None = arg_parser if not os.getenv('RUN_FROM_ARGENTA_RUNNER') else None
self._custom_providers: list[Provider] = custom_providers or [] self._custom_providers: list[Provider] = custom_providers or []
self._auto_inject_handlers: bool = auto_inject_handlers self._auto_inject_handlers: bool = auto_inject_handlers
self._arg_parser._parse_args() # pyright: ignore[reportPrivateUsage] if self._arg_parser is not None:
self._arg_parser._parse_args() # pyright: ignore[reportPrivateUsage]
def start_polling(self, app: App) -> None: def run_repl(self, app: App) -> None:
""" """
Public. Starting the user input processing cycle Public. Starting the user input processing cycle
:param app: a running application :param app: a running application
@@ -39,4 +41,4 @@ class Orchestrator:
) )
setup_dishka(app, container, auto_inject=self._auto_inject_handlers) setup_dishka(app, container, auto_inject=self._auto_inject_handlers)
app._run_polling() app._run_repl() # pyright: ignore[reportPrivateUsage]
+6 -1
View File
@@ -1,6 +1,11 @@
from __future__ import annotations
__all__ = ["Response"] __all__ = ["Response"]
from dishka import Container from typing import TYPE_CHECKING
if TYPE_CHECKING:
from dishka import Container
from argenta.command import InputFlags from argenta.command import InputFlags
from argenta.response.status import ResponseStatus from argenta.response.status import ResponseStatus
+2 -3
View File
@@ -3,8 +3,6 @@ __all__ = ["Router"]
from inspect import get_annotations, getfullargspec, getsourcefile, getsourcelines from inspect import get_annotations, getfullargspec, getsourcefile, getsourcelines
from typing import Callable from typing import Callable
from rich.console import Console
from argenta.app.protocols import HandlerFunc from argenta.app.protocols import HandlerFunc
from argenta.command import Command, InputCommand, InputFlags from argenta.command import Command, InputCommand, InputFlags
from argenta.command.flag import ValidationStatus from argenta.command.flag import ValidationStatus
@@ -20,7 +18,7 @@ from argenta.router.exceptions import (RepeatedAliasNameException,
class Router: class Router:
def __init__( def __init__(
self, self,
title: str = "Default title", title: str = "Title",
*, *,
disable_redirect_stdout: bool = False, disable_redirect_stdout: bool = False,
): ):
@@ -175,6 +173,7 @@ class Router:
response_arg_annotation = func_annotations.get(response_arg) response_arg_annotation = func_annotations.get(response_arg)
if response_arg_annotation is not None and response_arg_annotation is not Response: if response_arg_annotation is not None and response_arg_annotation is not Response:
from rich.console import Console
source_line: int = getsourcelines(func)[1] source_line: int = getsourcelines(func)[1]
Console().print( Console().print(
f'\nFile "{getsourcefile(func)}", line {source_line}\n[b red]WARNING:[/b red] [i]The typehint ' f'\nFile "{getsourcefile(func)}", line {source_line}\n[b red]WARNING:[/b red] [i]The typehint '
@@ -38,7 +38,7 @@ def test_empty_input_triggers_empty_command_handler(monkeypatch: pytest.MonkeyPa
app = App(override_system_messages=True, printer=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
app.set_empty_command_handler(lambda: print('Empty input command')) app.set_empty_command_handler(lambda: print('Empty input command'))
orchestrator.start_polling(app) orchestrator.run_repl(app)
output = capsys.readouterr().out output = capsys.readouterr().out
@@ -64,7 +64,7 @@ def test_unknown_command_triggers_unknown_command_handler(monkeypatch: pytest.Mo
app = App(override_system_messages=True, printer=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.trigger}')) app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.trigger}'))
orchestrator.start_polling(app) orchestrator.run_repl(app)
output = capsys.readouterr().out output = capsys.readouterr().out
@@ -85,7 +85,7 @@ def test_mixed_valid_and_unknown_commands_handled_correctly(monkeypatch: pytest.
app = App(override_system_messages=True, printer=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.trigger}')) app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.trigger}'))
orchestrator.start_polling(app) orchestrator.run_repl(app)
output = capsys.readouterr().out output = capsys.readouterr().out
@@ -110,7 +110,7 @@ def test_multiple_commands_with_unknown_command_in_between(monkeypatch: pytest.M
app = App(override_system_messages=True, printer=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.trigger}')) app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.trigger}'))
orchestrator.start_polling(app) orchestrator.run_repl(app)
output = capsys.readouterr().out output = capsys.readouterr().out
@@ -137,7 +137,7 @@ def test_unregistered_flag_without_value_is_accessible(monkeypatch: pytest.Monke
app = App(override_system_messages=True, printer=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
orchestrator.start_polling(app) orchestrator.run_repl(app)
output = capsys.readouterr().out output = capsys.readouterr().out
@@ -161,7 +161,7 @@ def test_unregistered_flag_with_value_is_accessible(monkeypatch: pytest.MonkeyPa
app = App(override_system_messages=True, printer=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
orchestrator.start_polling(app) orchestrator.run_repl(app)
output = capsys.readouterr().out output = capsys.readouterr().out
@@ -184,7 +184,7 @@ def test_registered_and_unregistered_flags_coexist(monkeypatch: pytest.MonkeyPat
app = App(override_system_messages=True, printer=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
orchestrator.start_polling(app) orchestrator.run_repl(app)
output = capsys.readouterr().out output = capsys.readouterr().out
@@ -210,7 +210,7 @@ def test_flag_without_value_triggers_incorrect_syntax_handler(monkeypatch: pytes
app = App(override_system_messages=True, printer=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
app.set_incorrect_input_syntax_handler(lambda command: print(f'Incorrect flag syntax: "{command}"')) app.set_incorrect_input_syntax_handler(lambda command: print(f'Incorrect flag syntax: "{command}"'))
orchestrator.start_polling(app) orchestrator.run_repl(app)
output = capsys.readouterr().out output = capsys.readouterr().out
@@ -236,7 +236,7 @@ def test_repeated_flags_trigger_repeated_flags_handler(monkeypatch: pytest.Monke
app = App(override_system_messages=True, printer=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
app.set_repeated_input_flags_handler(lambda command: print(f'Repeated input flags: "{command}"')) app.set_repeated_input_flags_handler(lambda command: print(f'Repeated input flags: "{command}"'))
orchestrator.start_polling(app) orchestrator.run_repl(app)
output = capsys.readouterr().out output = capsys.readouterr().out
@@ -38,7 +38,7 @@ def test_simple_command_executes_successfully(monkeypatch: pytest.MonkeyPatch, c
app = App(override_system_messages=True, printer=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
orchestrator.start_polling(app) orchestrator.run_repl(app)
output = capsys.readouterr().out output = capsys.readouterr().out
@@ -62,7 +62,7 @@ def test_two_commands_execute_sequentially(monkeypatch: pytest.MonkeyPatch, caps
app = App(override_system_messages=True, printer=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
orchestrator.start_polling(app) orchestrator.run_repl(app)
output = capsys.readouterr().out output = capsys.readouterr().out
@@ -90,7 +90,7 @@ def test_three_commands_execute_sequentially(monkeypatch: pytest.MonkeyPatch, ca
app = App(override_system_messages=True, printer=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
orchestrator.start_polling(app) orchestrator.run_repl(app)
output = capsys.readouterr().out output = capsys.readouterr().out
@@ -118,7 +118,7 @@ def test_custom_flag_without_value_is_recognized(monkeypatch: pytest.MonkeyPatch
app = App(override_system_messages=True, printer=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
orchestrator.start_polling(app) orchestrator.run_repl(app)
output = capsys.readouterr().out output = capsys.readouterr().out
@@ -141,7 +141,7 @@ def test_custom_flag_with_regex_validation_accepts_valid_value(monkeypatch: pyte
app = App(override_system_messages=True, repeat_command_groups_printing=True, printer=print) app = App(override_system_messages=True, repeat_command_groups_printing=True, printer=print)
app.include_router(router) app.include_router(router)
orchestrator.start_polling(app) orchestrator.run_repl(app)
output = capsys.readouterr().out output = capsys.readouterr().out
@@ -169,7 +169,7 @@ def test_predefined_short_help_flag_is_recognized(monkeypatch: pytest.MonkeyPatc
app = App(override_system_messages=True, printer=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
orchestrator.start_polling(app) orchestrator.run_repl(app)
output = capsys.readouterr().out output = capsys.readouterr().out
@@ -192,7 +192,7 @@ def test_predefined_info_flag_is_recognized(monkeypatch: pytest.MonkeyPatch, cap
app = App(override_system_messages=True, printer=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
orchestrator.start_polling(app) orchestrator.run_repl(app)
output = capsys.readouterr().out output = capsys.readouterr().out
@@ -215,7 +215,7 @@ def test_predefined_host_flag_with_value_is_recognized(monkeypatch: pytest.Monke
app = App(override_system_messages=True, printer=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
orchestrator.start_polling(app) orchestrator.run_repl(app)
output = capsys.readouterr().out output = capsys.readouterr().out
@@ -244,7 +244,7 @@ def test_two_predefined_flags_are_recognized_together(monkeypatch: pytest.Monkey
app = App(override_system_messages=True, printer=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
orchestrator.start_polling(app) orchestrator.run_repl(app)
output = capsys.readouterr().out output = capsys.readouterr().out
+63 -103
View File
@@ -10,14 +10,11 @@ from prompt_toolkit.completion import CompleteEvent
from prompt_toolkit.document import Document from prompt_toolkit.document import Document
from prompt_toolkit.history import InMemoryHistory from prompt_toolkit.history import InMemoryHistory
from argenta.app.autocompleter.entity import ( from argenta.app.autocompleter._ext_features_impl import CommandLexer, HistoryCompleter
AutoCompleter, from argenta.app.autocompleter.entity import AutoCompleter
CommandLexer,
HistoryCompleter
)
COMMANDS: set[str] = {"start", "stop", "status"} COMMANDS: set[str] = {"start", "stop", "status"}
_IMPL = "argenta.app.autocompleter._ext_features_impl"
def test_autocompleter_initializes_with_default_params() -> None: def test_autocompleter_initializes_with_default_params() -> None:
@@ -33,7 +30,7 @@ def test_autocompleter_initializes_with_custom_params() -> None:
history_filename="test.txt", history_filename="test.txt",
autocomplete_button="c-space", autocomplete_button="c-space",
command_highlighting=False, command_highlighting=False,
auto_suggestions=False auto_suggestions=False,
) )
assert completer.history_filename == "test.txt" assert completer.history_filename == "test.txt"
assert completer.autocomplete_button == "c-space" assert completer.autocomplete_button == "c-space"
@@ -191,8 +188,10 @@ def test_history_completer_returns_early_when_no_matches() -> None:
def test_autocompleter_initial_setup_with_commands() -> None: def test_autocompleter_initial_setup_with_commands() -> None:
completer = AutoCompleter() completer = AutoCompleter()
with patch.object(sys.stdin, 'isatty', return_value=True), \ with (
patch('argenta.app.autocompleter.entity.PromptSession') as mock_session: patch.object(sys.stdin, "isatty", return_value=True),
patch(f"{_IMPL}.PromptSession") as mock_session,
):
completer.initial_setup({"start", "stop", "status"}) completer.initial_setup({"start", "stop", "status"})
assert completer._session is not None assert completer._session is not None
@@ -201,15 +200,17 @@ def test_autocompleter_initial_setup_with_commands() -> None:
def test_autocompleter_initial_setup_with_history_file() -> None: def test_autocompleter_initial_setup_with_history_file() -> None:
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt') as f: with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".txt") as f:
history_file = f.name history_file = f.name
try: try:
completer = AutoCompleter(history_filename=history_file) completer = AutoCompleter(history_filename=history_file)
with patch.object(sys.stdin, 'isatty', return_value=True), \ with (
patch('argenta.app.autocompleter.entity.PromptSession'), \ patch.object(sys.stdin, "isatty", return_value=True),
patch('argenta.app.autocompleter.entity.ThreadedHistory') as mock_threaded_history: patch(f"{_IMPL}.PromptSession"),
patch(f"{_IMPL}.ThreadedHistory") as mock_threaded_history,
):
completer.initial_setup({"start", "stop"}) completer.initial_setup({"start", "stop"})
assert completer._session is not None assert completer._session is not None
@@ -223,9 +224,11 @@ def test_autocompleter_initial_setup_with_history_file() -> None:
def test_autocompleter_initial_setup_without_history_file() -> None: def test_autocompleter_initial_setup_without_history_file() -> None:
completer = AutoCompleter(history_filename=None) completer = AutoCompleter(history_filename=None)
with patch.object(sys.stdin, 'isatty', return_value=True), \ with (
patch('argenta.app.autocompleter.entity.PromptSession'), \ patch.object(sys.stdin, "isatty", return_value=True),
patch('argenta.app.autocompleter.entity.InMemoryHistory') as mock_in_memory: patch(f"{_IMPL}.PromptSession"),
patch(f"{_IMPL}.InMemoryHistory") as mock_in_memory,
):
completer.initial_setup({"start", "stop"}) completer.initial_setup({"start", "stop"})
assert completer._session is not None assert completer._session is not None
@@ -236,8 +239,10 @@ def test_autocompleter_initial_setup_without_history_file() -> None:
def test_autocompleter_initial_setup_with_custom_autocomplete_button() -> None: def test_autocompleter_initial_setup_with_custom_autocomplete_button() -> None:
completer = AutoCompleter(autocomplete_button="c-space") completer = AutoCompleter(autocomplete_button="c-space")
with patch.object(sys.stdin, 'isatty', return_value=True), \ with (
patch('argenta.app.autocompleter.entity.PromptSession'): patch.object(sys.stdin, "isatty", return_value=True),
patch(f"{_IMPL}.PromptSession"),
):
completer.initial_setup({"start", "stop"}) completer.initial_setup({"start", "stop"})
assert completer._session is not None assert completer._session is not None
@@ -247,31 +252,34 @@ def test_autocompleter_initial_setup_with_custom_autocomplete_button() -> None:
def test_autocompleter_initial_setup_without_auto_suggestions() -> None: def test_autocompleter_initial_setup_without_auto_suggestions() -> None:
completer = AutoCompleter(auto_suggestions=False) completer = AutoCompleter(auto_suggestions=False)
with patch.object(sys.stdin, 'isatty', return_value=True), \ with (
patch('argenta.app.autocompleter.entity.PromptSession') as mock_session: patch.object(sys.stdin, "isatty", return_value=True),
patch(f"{_IMPL}.PromptSession") as mock_session,
):
completer.initial_setup({"start", "stop"}) completer.initial_setup({"start", "stop"})
assert completer._session is not None assert completer._session is not None
call_kwargs = mock_session.call_args[1] call_kwargs = mock_session.call_args[1]
assert call_kwargs['auto_suggest'] is None assert call_kwargs["auto_suggest"] is None
def test_autocompleter_initial_setup_without_command_highlighting() -> None: def test_autocompleter_initial_setup_without_command_highlighting() -> None:
completer = AutoCompleter(command_highlighting=False) completer = AutoCompleter(command_highlighting=False)
with patch.object(sys.stdin, 'isatty', return_value=True), \ with (
patch('argenta.app.autocompleter.entity.PromptSession') as mock_session: patch.object(sys.stdin, "isatty", return_value=True),
patch(f"{_IMPL}.PromptSession") as mock_session,
):
completer.initial_setup({"start", "stop"}) completer.initial_setup({"start", "stop"})
assert completer._session is not None assert completer._session is not None
call_kwargs = mock_session.call_args[1] call_kwargs = mock_session.call_args[1]
assert call_kwargs['style'] is None assert call_kwargs["style"] is None
assert call_kwargs['lexer'] is None assert call_kwargs["lexer"] is None
def test_autocompleter_key_binding_handler_with_complete_state() -> None: def _setup_captured_handler(completer: AutoCompleter) -> Callable[[Any], None] | None:
completer = AutoCompleter() """Вспомогательная функция: поднимает initial_setup и захватывает kb-хендлер."""
captured_handler: Callable[[Any], None] | None = None captured_handler: Callable[[Any], None] | None = None
def capture_kb_add(key: str) -> Callable[[Callable[[Any], None]], Callable[[Any], None]]: def capture_kb_add(key: str) -> Callable[[Callable[[Any], None]], Callable[[Any], None]]:
@@ -281,16 +289,22 @@ def test_autocompleter_key_binding_handler_with_complete_state() -> None:
return func return func
return decorator return decorator
with patch.object(sys.stdin, 'isatty', return_value=True), \ with (
patch('argenta.app.autocompleter.entity.PromptSession'), \ patch.object(sys.stdin, "isatty", return_value=True),
patch('argenta.app.autocompleter.entity.KeyBindings') as mock_kb_class: patch(f"{_IMPL}.PromptSession"),
patch(f"{_IMPL}.KeyBindings") as mock_kb_class,
):
mock_kb = MagicMock() mock_kb = MagicMock()
mock_kb.add = capture_kb_add mock_kb.add = capture_kb_add
mock_kb_class.return_value = mock_kb mock_kb_class.return_value = mock_kb
completer.initial_setup({"start", "stop"}) completer.initial_setup({"start", "stop"})
return captured_handler
def test_autocompleter_key_binding_handler_with_complete_state() -> None:
completer = AutoCompleter()
captured_handler = _setup_captured_handler(completer)
assert captured_handler is not None assert captured_handler is not None
mock_event = MagicMock() mock_event = MagicMock()
@@ -305,25 +319,8 @@ def test_autocompleter_key_binding_handler_with_complete_state() -> None:
def test_autocompleter_key_binding_handler_no_completions() -> None: def test_autocompleter_key_binding_handler_no_completions() -> None:
completer = AutoCompleter() completer = AutoCompleter()
captured_handler = _setup_captured_handler(completer)
captured_handler: Callable[[Any], None] | None = None assert captured_handler is not None
def capture_kb_add(key: str) -> Callable[[Callable[[Any], None]], Callable[[Any], None]]:
def decorator(func: Callable[[Any], None]) -> Callable[[Any], None]:
nonlocal captured_handler
captured_handler = func
return func
return decorator
with patch.object(sys.stdin, 'isatty', return_value=True), \
patch('argenta.app.autocompleter.entity.PromptSession'), \
patch('argenta.app.autocompleter.entity.KeyBindings') as mock_kb_class:
mock_kb = MagicMock()
mock_kb.add = capture_kb_add
mock_kb_class.return_value = mock_kb
completer.initial_setup({"start", "stop"})
mock_event = MagicMock() mock_event = MagicMock()
mock_buff = MagicMock() mock_buff = MagicMock()
@@ -333,7 +330,6 @@ def test_autocompleter_key_binding_handler_no_completions() -> None:
mock_buff.completer = mock_completer mock_buff.completer = mock_completer
mock_event.app.current_buffer = mock_buff mock_event.app.current_buffer = mock_buff
assert captured_handler is not None
captured_handler(mock_event) captured_handler(mock_event)
mock_buff.start_completion.assert_not_called() mock_buff.start_completion.assert_not_called()
@@ -342,25 +338,8 @@ def test_autocompleter_key_binding_handler_no_completions() -> None:
def test_autocompleter_key_binding_handler_single_completion() -> None: def test_autocompleter_key_binding_handler_single_completion() -> None:
completer = AutoCompleter() completer = AutoCompleter()
captured_handler = _setup_captured_handler(completer)
captured_handler: Callable[[Any], None] | None = None assert captured_handler is not None
def capture_kb_add(key: str) -> Callable[[Callable[[Any], None]], Callable[[Any], None]]:
def decorator(func: Callable[[Any], None]) -> Callable[[Any], None]:
nonlocal captured_handler
captured_handler = func
return func
return decorator
with patch.object(sys.stdin, 'isatty', return_value=True), \
patch('argenta.app.autocompleter.entity.PromptSession'), \
patch('argenta.app.autocompleter.entity.KeyBindings') as mock_kb_class:
mock_kb = MagicMock()
mock_kb.add = capture_kb_add
mock_kb_class.return_value = mock_kb
completer.initial_setup({"start", "stop"})
mock_event = MagicMock() mock_event = MagicMock()
mock_buff = MagicMock() mock_buff = MagicMock()
@@ -371,7 +350,6 @@ def test_autocompleter_key_binding_handler_single_completion() -> None:
mock_buff.completer = mock_completer mock_buff.completer = mock_completer
mock_event.app.current_buffer = mock_buff mock_event.app.current_buffer = mock_buff
assert captured_handler is not None
captured_handler(mock_event) captured_handler(mock_event)
mock_buff.apply_completion.assert_called_once_with(mock_completion) mock_buff.apply_completion.assert_called_once_with(mock_completion)
@@ -380,25 +358,8 @@ def test_autocompleter_key_binding_handler_single_completion() -> None:
def test_autocompleter_key_binding_handler_multiple_completions() -> None: def test_autocompleter_key_binding_handler_multiple_completions() -> None:
completer = AutoCompleter() completer = AutoCompleter()
captured_handler = _setup_captured_handler(completer)
captured_handler: Callable[[Any], None] | None = None assert captured_handler is not None
def capture_kb_add(key: str) -> Callable[[Callable[[Any], None]], Callable[[Any], None]]:
def decorator(func: Callable[[Any], None]) -> Callable[[Any], None]:
nonlocal captured_handler
captured_handler = func
return func
return decorator
with patch.object(sys.stdin, 'isatty', return_value=True), \
patch('argenta.app.autocompleter.entity.PromptSession'), \
patch('argenta.app.autocompleter.entity.KeyBindings') as mock_kb_class:
mock_kb = MagicMock()
mock_kb.add = capture_kb_add
mock_kb_class.return_value = mock_kb
completer.initial_setup({"start", "stop"})
mock_event = MagicMock() mock_event = MagicMock()
mock_buff = MagicMock() mock_buff = MagicMock()
@@ -410,7 +371,6 @@ def test_autocompleter_key_binding_handler_multiple_completions() -> None:
mock_buff.completer = mock_completer mock_buff.completer = mock_completer
mock_event.app.current_buffer = mock_buff mock_event.app.current_buffer = mock_buff
assert captured_handler is not None
captured_handler(mock_event) captured_handler(mock_event)
mock_buff.start_completion.assert_called_once_with(select_first=False) mock_buff.start_completion.assert_called_once_with(select_first=False)
@@ -420,43 +380,43 @@ def test_autocompleter_key_binding_handler_multiple_completions() -> None:
def test_autocompleter_prompt_in_fallback_mode_with_string() -> None: def test_autocompleter_prompt_in_fallback_mode_with_string() -> None:
completer = AutoCompleter() completer = AutoCompleter()
with patch.object(sys.stdin, 'isatty', return_value=False): with patch.object(sys.stdin, "isatty", return_value=False):
completer.initial_setup({"start", "stop"}) completer.initial_setup({"start", "stop"})
assert completer._fallback_mode is True assert completer._fallback_mode is True
with patch('builtins.input', return_value='test input'): with patch("builtins.input", return_value="test input"):
result = completer.prompt(">>> ") result = completer.prompt(">>> ")
assert result == 'test input' assert result == "test input"
def test_autocompleter_prompt_in_fallback_mode_with_html() -> None: def test_autocompleter_prompt_in_fallback_mode_with_html() -> None:
completer = AutoCompleter() completer = AutoCompleter()
with patch.object(sys.stdin, 'isatty', return_value=False): with patch.object(sys.stdin, "isatty", return_value=False):
completer.initial_setup({"start", "stop"}) completer.initial_setup({"start", "stop"})
assert completer._fallback_mode is True assert completer._fallback_mode is True
with patch('builtins.input', return_value='test input'): with patch("builtins.input", return_value="test input"):
result = completer.prompt(HTML("<b>>>> </b>")) result = completer.prompt(HTML("<b>>>> </b>"))
assert result == 'test input' assert result == "test input"
def test_autocompleter_prompt_with_html_in_normal_mode() -> None: def test_autocompleter_prompt_with_html_in_normal_mode() -> None:
completer = AutoCompleter() completer = AutoCompleter()
mock_session = MagicMock() mock_session = MagicMock()
mock_session.prompt.return_value = 'test result' mock_session.prompt.return_value = "test result"
completer._session = mock_session completer._session = mock_session
completer._fallback_mode = False completer._fallback_mode = False
html_prompt = HTML("<b>>>> </b>") html_prompt = HTML("<b>>>> </b>")
result = completer.prompt(html_prompt) result = completer.prompt(html_prompt)
assert result == 'test result' assert result == "test result"
mock_session.prompt.assert_called_once() mock_session.prompt.assert_called_once()
call_args = mock_session.prompt.call_args call_args = mock_session.prompt.call_args
assert call_args[0][0] == html_prompt assert call_args[0][0] == html_prompt
@@ -466,13 +426,13 @@ def test_autocompleter_prompt_with_string_in_normal_mode() -> None:
completer = AutoCompleter() completer = AutoCompleter()
mock_session = MagicMock() mock_session = MagicMock()
mock_session.prompt.return_value = 'test result' mock_session.prompt.return_value = "test result"
completer._session = mock_session completer._session = mock_session
completer._fallback_mode = False completer._fallback_mode = False
result = completer.prompt(">>> ") result = completer.prompt(">>> ")
assert result == 'test result' assert result == "test result"
mock_session.prompt.assert_called_once() mock_session.prompt.assert_called_once()
call_args = mock_session.prompt.call_args call_args = mock_session.prompt.call_args
assert isinstance(call_args[0][0], HTML) assert isinstance(call_args[0][0], HTML)
+35 -36
View File
@@ -45,12 +45,11 @@ def sample_router() -> Router:
# ============================================================================ # ============================================================================
def test_orchestrator_initializes_with_default_argparser(mocker: MockerFixture) -> None: def test_orchestrator_initializes_with_no_argparser(mocker: MockerFixture) -> None:
"""Test Orchestrator initialization with default ArgParser""" """Test Orchestrator initialization with no ArgParser"""
mocker.patch('sys.argv', ['test_program']) mocker.patch('sys.argv', ['test_program'])
orchestrator = Orchestrator() orchestrator = Orchestrator()
assert orchestrator._arg_parser is not None assert orchestrator._arg_parser is None
assert isinstance(orchestrator._arg_parser, ArgParser)
def test_orchestrator_initializes_with_custom_argparser(mock_argparser: ArgParser) -> None: def test_orchestrator_initializes_with_custom_argparser(mock_argparser: ArgParser) -> None:
@@ -89,80 +88,80 @@ def test_orchestrator_parses_args_on_initialization(mocker: MockerFixture, mock_
# ============================================================================ # ============================================================================
# Tests for start_polling method # Tests for run_repl method
# ============================================================================ # ============================================================================
def test_start_polling_creates_dishka_container( def test_run_repl_creates_dishka_container(
mocker: MockerFixture, mock_argparser: ArgParser, sample_app: App mocker: MockerFixture, mock_argparser: ArgParser, sample_app: App
) -> None: ) -> None:
"""Test that start_polling creates a dishka container""" """Test that run_repl creates a dishka container"""
mock_make_container = mocker.patch('argenta.orchestrator.entity.make_container') mock_make_container = mocker.patch('argenta.orchestrator.entity.make_container')
_mock_setup_dishka = mocker.patch('argenta.orchestrator.entity.setup_dishka') _mock_setup_dishka = mocker.patch('argenta.orchestrator.entity.setup_dishka')
mocker.patch.object(sample_app, '_run_polling') mocker.patch.object(sample_app, '_run_repl')
orchestrator = Orchestrator(arg_parser=mock_argparser) orchestrator = Orchestrator(arg_parser=mock_argparser)
orchestrator.start_polling(sample_app) orchestrator.run_repl(sample_app)
mock_make_container.assert_called_once() mock_make_container.assert_called_once()
assert mock_make_container.call_args[1]['context'] == {ArgParser: mock_argparser} assert mock_make_container.call_args[1]['context'] == {ArgParser: mock_argparser}
def test_start_polling_calls_setup_dishka_with_auto_inject_enabled( def test_run_repl_calls_setup_dishka_with_auto_inject_enabled(
mocker: MockerFixture, mock_argparser: ArgParser, sample_app: App mocker: MockerFixture, mock_argparser: ArgParser, sample_app: App
) -> None: ) -> None:
"""Test that start_polling calls setup_dishka with auto_inject=True""" """Test that run_repl calls setup_dishka with auto_inject=True"""
mock_container = mocker.MagicMock() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType] mock_container = mocker.MagicMock() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
mocker.patch('argenta.orchestrator.entity.make_container', return_value=mock_container) mocker.patch('argenta.orchestrator.entity.make_container', return_value=mock_container)
mock_setup_dishka = mocker.patch('argenta.orchestrator.entity.setup_dishka') mock_setup_dishka = mocker.patch('argenta.orchestrator.entity.setup_dishka')
mocker.patch.object(sample_app, '_run_polling') mocker.patch.object(sample_app, '_run_repl')
orchestrator = Orchestrator(arg_parser=mock_argparser, auto_inject_handlers=True) orchestrator = Orchestrator(arg_parser=mock_argparser, auto_inject_handlers=True)
orchestrator.start_polling(sample_app) orchestrator.run_repl(sample_app)
mock_setup_dishka.assert_called_once_with(sample_app, mock_container, auto_inject=True) mock_setup_dishka.assert_called_once_with(sample_app, mock_container, auto_inject=True)
def test_start_polling_calls_setup_dishka_with_auto_inject_disabled( def test_run_repl_calls_setup_dishka_with_auto_inject_disabled(
mocker: MockerFixture, mock_argparser: ArgParser, sample_app: App mocker: MockerFixture, mock_argparser: ArgParser, sample_app: App
) -> None: ) -> None:
"""Test that start_polling calls setup_dishka with auto_inject=False""" """Test that run_repl calls setup_dishka with auto_inject=False"""
mock_container = mocker.MagicMock() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType] mock_container = mocker.MagicMock() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
mocker.patch('argenta.orchestrator.entity.make_container', return_value=mock_container) mocker.patch('argenta.orchestrator.entity.make_container', return_value=mock_container)
mock_setup_dishka = mocker.patch('argenta.orchestrator.entity.setup_dishka') mock_setup_dishka = mocker.patch('argenta.orchestrator.entity.setup_dishka')
mocker.patch.object(sample_app, '_run_polling') mocker.patch.object(sample_app, '_run_repl')
orchestrator = Orchestrator(arg_parser=mock_argparser, auto_inject_handlers=False) orchestrator = Orchestrator(arg_parser=mock_argparser, auto_inject_handlers=False)
orchestrator.start_polling(sample_app) orchestrator.run_repl(sample_app)
mock_setup_dishka.assert_called_once_with(sample_app, mock_container, auto_inject=False) mock_setup_dishka.assert_called_once_with(sample_app, mock_container, auto_inject=False)
def test_start_polling_calls_app_run_polling( def test_run_repl_calls_app_run_repl(
mocker: MockerFixture, mock_argparser: ArgParser, sample_app: App mocker: MockerFixture, mock_argparser: ArgParser, sample_app: App
) -> None: ) -> None:
"""Test that start_polling calls app.run_polling()""" """Test that run_repl calls app.run_polling()"""
mocker.patch('argenta.orchestrator.entity.make_container') mocker.patch('argenta.orchestrator.entity.make_container')
mocker.patch('argenta.orchestrator.entity.setup_dishka') mocker.patch('argenta.orchestrator.entity.setup_dishka')
mock_run_polling = mocker.patch.object(sample_app, '_run_polling') mock_run_repl = mocker.patch.object(sample_app, '_run_repl')
orchestrator = Orchestrator(arg_parser=mock_argparser) orchestrator = Orchestrator(arg_parser=mock_argparser)
orchestrator.start_polling(sample_app) orchestrator.run_repl(sample_app)
mock_run_polling.assert_called_once() mock_run_repl.assert_called_once()
def test_start_polling_includes_custom_providers_in_container( def test_run_repl_includes_custom_providers_in_container(
mocker: MockerFixture, mock_argparser: ArgParser, sample_app: App mocker: MockerFixture, mock_argparser: ArgParser, sample_app: App
) -> None: ) -> None:
"""Test that start_polling includes custom providers in container""" """Test that run_repl includes custom providers in container"""
custom_provider = Provider() custom_provider = Provider()
mock_make_container = mocker.patch('argenta.orchestrator.entity.make_container') mock_make_container = mocker.patch('argenta.orchestrator.entity.make_container')
mocker.patch('argenta.orchestrator.entity.setup_dishka') mocker.patch('argenta.orchestrator.entity.setup_dishka')
mocker.patch.object(sample_app, '_run_polling') mocker.patch.object(sample_app, '_run_repl')
orchestrator = Orchestrator(arg_parser=mock_argparser, custom_providers=[custom_provider]) orchestrator = Orchestrator(arg_parser=mock_argparser, custom_providers=[custom_provider])
orchestrator.start_polling(sample_app) orchestrator.run_repl(sample_app)
# Check that custom_provider was passed to make_container # Check that custom_provider was passed to make_container
call_args = mock_make_container.call_args[0] call_args = mock_make_container.call_args[0]
@@ -180,14 +179,14 @@ def test_orchestrator_integrates_with_app_with_router(
"""Test that Orchestrator properly integrates with App that has routers""" """Test that Orchestrator properly integrates with App that has routers"""
mocker.patch('argenta.orchestrator.entity.make_container') mocker.patch('argenta.orchestrator.entity.make_container')
mocker.patch('argenta.orchestrator.entity.setup_dishka') mocker.patch('argenta.orchestrator.entity.setup_dishka')
mock_run_polling = mocker.patch.object(sample_app, '_run_polling') mock_run_repl = mocker.patch.object(sample_app, '_run_repl')
sample_app.include_router(sample_router) sample_app.include_router(sample_router)
orchestrator = Orchestrator(arg_parser=mock_argparser) orchestrator = Orchestrator(arg_parser=mock_argparser)
orchestrator.start_polling(sample_app) orchestrator.run_repl(sample_app)
mock_run_polling.assert_called_once() mock_run_repl.assert_called_once()
assert len(sample_app.registered_routers.registered_routers) == 1 assert len(sample_app.registered_routers.registered_routers) == 1
@@ -202,10 +201,10 @@ def test_orchestrator_passes_argparser_to_container_context(
"""Test that Orchestrator passes ArgParser instance to container context""" """Test that Orchestrator passes ArgParser instance to container context"""
mock_make_container = mocker.patch('argenta.orchestrator.entity.make_container') mock_make_container = mocker.patch('argenta.orchestrator.entity.make_container')
mocker.patch('argenta.orchestrator.entity.setup_dishka') mocker.patch('argenta.orchestrator.entity.setup_dishka')
mocker.patch.object(sample_app, '_run_polling') mocker.patch.object(sample_app, '_run_repl')
orchestrator = Orchestrator(arg_parser=mock_argparser) orchestrator = Orchestrator(arg_parser=mock_argparser)
orchestrator.start_polling(sample_app) orchestrator.run_repl(sample_app)
# Verify that ArgParser was passed in context # Verify that ArgParser was passed in context
call_kwargs = mock_make_container.call_args[1] call_kwargs = mock_make_container.call_args[1]
@@ -219,18 +218,18 @@ def test_orchestrator_passes_argparser_to_container_context(
# ============================================================================ # ============================================================================
def test_orchestrator_handles_app_run_polling_exception( def test_orchestrator_handles_app_run_repl_exception(
mocker: MockerFixture, mock_argparser: ArgParser, sample_app: App mocker: MockerFixture, mock_argparser: ArgParser, sample_app: App
) -> None: ) -> None:
"""Test that Orchestrator propagates exceptions from app.run_polling()""" """Test that Orchestrator propagates exceptions from app.run_polling()"""
mocker.patch('argenta.orchestrator.entity.make_container') mocker.patch('argenta.orchestrator.entity.make_container')
mocker.patch('argenta.orchestrator.entity.setup_dishka') mocker.patch('argenta.orchestrator.entity.setup_dishka')
mocker.patch.object(sample_app, '_run_polling', side_effect=RuntimeError("Test error")) mocker.patch.object(sample_app, '_run_repl', side_effect=RuntimeError("Test error"))
orchestrator = Orchestrator(arg_parser=mock_argparser) orchestrator = Orchestrator(arg_parser=mock_argparser)
with pytest.raises(RuntimeError, match="Test error"): with pytest.raises(RuntimeError, match="Test error"):
orchestrator.start_polling(sample_app) orchestrator.run_repl(sample_app)
# ============================================================================ # ============================================================================
@@ -246,13 +245,13 @@ def test_orchestrator_accepts_multiple_custom_providers(
provider2 = Provider() provider2 = Provider()
mock_make_container = mocker.patch('argenta.orchestrator.entity.make_container') mock_make_container = mocker.patch('argenta.orchestrator.entity.make_container')
mocker.patch('argenta.orchestrator.entity.setup_dishka') mocker.patch('argenta.orchestrator.entity.setup_dishka')
mocker.patch.object(sample_app, '_run_polling') mocker.patch.object(sample_app, '_run_repl')
orchestrator = Orchestrator( orchestrator = Orchestrator(
arg_parser=mock_argparser, arg_parser=mock_argparser,
custom_providers=[provider1, provider2] custom_providers=[provider1, provider2]
) )
orchestrator.start_polling(sample_app) orchestrator.run_repl(sample_app)
call_args = mock_make_container.call_args[0] call_args = mock_make_container.call_args[0]
assert provider1 in call_args assert provider1 in call_args
Generated
+349 -64
View File
@@ -1,6 +1,6 @@
version = 1 version = 1
revision = 3 revision = 3
requires-python = ">=3.12" requires-python = ">=3.12, <3.15"
[[package]] [[package]]
name = "aiosqlite" name = "aiosqlite"
@@ -48,12 +48,23 @@ dependencies = [
{ name = "rich" }, { name = "rich" },
] ]
[package.optional-dependencies]
cli = [
{ name = "nuitka", extra = ["onefile"] },
{ name = "typer" },
]
[package.dev-dependencies] [package.dev-dependencies]
dev = [ dev = [
{ name = "cairosvg" },
{ name = "esbonio" }, { name = "esbonio" },
{ name = "isort" }, { name = "isort" },
{ name = "matplotlib" },
{ name = "mypy" }, { name = "mypy" },
{ name = "psutil" },
{ name = "py-cpuinfo" },
{ name = "pyfakefs" }, { name = "pyfakefs" },
{ name = "pygal" },
{ name = "pytest" }, { name = "pytest" },
{ name = "pytest-cov" }, { name = "pytest-cov" },
{ name = "pytest-mock" }, { name = "pytest-mock" },
@@ -78,9 +89,11 @@ linters = [
{ name = "wemake-python-styleguide" }, { name = "wemake-python-styleguide" },
] ]
metrics = [ metrics = [
{ name = "cairosvg" },
{ name = "matplotlib" }, { name = "matplotlib" },
{ name = "psutil" }, { name = "psutil" },
{ name = "py-cpuinfo" }, { name = "py-cpuinfo" },
{ name = "pygal" },
] ]
tests = [ tests = [
{ name = "pyfakefs" }, { name = "pyfakefs" },
@@ -96,16 +109,24 @@ typecheckers = [
requires-dist = [ requires-dist = [
{ name = "art", specifier = ">=6.4,<7.0" }, { name = "art", specifier = ">=6.4,<7.0" },
{ name = "dishka", specifier = ">=1.7.2" }, { name = "dishka", specifier = ">=1.7.2" },
{ name = "nuitka", extras = ["onefile"], marker = "extra == 'cli'", specifier = ">=4.0.5" },
{ name = "prompt-toolkit", specifier = ">=3.0.52" }, { name = "prompt-toolkit", specifier = ">=3.0.52" },
{ name = "rich", specifier = ">=14.0.0,<15.0.0" }, { name = "rich", specifier = ">=14.0.0,<15.0.0" },
{ name = "typer", marker = "extra == 'cli'", specifier = ">=0.9,!=0.12,<=0.21.1" },
] ]
provides-extras = ["cli"]
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [ dev = [
{ name = "cairosvg", specifier = ">=2.8.2" },
{ name = "esbonio", specifier = ">=1.0.0" }, { name = "esbonio", specifier = ">=1.0.0" },
{ name = "isort", specifier = ">=7.0.0" }, { name = "isort", specifier = ">=7.0.0" },
{ name = "matplotlib", specifier = ">=3.10.8" },
{ name = "mypy", specifier = ">=1.14.1" }, { name = "mypy", specifier = ">=1.14.1" },
{ name = "psutil", specifier = ">=7.2.1" },
{ name = "py-cpuinfo", specifier = ">=9.0.0" },
{ name = "pyfakefs", specifier = ">=5.5.0" }, { name = "pyfakefs", specifier = ">=5.5.0" },
{ name = "pygal", specifier = ">=3.1.0" },
{ name = "pytest", specifier = ">=8.3.2" }, { name = "pytest", specifier = ">=8.3.2" },
{ name = "pytest-cov", specifier = ">=7.0.0" }, { name = "pytest-cov", specifier = ">=7.0.0" },
{ name = "pytest-mock", specifier = ">=3.15.1" }, { name = "pytest-mock", specifier = ">=3.15.1" },
@@ -130,9 +151,11 @@ linters = [
{ name = "wemake-python-styleguide", specifier = ">=0.17.0" }, { name = "wemake-python-styleguide", specifier = ">=0.17.0" },
] ]
metrics = [ metrics = [
{ name = "cairosvg", specifier = ">=2.8.2" },
{ name = "matplotlib", specifier = ">=3.10.8" }, { name = "matplotlib", specifier = ">=3.10.8" },
{ name = "psutil", specifier = ">=7.2.1" }, { name = "psutil", specifier = ">=7.2.1" },
{ name = "py-cpuinfo", specifier = ">=9.0.0" }, { name = "py-cpuinfo", specifier = ">=9.0.0" },
{ name = "pygal", specifier = ">=3.1.0" },
] ]
tests = [ tests = [
{ name = "pyfakefs", specifier = ">=5.5.0" }, { name = "pyfakefs", specifier = ">=5.5.0" },
@@ -169,6 +192,34 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" },
] ]
[[package]]
name = "cairocffi"
version = "1.7.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi" },
]
sdist = { url = "https://files.pythonhosted.org/packages/70/c5/1a4dc131459e68a173cbdab5fad6b524f53f9c1ef7861b7698e998b837cc/cairocffi-1.7.1.tar.gz", hash = "sha256:2e48ee864884ec4a3a34bfa8c9ab9999f688286eb714a15a43ec9d068c36557b", size = 88096, upload-time = "2024-06-18T10:56:06.741Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/93/d8/ba13451aa6b745c49536e87b6bf8f629b950e84bd0e8308f7dc6883b67e2/cairocffi-1.7.1-py3-none-any.whl", hash = "sha256:9803a0e11f6c962f3b0ae2ec8ba6ae45e957a146a004697a1ac1bbf16b073b3f", size = 75611, upload-time = "2024-06-18T10:55:59.489Z" },
]
[[package]]
name = "cairosvg"
version = "2.8.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cairocffi" },
{ name = "cssselect2" },
{ name = "defusedxml" },
{ name = "pillow" },
{ name = "tinycss2" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ab/b9/5106168bd43d7cd8b7cc2a2ee465b385f14b63f4c092bb89eee2d48c8e67/cairosvg-2.8.2.tar.gz", hash = "sha256:07cbf4e86317b27a92318a4cac2a4bb37a5e9c1b8a27355d06874b22f85bef9f", size = 8398590, upload-time = "2025-05-15T06:56:32.653Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/67/48/816bd4aaae93dbf9e408c58598bc32f4a8c65f4b86ab560864cb3ee60adb/cairosvg-2.8.2-py3-none-any.whl", hash = "sha256:eab46dad4674f33267a671dce39b64be245911c901c70d65d2b7b0821e852bf5", size = 45773, upload-time = "2025-05-15T06:56:28.552Z" },
]
[[package]] [[package]]
name = "cattrs" name = "cattrs"
version = "25.2.0" version = "25.2.0"
@@ -191,6 +242,63 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" }, { url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" },
] ]
[[package]]
name = "cffi"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
{ url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
{ url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
{ url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
{ url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
{ url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
{ url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
{ url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
{ url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
{ url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
{ url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
{ url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
{ url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
{ url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
{ url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
{ url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
{ url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
{ url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
{ url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
{ url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
{ url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
{ url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
{ url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
{ url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
{ url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
{ url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
{ url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
{ url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
{ url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
{ url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
{ url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
{ url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
{ url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
{ url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
{ url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
{ url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
{ url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
{ url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
{ url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
{ url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
{ url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
{ url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
{ url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
{ url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
{ url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
]
[[package]] [[package]]
name = "charset-normalizer" name = "charset-normalizer"
version = "3.4.4" version = "3.4.4"
@@ -421,6 +529,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5f/04/642c1d8a448ae5ea1369eac8495740a79eb4e581a9fb0cbdce56bbf56da1/coverage-7.11.0-py3-none-any.whl", hash = "sha256:4b7589765348d78fb4e5fb6ea35d07564e387da2fc5efff62e0222971f155f68", size = 207761, upload-time = "2025-10-15T15:15:06.439Z" }, { url = "https://files.pythonhosted.org/packages/5f/04/642c1d8a448ae5ea1369eac8495740a79eb4e581a9fb0cbdce56bbf56da1/coverage-7.11.0-py3-none-any.whl", hash = "sha256:4b7589765348d78fb4e5fb6ea35d07564e387da2fc5efff62e0222971f155f68", size = 207761, upload-time = "2025-10-15T15:15:06.439Z" },
] ]
[[package]]
name = "cssselect2"
version = "0.9.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "tinycss2" },
{ name = "webencodings" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e0/20/92eaa6b0aec7189fa4b75c890640e076e9e793095721db69c5c81142c2e1/cssselect2-0.9.0.tar.gz", hash = "sha256:759aa22c216326356f65e62e791d66160a0f9c91d1424e8d8adc5e74dddfc6fb", size = 35595, upload-time = "2026-02-12T17:16:39.614Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/21/0e/8459ca4413e1a21a06c97d134bfaf18adfd27cea068813dc0faae06cbf00/cssselect2-0.9.0-py3-none-any.whl", hash = "sha256:6a99e5f91f9a016a304dd929b0966ca464bcfda15177b6fb4a118fc0fb5d9563", size = 15453, upload-time = "2026-02-12T17:16:38.317Z" },
]
[[package]] [[package]]
name = "cycler" name = "cycler"
version = "0.12.1" version = "0.12.1"
@@ -430,6 +551,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" },
] ]
[[package]]
name = "defusedxml"
version = "0.7.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" },
]
[[package]] [[package]]
name = "dishka" name = "dishka"
version = "1.7.2" version = "1.7.2"
@@ -546,6 +676,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" }, { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" },
] ]
[[package]]
name = "importlib-metadata"
version = "8.7.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "zipp" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" },
]
[[package]] [[package]]
name = "iniconfig" name = "iniconfig"
version = "2.1.0" version = "2.1.0"
@@ -849,6 +991,17 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
] ]
[[package]]
name = "nuitka"
version = "4.0.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f8/40/f73e578922084f9e465b30abdb9963aadcc087b5a9399033472d9ef641ab/nuitka-4.0.5.tar.gz", hash = "sha256:45e7d90266e76fe64eeb8d196c17666d7cd7cffbf68d6a24f233c3c03b6feaa8", size = 4420878, upload-time = "2026-03-12T10:03:39.562Z" }
[package.optional-dependencies]
onefile = [
{ name = "zstandard" },
]
[[package]] [[package]]
name = "numpy" name = "numpy"
version = "2.4.1" version = "2.4.1"
@@ -930,71 +1083,71 @@ wheels = [
[[package]] [[package]]
name = "pillow" name = "pillow"
version = "12.2.0" version = "12.1.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, { url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" },
{ url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, { url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" },
{ url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, { url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" },
{ url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, { url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" },
{ url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, { url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" },
{ url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, { url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" },
{ url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, { url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" },
{ url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, { url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" },
{ url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, { url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" },
{ url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, { url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" },
{ url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, { url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" },
{ url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, { url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" },
{ url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, { url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" },
{ url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, { url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" },
{ url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, { url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" },
{ url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, { url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" },
{ url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, { url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" },
{ url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, { url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" },
{ url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, { url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" },
{ url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, { url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" },
{ url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, { url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" },
{ url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, { url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" },
{ url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, { url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" },
{ url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, { url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" },
{ url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, { url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" },
{ url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, { url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" },
{ url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, { url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" },
{ url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, { url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" },
{ url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, { url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" },
{ url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, { url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" },
{ url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, { url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" },
{ url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, { url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" },
{ url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, { url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" },
{ url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, { url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" },
{ url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, { url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" },
{ url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, { url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" },
{ url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, { url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" },
{ url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, { url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" },
{ url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, { url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" },
{ url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, { url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" },
{ url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, { url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" },
{ url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, { url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" },
{ url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, { url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" },
{ url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, { url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" },
{ url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, { url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" },
{ url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, { url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" },
{ url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, { url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" },
{ url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, { url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" },
{ url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, { url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" },
{ url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, { url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" },
{ url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, { url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" },
{ url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, { url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" },
{ url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, { url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" },
{ url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, { url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" },
{ url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, { url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" },
{ url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, { url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" },
{ url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, { url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" },
{ url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, { url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" },
{ url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, { url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" },
{ url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, { url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" },
{ url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, { url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" },
] ]
[[package]] [[package]]
@@ -1073,6 +1226,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" }, { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" },
] ]
[[package]]
name = "pycparser"
version = "3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
]
[[package]] [[package]]
name = "pyfakefs" name = "pyfakefs"
version = "5.10.0" version = "5.10.0"
@@ -1091,6 +1253,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" }, { url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" },
] ]
[[package]]
name = "pygal"
version = "3.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "importlib-metadata" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b0/b6/04176faeb312c84d7f9bc1a810f96ee38d15597e226bb9bda59f3a5cb122/pygal-3.1.0.tar.gz", hash = "sha256:fbdee7351a7423e7907fb8a9c3b77305f6b5678cb2e6fd0db36a8825e42955ec", size = 81006, upload-time = "2025-12-09T10:29:19.587Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/4c/2862dd25352fe4b22ec7760d4fa12cb692587cd7ec3e378cbf644fc0d2a8/pygal-3.1.0-py3-none-any.whl", hash = "sha256:4e923490f3490c90c481f4535fa3adcda20ff374257ab9d8ae897f91b632c0bb", size = 130171, upload-time = "2025-12-09T10:29:16.721Z" },
]
[[package]] [[package]]
name = "pygls" name = "pygls"
version = "2.0.0" version = "2.0.0"
@@ -1269,6 +1443,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/02/e7/062480ede84ecb56ee0f8f2e5b5a3b2a5bceeb73bbdf909d3c13f5438749/scriv-1.8.0-py3-none-any.whl", hash = "sha256:f00f51325b2f4bc96b16fbb1239d4ab577cc2422301a5dd4f5f9378aae2549e0", size = 39085, upload-time = "2025-12-30T00:01:08.599Z" }, { url = "https://files.pythonhosted.org/packages/02/e7/062480ede84ecb56ee0f8f2e5b5a3b2a5bceeb73bbdf909d3c13f5438749/scriv-1.8.0-py3-none-any.whl", hash = "sha256:f00f51325b2f4bc96b16fbb1239d4ab577cc2422301a5dd4f5f9378aae2549e0", size = 39085, upload-time = "2025-12-30T00:01:08.599Z" },
] ]
[[package]]
name = "shellingham"
version = "1.5.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
]
[[package]] [[package]]
name = "shibuya" name = "shibuya"
version = "2025.9.25" version = "2025.9.25"
@@ -1435,6 +1618,33 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/be/72/2db2f49247d0a18b4f1bb9a5a39a0162869acf235f3a96418363947b3d46/starlette-0.48.0-py3-none-any.whl", hash = "sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659", size = 73736, upload-time = "2025-09-13T08:41:03.869Z" }, { url = "https://files.pythonhosted.org/packages/be/72/2db2f49247d0a18b4f1bb9a5a39a0162869acf235f3a96418363947b3d46/starlette-0.48.0-py3-none-any.whl", hash = "sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659", size = 73736, upload-time = "2025-09-13T08:41:03.869Z" },
] ]
[[package]]
name = "tinycss2"
version = "1.5.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "webencodings" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a3/ae/2ca4913e5c0f09781d75482874c3a95db9105462a92ddd303c7d285d3df2/tinycss2-1.5.1.tar.gz", hash = "sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957", size = 88195, upload-time = "2025-11-23T10:29:10.082Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl", hash = "sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661", size = 28404, upload-time = "2025-11-23T10:29:08.676Z" },
]
[[package]]
name = "typer"
version = "0.21.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "rich" },
{ name = "shellingham" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/36/bf/8825b5929afd84d0dabd606c67cd57b8388cb3ec385f7ef19c5cc2202069/typer-0.21.1.tar.gz", hash = "sha256:ea835607cd752343b6b2b7ce676893e5a0324082268b48f27aa058bdb7d2145d", size = 110371, upload-time = "2026-01-06T11:21:10.989Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-py3-none-any.whl", hash = "sha256:7985e89081c636b88d172c2ee0cfe33c253160994d47bdfdc302defd7d1f1d01", size = 47381, upload-time = "2026-01-06T11:21:09.824Z" },
]
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.13.2" version = "4.13.2"
@@ -1545,6 +1755,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/18/0e/a5f0257ab47492b7afb5fb60347d14ba19445e2773fc8352d4be6bd2f6f8/wcwidth-0.3.0-py3-none-any.whl", hash = "sha256:073a1acb250e4add96cfd5ef84e0036605cd6e0d0782c8c15c80e42202348458", size = 85520, upload-time = "2026-01-21T17:44:08.002Z" }, { url = "https://files.pythonhosted.org/packages/18/0e/a5f0257ab47492b7afb5fb60347d14ba19445e2773fc8352d4be6bd2f6f8/wcwidth-0.3.0-py3-none-any.whl", hash = "sha256:073a1acb250e4add96cfd5ef84e0036605cd6e0d0782c8c15c80e42202348458", size = 85520, upload-time = "2026-01-21T17:44:08.002Z" },
] ]
[[package]]
name = "webencodings"
version = "0.5.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" },
]
[[package]] [[package]]
name = "websockets" name = "websockets"
version = "15.0.1" version = "15.0.1"
@@ -1589,3 +1808,69 @@ sdist = { url = "https://files.pythonhosted.org/packages/b8/08/c0776aa654dc43cb3
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/9b/58/98c4aa00e3de8e45726029799d8facbdcd75347b2f48b285857577e8efd8/wemake_python_styleguide-1.4.0-py3-none-any.whl", hash = "sha256:c0727475a20a1b7d59f1d806040e84768bdb0935d1147023453aa44c14b65c95", size = 215985, upload-time = "2025-08-25T10:15:06.713Z" }, { url = "https://files.pythonhosted.org/packages/9b/58/98c4aa00e3de8e45726029799d8facbdcd75347b2f48b285857577e8efd8/wemake_python_styleguide-1.4.0-py3-none-any.whl", hash = "sha256:c0727475a20a1b7d59f1d806040e84768bdb0935d1147023453aa44c14b65c95", size = 215985, upload-time = "2025-08-25T10:15:06.713Z" },
] ]
[[package]]
name = "zipp"
version = "3.23.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" },
]
[[package]]
name = "zstandard"
version = "0.25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738, upload-time = "2025-09-14T22:16:56.237Z" },
{ url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436, upload-time = "2025-09-14T22:16:57.774Z" },
{ url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019, upload-time = "2025-09-14T22:16:59.302Z" },
{ url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012, upload-time = "2025-09-14T22:17:01.156Z" },
{ url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148, upload-time = "2025-09-14T22:17:03.091Z" },
{ url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652, upload-time = "2025-09-14T22:17:04.979Z" },
{ url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993, upload-time = "2025-09-14T22:17:06.781Z" },
{ url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806, upload-time = "2025-09-14T22:17:08.415Z" },
{ url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659, upload-time = "2025-09-14T22:17:10.164Z" },
{ url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933, upload-time = "2025-09-14T22:17:11.857Z" },
{ url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008, upload-time = "2025-09-14T22:17:13.627Z" },
{ url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517, upload-time = "2025-09-14T22:17:16.103Z" },
{ url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292, upload-time = "2025-09-14T22:17:17.827Z" },
{ url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237, upload-time = "2025-09-14T22:17:19.954Z" },
{ url = "https://files.pythonhosted.org/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", size = 436922, upload-time = "2025-09-14T22:17:24.398Z" },
{ url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276, upload-time = "2025-09-14T22:17:21.429Z" },
{ url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679, upload-time = "2025-09-14T22:17:23.147Z" },
{ url = "https://files.pythonhosted.org/packages/35/0b/8df9c4ad06af91d39e94fa96cc010a24ac4ef1378d3efab9223cc8593d40/zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", size = 795735, upload-time = "2025-09-14T22:17:26.042Z" },
{ url = "https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", size = 640440, upload-time = "2025-09-14T22:17:27.366Z" },
{ url = "https://files.pythonhosted.org/packages/d9/14/933d27204c2bd404229c69f445862454dcc101cd69ef8c6068f15aaec12c/zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", size = 5343070, upload-time = "2025-09-14T22:17:28.896Z" },
{ url = "https://files.pythonhosted.org/packages/6d/db/ddb11011826ed7db9d0e485d13df79b58586bfdec56e5c84a928a9a78c1c/zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", size = 5063001, upload-time = "2025-09-14T22:17:31.044Z" },
{ url = "https://files.pythonhosted.org/packages/db/00/87466ea3f99599d02a5238498b87bf84a6348290c19571051839ca943777/zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", size = 5394120, upload-time = "2025-09-14T22:17:32.711Z" },
{ url = "https://files.pythonhosted.org/packages/2b/95/fc5531d9c618a679a20ff6c29e2b3ef1d1f4ad66c5e161ae6ff847d102a9/zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", size = 5451230, upload-time = "2025-09-14T22:17:34.41Z" },
{ url = "https://files.pythonhosted.org/packages/63/4b/e3678b4e776db00f9f7b2fe58e547e8928ef32727d7a1ff01dea010f3f13/zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", size = 5547173, upload-time = "2025-09-14T22:17:36.084Z" },
{ url = "https://files.pythonhosted.org/packages/4e/d5/ba05ed95c6b8ec30bd468dfeab20589f2cf709b5c940483e31d991f2ca58/zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", size = 5046736, upload-time = "2025-09-14T22:17:37.891Z" },
{ url = "https://files.pythonhosted.org/packages/50/d5/870aa06b3a76c73eced65c044b92286a3c4e00554005ff51962deef28e28/zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", size = 5576368, upload-time = "2025-09-14T22:17:40.206Z" },
{ url = "https://files.pythonhosted.org/packages/5d/35/398dc2ffc89d304d59bc12f0fdd931b4ce455bddf7038a0a67733a25f550/zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", size = 4954022, upload-time = "2025-09-14T22:17:41.879Z" },
{ url = "https://files.pythonhosted.org/packages/9a/5c/36ba1e5507d56d2213202ec2b05e8541734af5f2ce378c5d1ceaf4d88dc4/zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", size = 5267889, upload-time = "2025-09-14T22:17:43.577Z" },
{ url = "https://files.pythonhosted.org/packages/70/e8/2ec6b6fb7358b2ec0113ae202647ca7c0e9d15b61c005ae5225ad0995df5/zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", size = 5433952, upload-time = "2025-09-14T22:17:45.271Z" },
{ url = "https://files.pythonhosted.org/packages/7b/01/b5f4d4dbc59ef193e870495c6f1275f5b2928e01ff5a81fecb22a06e22fb/zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", size = 5814054, upload-time = "2025-09-14T22:17:47.08Z" },
{ url = "https://files.pythonhosted.org/packages/b2/e5/fbd822d5c6f427cf158316d012c5a12f233473c2f9c5fe5ab1ae5d21f3d8/zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", size = 5360113, upload-time = "2025-09-14T22:17:48.893Z" },
{ url = "https://files.pythonhosted.org/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", size = 436936, upload-time = "2025-09-14T22:17:52.658Z" },
{ url = "https://files.pythonhosted.org/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", size = 506232, upload-time = "2025-09-14T22:17:50.402Z" },
{ url = "https://files.pythonhosted.org/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", size = 462671, upload-time = "2025-09-14T22:17:51.533Z" },
{ url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887, upload-time = "2025-09-14T22:17:54.198Z" },
{ url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658, upload-time = "2025-09-14T22:17:55.423Z" },
{ url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849, upload-time = "2025-09-14T22:17:57.372Z" },
{ url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095, upload-time = "2025-09-14T22:17:59.498Z" },
{ url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751, upload-time = "2025-09-14T22:18:01.618Z" },
{ url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818, upload-time = "2025-09-14T22:18:03.769Z" },
{ url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402, upload-time = "2025-09-14T22:18:05.954Z" },
{ url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108, upload-time = "2025-09-14T22:18:07.68Z" },
{ url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248, upload-time = "2025-09-14T22:18:09.753Z" },
{ url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330, upload-time = "2025-09-14T22:18:11.966Z" },
{ url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123, upload-time = "2025-09-14T22:18:13.907Z" },
{ url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591, upload-time = "2025-09-14T22:18:16.465Z" },
{ url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513, upload-time = "2025-09-14T22:18:20.61Z" },
{ url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" },
{ url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" },
]