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
+5 -1
View File
@@ -1,6 +1,9 @@
#### joe made this: http://goel.io/joe
metrics/reports/diagrams
*.dist
*build
*.exe
#### python ####
# Byte-compiled / optimized / DLL files
@@ -321,4 +324,5 @@ http-client.private.env.json
.idea/.cache/.Apifox_Helper
.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 arg_parser.parsed_argspace.get_by_name("dev"):
orchestrator.start_polling(App(initial_message="ArgentaDev"))
orchestrator.run_repl(App(initial_message="ArgentaDev"))
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" Port: {port.value}")
orchestrator.start_polling(app)
orchestrator.run_repl(app)
if __name__ == "__main__":
+1 -1
View File
@@ -24,4 +24,4 @@ orchestrator = Orchestrator(custom_providers=[ConnectionProvider()])
# 4. Start the application
if __name__ == "__main__":
orchestrator.start_polling(app)
orchestrator.run_repl(app)
@@ -60,7 +60,7 @@ orchestrator = Orchestrator()
def main():
app.include_router(router)
orchestrator.start_polling(app)
orchestrator.run_repl(app)
if __name__ == "__main__":
+1 -1
View File
@@ -9,7 +9,7 @@ orchestrator: Orchestrator = Orchestrator()
def main() -> None:
app.include_router(router)
orchestrator.start_polling(app)
orchestrator.run_repl(app)
if __name__ == "__main__":
+1 -1
View File
@@ -30,4 +30,4 @@ app.include_router(main_router)
# 5. Start application
if __name__ == "__main__":
orchestrator.start_polling(app)
orchestrator.run_repl(app)
@@ -15,4 +15,4 @@ app.include_router(router)
# 3. Start polling via orchestrator
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}"))
with patch("builtins.input", side_effect=["help", "q"]):
orchestrator.start_polling(app)
orchestrator.run_repl(app)
output = capsys.readouterr().out
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.HELP)
orchestrator.start_polling(app)
orchestrator.run_repl(app)
if __name__ == "__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-тестирование цикла
----------------------
Полный запуск цикла ``start_polling`` можно покрывать через подпроцесс с передачей строк в ``stdin``. Это тяжелее и обычно не требуется. Если всё же необходимо — пример ниже.
Полный запуск цикла ``run_repl`` можно покрывать через подпроцесс с передачей строк в ``stdin``. Это тяжелее и обычно не требуется. Если всё же необходимо — пример ниже.
.. danger::
**Важно:** Обязательно передавайте строковый триггер команды выхода последним элементом в списке ``side_effects`` при патче ``input``.
+29 -8
View File
@@ -1,36 +1,57 @@
set windows-shell := ["powershell.exe", "-NoLogo", "-Command"]
set shell := ["bash", "-c"]
# Вывести список всех рецептов
# List all available recipes
default:
@just --list
# Запустить тесты через pytest
# ── Testing ───────────────────────────────────────────────────────────────────
# Run tests via pytest
tests:
python -m pytest tests
# Запустить тесты с отчетом о покрытии
# Run tests with coverage report
tests-cov:
python -m pytest --cov=argenta tests
# Запустить тесты с отчетом о покрытии с html репортом
# Run tests with coverage HTML report
tests-cov-html:
python -m pytest --cov=argenta tests --cov-report=html
# Отформатировать код (Ruff + isort)
# ── Code quality ──────────────────────────────────────────────────────────────
# Format code (Ruff + isort)
format:
python -m ruff format ./src
python -m isort ./src
# Проверить типы через mypy (strict)
# Check types via mypy (strict)
mypy:
python -m mypy -p argenta --strict
# Проверить стиль через wemake-python-styleguide
# Check style via wemake-python-styleguide
wps:
python -m flake8 --format=wemake ./src
# Запустить линтер Ruff
# Run Ruff linter
ruff:
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.app import DynamicDividingLine
from argenta import App, Command, Orchestrator
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()
def main() -> None:
app.include_router(router)
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__":
+2
View File
@@ -13,8 +13,10 @@ class BenchmarksNotFound(Exception):
def __str__(self) -> str:
return f"Benchmarks with type '{self.type_}' not found"
class BenchmarksWithSameNameAlreadyExists(Exception):
def __init__(self, benchmark_name: str):
self.benchmark_name = benchmark_name
def __str__(self) -> str:
return f"Benchmarks with name '{self.benchmark_name}' already exists"
+25 -30
View File
@@ -1,16 +1,11 @@
__all__ = [
"Benchmark",
"Benchmarks",
"BenchmarkResult",
"BenchmarkGroupResult"
]
__all__ = ["Benchmark", "Benchmarks", "BenchmarkResult", "BenchmarkGroupResult"]
import gc
import io
import statistics
import time
from contextlib import redirect_stdout
from dataclasses import dataclass
import time
import gc
import statistics
from typing import Callable, override
from .exceptions import BenchmarkNotFound, BenchmarksNotFound, BenchmarksWithSameNameAlreadyExists
@@ -40,14 +35,7 @@ class BenchmarkGroupResult:
class Benchmark:
def __init__(
self,
func: FuncForBenchmark,
*,
type_: str,
name: str,
description: str
) -> None:
def __init__(self, func: FuncForBenchmark, *, type_: str, name: str, description: str) -> None:
self.func = func
self.type_ = type_
self.name = name
@@ -78,11 +66,11 @@ class Benchmark:
@override
def __repr__(self) -> str:
return f'Benchmark<{self.type_=}, {self.name=}, {self.description=}>'
return f"Benchmark<{self.type_=}, {self.name=}, {self.description=}>"
@override
def __str__(self) -> str:
return f'benchmark {self.name} with type {self.type_}'
return f"benchmark {self.name} with type {self.type_}"
class Benchmarks:
@@ -92,16 +80,14 @@ class Benchmarks:
self._benchmarks_paired_by_name: dict[str, Benchmark] = {}
def register(
self,
type_: str,
description: str = ""
self, type_: str, description: str = ""
) -> Callable[[FuncForBenchmark], FuncForBenchmark]:
def decorator(func: FuncForBenchmark) -> FuncForBenchmark:
benchmark = Benchmark(
func,
type_=type_,
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__):
raise BenchmarksWithSameNameAlreadyExists(func.__name__)
@@ -110,9 +96,12 @@ class Benchmarks:
self._benchmarks.append(benchmark)
self._benchmarks_grouped_by_type.setdefault(type_, []).append(benchmark)
return func
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)
if not benchmark:
raise BenchmarkNotFound(name)
@@ -130,28 +119,34 @@ class Benchmarks:
is_gc_disabled=is_gc_disables,
avg_time=avg,
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_)
if not benchmarks:
raise BenchmarksNotFound(type_)
benchmark_results: list[BenchmarkResult] = []
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(
type_=type_,
iterations=iterations,
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] = []
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))
return results
+21 -16
View File
@@ -3,11 +3,11 @@ __all__ = [
"benchmark_command_with_flags",
"benchmark_many_commands",
"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.models import Command, InputCommand
from argenta.response import Response
from argenta.router import Router
@@ -18,11 +18,11 @@ from .entity import benchmarks
def benchmark_simple_command() -> None:
router = Router()
@router.command(Command('test'))
@router.command(Command("test"))
def handler(_res: Response) -> None:
pass
input_cmd = InputCommand.parse('test')
input_cmd = InputCommand.parse("test")
router.finds_appropriate_handler(input_cmd)
@@ -30,11 +30,11 @@ def benchmark_simple_command() -> None:
def benchmark_command_with_flags() -> None:
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:
pass
input_cmd = InputCommand.parse('test -a -b -c')
input_cmd = InputCommand.parse("test -a -b -c")
router.finds_appropriate_handler(input_cmd)
@@ -43,38 +43,43 @@ def benchmark_many_commands() -> None:
router = Router()
for i in range(50):
@router.command(Command(f'cmd{i}'))
@router.command(Command(f"cmd{i}"))
def handler(_res: Response) -> None:
pass
input_cmd = InputCommand.parse('cmd25')
input_cmd = InputCommand.parse("cmd25")
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:
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:
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)
@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:
router = Router()
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:
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)
+13 -29
View File
@@ -7,7 +7,7 @@ __all__ = [
"benchmark_validate_regex_complex",
"benchmark_validate_multiple_flags_10",
"benchmark_validate_multiple_flags_50",
"benchmark_validate_extreme_100_flags"
"benchmark_validate_extreme_100_flags",
]
import re
@@ -58,45 +58,29 @@ def benchmark_validate_regex_complex() -> None:
@benchmarks.register(type_="flag_validation", description="Multiple flags validation (10 flags)")
def benchmark_validate_multiple_flags_10() -> None:
flags = [
Flag(f"flag{i}", possible_values=PossibleValues.ALL)
for i in range(10)
]
input_flags = [
InputFlag(f"flag{i}", input_value=f"value{i}")
for i in range(10)
]
flags = [Flag(f"flag{i}", possible_values=PossibleValues.ALL) 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):
flag.validate_input_flag_value(input_flag.input_value)
@benchmarks.register(type_="flag_validation", description="Multiple flags validation (50 flags)")
def benchmark_validate_multiple_flags_50() -> None:
flags = [
Flag(f"flag{i}", possible_values=PossibleValues.ALL)
for i in range(50)
]
input_flags = [
InputFlag(f"flag{i}", input_value=f"value{i}")
for i in range(50)
]
flags = [Flag(f"flag{i}", possible_values=PossibleValues.ALL) 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):
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:
pattern = re.compile(r"^[a-zA-Z0-9_-]+$")
flags = [
Flag(f"flag{i}", possible_values=pattern)
for i in range(100)
]
input_flags = [
InputFlag(f"flag{i}", input_value=f"valid_value_{i}")
for i in range(100)
]
flags = [Flag(f"flag{i}", possible_values=pattern) 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):
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_long_values",
"benchmark_command_with_quoted_values",
"benchmark_extreme_many_flags"
"benchmark_extreme_many_flags",
]
from argenta.command.models import InputCommand
@@ -23,12 +23,16 @@ def benchmark_command_with_few_flags() -> None:
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:
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:
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)
@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:
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_aliases",
"benchmark_partial_match",
"benchmark_extreme_commands"
"benchmark_extreme_commands",
]
from argenta import App
@@ -19,9 +19,11 @@ def setup_app_with_commands(command_count: int, aliases_per_command: int = 0) ->
router = Router()
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:
pass
@@ -29,31 +31,41 @@ def setup_app_with_commands(command_count: int, aliases_per_command: int = 0) ->
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:
app = setup_app_with_commands(10)
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:
app = setup_app_with_commands(50)
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:
app = setup_app_with_commands(20, aliases_per_command=10)
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:
app = setup_app_with_commands(50)
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:
app = setup_app_with_commands(100, aliases_per_command=20)
app._most_similar_command("comm")
+31 -31
View File
@@ -3,7 +3,7 @@ __all__ = [
"benchmark_with_many_aliases",
"benchmark_few_aliases",
"benchmark_extreme_aliases",
"benchmark_very_many_aliases"
"benchmark_very_many_aliases",
]
from argenta import App
@@ -19,16 +19,16 @@ def benchmark_no_aliases() -> None:
app = App(override_system_messages=True)
router = Router()
@router.command(Command('command1'))
def handler1(_res: Response) -> None:
@router.command(Command("command1"))
def handler1(_res: Response) -> None:
pass
@router.command(Command('command2'))
def handler2(_res: Response) -> None:
@router.command(Command("command2"))
def handler2(_res: Response) -> None:
pass
@router.command(Command('command3'))
def handler3(_res: Response) -> None:
@router.command(Command("command3"))
def handler3(_res: Response) -> None:
pass
app.include_router(router)
@@ -40,16 +40,16 @@ def benchmark_few_aliases() -> None:
app = App(override_system_messages=True)
router = Router()
@router.command(Command('command1', aliases={'c1', 'cmd1'}))
def handler1(_res: Response) -> None:
@router.command(Command("command1", aliases={"c1", "cmd1"}))
def handler1(_res: Response) -> None:
pass
@router.command(Command('command2', aliases={'c2', 'cmd2'}))
def handler2(_res: Response) -> None:
@router.command(Command("command2", aliases={"c2", "cmd2"}))
def handler2(_res: Response) -> None:
pass
@router.command(Command('command3', aliases={'c3', 'cmd3'}))
def handler3(_res: Response) -> None:
@router.command(Command("command3", aliases={"c3", "cmd3"}))
def handler3(_res: Response) -> None:
pass
app.include_router(router)
@@ -61,16 +61,16 @@ def benchmark_with_many_aliases() -> None:
app = App(override_system_messages=True)
router = Router()
@router.command(Command('command1', aliases={'c1', 'cmd1', 'com1', 'first', 'one'}))
def handler1(_res: Response) -> None:
@router.command(Command("command1", aliases={"c1", "cmd1", "com1", "first", "one"}))
def handler1(_res: Response) -> None:
pass
@router.command(Command('command2', aliases={'c2', 'cmd2', 'com2', 'second', 'two'}))
def handler2(_res: Response) -> None:
@router.command(Command("command2", aliases={"c2", "cmd2", "com2", "second", "two"}))
def handler2(_res: Response) -> None:
pass
@router.command(Command('command3', aliases={'c3', 'cmd3', 'com3', 'third', 'three'}))
def handler3(_res: Response) -> None:
@router.command(Command("command3", aliases={"c3", "cmd3", "com3", "third", "three"}))
def handler3(_res: Response) -> None:
pass
app.include_router(router)
@@ -82,16 +82,16 @@ def benchmark_very_many_aliases() -> None:
app = App(override_system_messages=True)
router = Router()
@router.command(Command('command1', aliases={f'alias1_{i}' for i in range(20)}))
def handler1(_res: Response) -> None:
@router.command(Command("command1", aliases={f"alias1_{i}" for i in range(20)}))
def handler1(_res: Response) -> None:
pass
@router.command(Command('command2', aliases={f'alias2_{i}' for i in range(20)}))
def handler2(_res: Response) -> None:
@router.command(Command("command2", aliases={f"alias2_{i}" for i in range(20)}))
def handler2(_res: Response) -> None:
pass
@router.command(Command('command3', aliases={f'alias3_{i}' for i in range(20)}))
def handler3(_res: Response) -> None:
@router.command(Command("command3", aliases={f"alias3_{i}" for i in range(20)}))
def handler3(_res: Response) -> None:
pass
app.include_router(router)
@@ -103,16 +103,16 @@ def benchmark_extreme_aliases() -> None:
app = App(override_system_messages=True)
router = Router()
@router.command(Command('command1', aliases={f'alias1_{i}' for i in range(100)}))
def handler1(_res: Response) -> None:
@router.command(Command("command1", aliases={f"alias1_{i}" for i in range(100)}))
def handler1(_res: Response) -> None:
pass
@router.command(Command('command2', aliases={f'alias2_{i}' for i in range(100)}))
def handler2(_res: Response) -> None:
@router.command(Command("command2", aliases={f"alias2_{i}" for i in range(100)}))
def handler2(_res: Response) -> None:
pass
@router.command(Command('command3', aliases={f'alias3_{i}' for i in range(100)}))
def handler3(_res: Response) -> None:
@router.command(Command("command3", aliases={f"alias3_{i}" for i in range(100)}))
def handler3(_res: Response) -> None:
pass
app.include_router(router)
@@ -3,7 +3,7 @@ __all__ = [
"benchmark_many_routers",
"benchmark_many_commands_per_router",
"benchmark_many_aliases_per_command",
"benchmark_extreme_routers"
"benchmark_extreme_routers",
]
from argenta import App
@@ -14,14 +14,17 @@ from argenta.router import Router
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:
app = App(override_system_messages=True)
for i in range(3):
router = Router()
@router.command(Command(f'cmd{i}'))
@router.command(Command(f"cmd{i}"))
def handler(_res: Response) -> None:
pass
@@ -31,14 +34,17 @@ def benchmark_few_routers() -> None:
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:
app = App(override_system_messages=True)
for i in range(10):
router = Router()
@router.command(Command(f'cmd{i}'))
@router.command(Command(f"cmd{i}"))
def handler(_res: Response) -> None:
pass
@@ -48,7 +54,10 @@ def benchmark_many_routers() -> None:
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:
app = App(override_system_messages=True)
@@ -56,7 +65,8 @@ def benchmark_many_commands_per_router() -> None:
router = Router()
for j in range(10):
@router.command(Command(f'cmd{i}_{j}'))
@router.command(Command(f"cmd{i}_{j}"))
def handler(_res: Response) -> None:
pass
@@ -66,7 +76,10 @@ def benchmark_many_commands_per_router() -> None:
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:
app = App(override_system_messages=True)
@@ -74,7 +87,10 @@ def benchmark_many_aliases_per_command() -> None:
router = Router()
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:
pass
@@ -84,7 +100,10 @@ def benchmark_many_aliases_per_command() -> None:
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:
app = App(override_system_messages=True)
@@ -92,7 +111,10 @@ def benchmark_extreme_routers() -> None:
router = Router()
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:
pass
+80 -57
View File
@@ -5,17 +5,18 @@ from pathlib import Path
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.models import Command
from argenta.response import Response
from argenta.router import Router
from .benchmarks.core.models import BenchmarkGroupResult
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.release_generator import ReleaseGenerator
from .services.report_table_generator import ReportTableGenerator
from .services.system_info_reader import get_system_info
console = Console()
router = Router(title="Metrics commands:", disable_redirect_stdout=True)
@@ -27,22 +28,30 @@ POSITIVE_INTEGER_PATTERN = re.compile(r"^[1-9]\d*$")
Command(
"run-all",
description="Print all benchmarks results",
flags=Flags([
Flag('without-gc', possible_values=PossibleValues.NEITHER),
Flag('without-system-info', possible_values=PossibleValues.NEITHER)
])
flags=Flags(
[
Flag("without-gc", possible_values=PossibleValues.NEITHER),
Flag("without-system-info", possible_values=PossibleValues.NEITHER),
]
),
)
)
def all_print_handler(response: Response) -> None:
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:
console.print(report_generator.generate_system_info_header())
console.print(report_generator.generate_system_info_table())
is_gc_disabled = response.input_flags.get_flag_by_name("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))
is_gc_disabled = response.input_flags.get_flag_by_name(
"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:
console.print(report_generator.generate_benchmark_table_header(benchmark_group_result))
@@ -52,11 +61,11 @@ def all_print_handler(response: Response) -> None:
@router.command(Command("list-types", description="List all benchmark types"))
def list_types_handler(_: Response) -> None:
types = registered_benchmarks.get_types()
if not types:
console.print("[yellow]No benchmark types found[/yellow]")
return
console.print("[bold cyan]Available benchmark types:[/bold cyan]\n")
for type_ in types:
benchmarks_count = len(registered_benchmarks.get_benchmarks_by_type(type_))
@@ -67,23 +76,25 @@ def list_types_handler(_: Response) -> None:
Command(
"run-type",
description="Run benchmarks by specific type",
flags=Flags([
Flag('type', possible_values=registered_benchmarks.get_types()),
Flag('without-gc', possible_values=PossibleValues.NEITHER),
Flag('without-system-info', possible_values=PossibleValues.NEITHER)
])
flags=Flags(
[
Flag("type", possible_values=registered_benchmarks.get_types()),
Flag("without-gc", possible_values=PossibleValues.NEITHER),
Flag("without-system-info", possible_values=PossibleValues.NEITHER),
]
),
)
)
def run_type_handler(response: Response) -> None:
type_flag = response.input_flags.get_flag_by_name("type")
if not type_flag:
console.print("[red]Error: --type flag is required[/red]")
console.print("[yellow]Usage: run-type --type <type_name>[/yellow]")
return
benchmark_type = type_flag.input_value
if not type_flag.status == ValidationStatus.VALID:
console.print(f"[red]Error: No benchmarks found for type '{benchmark_type}'[/red]")
console.print("\n[yellow]Available types:[/yellow]")
@@ -91,17 +102,23 @@ def run_type_handler(response: Response) -> None:
for t in types:
console.print(f"{t}")
return
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:
console.print(report_generator.generate_system_info_header())
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)
benchmark_group_result = registered_benchmarks.run_benchmarks_by_type(benchmark_type, is_gc_disabled=bool(is_gc_disabled))
is_gc_disabled = response.input_flags.get_flag_by_name(
"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_report_table(benchmark_group_result))
@@ -109,26 +126,25 @@ def run_type_handler(response: Response) -> None:
@router.command(Command("release-generate", description="Generate release report"))
def release_generate_handler(_: Response) -> None:
lib_version = version("argenta")
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")
type_grouped_benchmarks: list[BenchmarkGroupResult] = registered_benchmarks.run_benchmarks_grouped_by_type(
iterations=1000,
is_gc_disabled=True
type_grouped_benchmarks: list[BenchmarkGroupResult] = (
registered_benchmarks.run_benchmarks_grouped_by_type(iterations=1000, is_gc_disabled=True)
)
release_generator = ReleaseGenerator(lib_version)
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:
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_}.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]")
@@ -136,26 +152,33 @@ def release_generate_handler(_: Response) -> None:
Command(
"diagrams-generate",
description="Generate diagrams for all benchmarks",
flags=Flags([
Flag('without-gc', possible_values=PossibleValues.NEITHER),
Flag('iterations', possible_values=POSITIVE_INTEGER_PATTERN)
])
flags=Flags(
[
Flag("without-gc", possible_values=PossibleValues.NEITHER),
Flag("iterations", possible_values=POSITIVE_INTEGER_PATTERN),
]
),
)
)
def diagrams_generate_handler(response: Response) -> None:
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:
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(f"[dim]Iterations: {iterations}, GC Disabled: {is_gc_disabled}[/dim]\n")
type_grouped_benchmarks: list[BenchmarkGroupResult] = registered_benchmarks.run_benchmarks_grouped_by_type(
iterations=iterations,
is_gc_disabled=is_gc_disabled
type_grouped_benchmarks: list[BenchmarkGroupResult] = (
registered_benchmarks.run_benchmarks_grouped_by_type(
iterations=iterations, is_gc_disabled=is_gc_disabled
)
)
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
@@ -163,17 +186,17 @@ def diagrams_generate_handler(response: Response) -> None:
output_dir.mkdir(parents=True, exist_ok=True)
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
for benchmark_group in type_grouped_benchmarks:
console.print(f"[cyan]Generating diagram for:[/cyan] [bold]{benchmark_group.type_}[/bold]")
comparison_path = diagram_generator.generate_comparison_diagram(benchmark_group)
generated_count += 1
console.print(f" [green]✓[/green] {comparison_path.name}\n")
console.print(f"[bold green]✓ Successfully generated {generated_count} diagrams[/bold green]")
console.print(f"[cyan]Output directory:[/cyan] [bold]{output_dir}[/bold]")
+1 -1
View File
@@ -1,6 +1,6 @@
from .diagram_generator import DiagramGenerator
from .release_generator import ReleaseGenerator
from .report_table_generator import ReportTableGenerator
from .system_info_reader import get_system_info
from .release_generator import ReleaseGenerator
__all__ = ["DiagramGenerator", "ReportTableGenerator", "get_system_info", "ReleaseGenerator"]
+58 -75
View File
@@ -2,8 +2,9 @@ __all__ = ["DiagramGenerator"]
from pathlib import Path
import matplotlib
import matplotlib.pyplot as plt
import cairosvg
import pygal
from pygal.style import Style
from ..benchmarks.core.models import BenchmarkGroupResult
@@ -12,8 +13,26 @@ class DiagramGenerator:
def __init__(self, output_dir: Path | str) -> None:
self.output_dir: Path = Path(output_dir) if isinstance(output_dir, str) else output_dir
matplotlib.use('Agg')
plt.style.use('seaborn-v0_8-whitegrid')
self._style = Style(
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:
results = benchmark_group.benchmark_results
@@ -27,84 +46,48 @@ class DiagramGenerator:
max_value = max(
max(avg_times) if avg_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
items_count = len(descriptions)
x_positions: list[int] = list(range(items_count))
bar_width = 0.25
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()
title_text = f"{benchmark_group.type_.replace('_', ' ').title()}"
metadata_text = (
f"Iterations: {benchmark_group.iterations} | GC: "
f"{'Disabled' if benchmark_group.is_gc_disabled else 'Enabled'}"
)
filename = f"{benchmark_group.type_}_comparison.png"
output_path = self.output_dir / filename
self.output_dir.mkdir(parents=True, exist_ok=True)
plt.savefig(output_path, dpi=200, bbox_inches='tight', facecolor='white')
plt.close(fig)
dynamic_height = 600 + (len(descriptions) * 150)
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
+10 -10
View File
@@ -12,20 +12,20 @@ class ReleaseGenerator:
def __init__(self, lib_version: str) -> None:
self.lib_version = lib_version
self.output_dir = Path("metrics/reports/releases") / lib_version
def generate_release(self, benchmark_groups: list[BenchmarkGroupResult]) -> Path:
if self.output_dir.exists():
shutil.rmtree(self.output_dir)
self.output_dir.mkdir(parents=True, exist_ok=True)
for benchmark_group in benchmark_groups:
type_dir = self.output_dir / benchmark_group.type_
type_dir.mkdir(exist_ok=True)
diagram_generator = DiagramGenerator(type_dir)
diagram_generator.generate_comparison_diagram(benchmark_group)
json_data = {
"type": benchmark_group.type_,
"iterations": benchmark_group.iterations,
@@ -36,14 +36,14 @@ class ReleaseGenerator:
"description": br.description,
"avg_time": br.avg_time,
"median_time": br.median_time,
"std_dev": br.std_dev
"std_dev": br.std_dev,
}
for br in benchmark_group.benchmark_results
]
],
}
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)
return self.output_dir
+22 -14
View File
@@ -3,7 +3,7 @@ from rich.table import Table
from rich.text import Text
from ..benchmarks.core.models import BenchmarkGroupResult
from metrics.services.system_info_reader import SystemInfo
from .system_info_reader import SystemInfo
class ReportTableGenerator:
@@ -12,11 +12,15 @@ class ReportTableGenerator:
self._cached_benchmark_tables: dict[int, Table] = {}
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)):
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("Avg Time", justify="right", style="bold yellow")
table.add_column("Median Time", justify="right", style="bold yellow")
@@ -34,18 +38,22 @@ class ReportTableGenerator:
@staticmethod
def generate_benchmark_table_header(benchmark_group_result: BenchmarkGroupResult) -> Panel:
header_text = Text(f"TYPE: {benchmark_group_result.type_.upper()} ; "
f"ITERATIONS: {benchmark_group_result.iterations} ; "
f"GC {"DISABLED" if benchmark_group_result.is_gc_disabled else "ENABLED"} ; "
f"ALL TIME IN MS",
style="bold magenta")
header_text = Text(
f"TYPE: {benchmark_group_result.type_.upper()} ; "
f"ITERATIONS: {benchmark_group_result.iterations} ; "
f"GC {'DISABLED' if benchmark_group_result.is_gc_disabled else 'ENABLED'} ; "
f"ALL TIME IN MS",
style="bold magenta",
)
return Panel(header_text, expand=False, border_style="magenta")
def generate_system_info_table(self) -> Table:
if self._cached_system_info_table is not None:
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("Value", style="yellow")
@@ -55,10 +63,10 @@ class ReportTableGenerator:
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 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("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("Available RAM", str(self.system_info.memory_info.available_ram) + ' GB')
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("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("Python Version", self.system_info.python_info.version)
table.add_row("Python Implementation", self.system_info.python_info.implementation)
table.add_row("Python Compiler", self.system_info.python_info.compiler)
@@ -69,4 +77,4 @@ class ReportTableGenerator:
@staticmethod
def generate_system_info_header() -> Panel:
header_text = Text("SYSTEM INFO", style="bold magenta")
return Panel(header_text, expand=False, border_style="magenta")
return Panel(header_text, expand=False, border_style="magenta")
+36 -41
View File
@@ -1,28 +1,19 @@
__all__ = [
"SystemInfo",
"get_system_info"
]
__all__ = ["SystemInfo", "get_system_info"]
from dataclasses import dataclass
import platform
import sys
from dataclasses import dataclass
import cpuinfo
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)
class OSInfo:
name: str
kernel_version: str
@dataclass(frozen=True, slots=True)
class CPUInfo:
name: str
@@ -31,11 +22,13 @@ class CPUInfo:
logical_cores: int
max_frequency: float
@dataclass(frozen=True, slots=True)
class MemoryInfo:
total_ram: float # in GB
used_ram: float # in GB
available_ram: float # in GB
total_ram: float # in GB
used_ram: float # in GB
available_ram: float # in GB
@dataclass(frozen=True, slots=True)
class PythonInfo:
@@ -44,18 +37,6 @@ class PythonInfo:
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:
system = platform.system()
@@ -73,22 +54,17 @@ def get_os_info() -> OSInfo:
kernel_version=kernel_version,
)
elif system == "Darwin":
return OSInfo(
kernel_version=platform.release(),
name=f"macOS {platform.mac_ver()[0]}"
)
return OSInfo(kernel_version=platform.release(), name=f"macOS {platform.mac_ver()[0]}")
else:
return OSInfo(
kernel_version=platform.release(),
name=platform.system()
)
return OSInfo(kernel_version=platform.release(), name=platform.system())
def get_cpu_info() -> CPUInfo:
cpu_info = cpuinfo.get_cpu_info()
cpu_name = cpu_info["brand_raw"]
cpu_architecture = cpu_info["arch"]
cpu_physical_cores = psutil.cpu_count(logical=False)
cpu_logical_cores = psutil.cpu_count(logical=True)
cpu_physical_cores = psutil.cpu_count(logical=False) or 0
cpu_logical_cores = psutil.cpu_count(logical=True) or 0
cpu_freq = psutil.cpu_freq()
cpu_max_frequency = cpu_freq.max
@@ -98,9 +74,10 @@ def get_cpu_info() -> CPUInfo:
architecture=cpu_architecture,
physical_cores=cpu_physical_cores,
logical_cores=cpu_logical_cores,
max_frequency=cpu_max_frequency
max_frequency=cpu_max_frequency,
)
def get_memory_info() -> MemoryInfo:
mem = psutil.virtual_memory()
total_ram = round(mem.total / (1024**3), 2)
@@ -113,14 +90,32 @@ def get_memory_info() -> MemoryInfo:
available_ram=available_ram,
)
def get_python_info() -> PythonInfo:
python_version = platform.python_version()
python_implementation = platform.python_implementation()
python_compiler = platform.python_compiler()
return PythonInfo(
version=python_version,
implementation=python_implementation,
compiler=python_compiler
version=python_version, 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:
app.include_router(router)
orchestrator.start_polling(app)
orchestrator.run_repl(app)
if __name__ == '__main__':
main()
+3 -2
View File
@@ -3,12 +3,13 @@ from prompt_toolkit import HTML
from argenta import App, Orchestrator
from argenta.app import PredefinedMessages, StaticDividingLine, AutoCompleter
from argenta.app.dividing_line.models import DynamicDividingLine
from argenta.orchestrator import ArgParser
from mock.mock_app.routers import work_router
app: App = App(
dividing_line=StaticDividingLine('~')
)
orchestrator: Orchestrator = Orchestrator()
orchestrator: Orchestrator = Orchestrator(arg_parser=ArgParser(processed_args=[]))
def main():
@@ -18,7 +19,7 @@ def main():
app.add_message_on_startup(PredefinedMessages.AUTOCOMPLETE)
app.add_message_on_startup(PredefinedMessages.HELP)
orchestrator.start_polling(app)
orchestrator.run_repl(app)
if __name__ == "__main__":
main()
+13
View File
@@ -13,12 +13,19 @@ dependencies = [
"prompt-toolkit>=3.0.52",
]
[project.optional-dependencies]
cli = [
"nuitka[onefile]>=4.0.5",
"typer>=0.9,!=0.12,<=0.21.1",
]
[dependency-groups]
dev = [
{include-group = "linters"},
{include-group = "typecheckers"},
{include-group = "docs"},
{include-group = "tests"},
{include-group = "metrics"},
"scriv>=1.8.0",
]
linters = [
@@ -46,13 +53,19 @@ metrics = [
"matplotlib>=3.10.8",
"psutil>=7.2.1",
"py-cpuinfo>=9.0.0",
"cairosvg>=2.8.2",
"pygal>=3.1.0",
]
[project.scripts]
argenta = "argenta._cli.__main__:main"
[tool.ruff]
line-length=100
[tool.pyright]
typeCheckingMode = "strict"
reportMissingTypeStubs = false
[[tool.pyright.executionEnvironments]]
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,
)
+22 -114
View File
@@ -1,86 +1,21 @@
from __future__ import annotations
__all__ = ["AutoCompleter"]
import sys
from typing import Callable, Iterable
from typing import TYPE_CHECKING
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
if TYPE_CHECKING:
from prompt_toolkit import PromptSession, HTML
class AutoCompleter:
def __init__(
self,
history_filename: str | None = None,
autocomplete_button: str = "tab",
command_highlighting: bool = True,
auto_suggestions: bool = True,
self,
history_filename: str | None = None,
autocomplete_button: str = "tab",
command_highlighting: bool = True,
auto_suggestions: bool = True,
) -> None:
self.history_filename: str | None = history_filename
self.autocomplete_button: str = autocomplete_button
@@ -94,42 +29,15 @@ class AutoCompleter:
self._session = None
self._fallback_mode = True
return
from ._ext_features_impl import build_session
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(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,
self._session = build_session(
self.history_filename,
self.autocomplete_button,
self.command_highlighting,
self.auto_suggestions,
all_commands
)
def prompt(self, prompt_text: str | HTML = ">>> ") -> str:
@@ -137,7 +45,7 @@ class AutoCompleter:
return input(prompt_text if isinstance(prompt_text, str) else ">>> ")
if self._session is None:
raise RuntimeError("Call initial_setup() before using prompt()")
return self._session.prompt(
HTML(prompt_text) if isinstance(prompt_text, str) else prompt_text,
cursor=CursorShape.BLINKING_BEAM
)
from ._ext_features_impl import do_prompt
return do_prompt(self._session, prompt_text)
+4 -5
View File
@@ -3,8 +3,6 @@ __all__ = ["App"]
import difflib
from typing import Never, TypeAlias
from rich.console import Console
from argenta.app.autocompleter import AutoCompleter
from argenta.app.behavior_handlers.models import (BehaviorHandlersFabric,
BehaviorHandlersSettersMixin)
@@ -144,7 +142,7 @@ class BaseApp(BehaviorHandlersSettersMixin):
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._pre_cycle_setup()
while True:
@@ -189,7 +187,7 @@ class App(BaseApp):
repeat_command_groups_printing: bool = False,
override_system_messages: bool = False,
autocompleter: AutoCompleter | None = None,
printer: Printer = Console().print,
printer: Printer | None = None,
) -> None:
"""
Public. The essence of the application itself.
@@ -206,6 +204,7 @@ class App(BaseApp):
:param printer: system messages text output function
:return: None
"""
from rich.console import Console
super().__init__(
prompt=prompt,
initial_message=initial_message,
@@ -216,7 +215,7 @@ class App(BaseApp):
repeat_command_groups_printing=repeat_command_groups_printing,
override_system_messages=override_system_messages,
autocompleter=autocompleter or AutoCompleter(),
printer=printer,
printer=printer or Console().print,
)
def include_router(self, router: Router) -> None:
+2 -5
View File
@@ -7,9 +7,8 @@ __all__ = [
"HandlerFunc",
]
from typing import Any, Protocol, TypeVar
from typing import Any, Protocol, TypeVar, Callable
from argenta.response import Response
T = TypeVar("T", contravariant=True)
@@ -39,6 +38,4 @@ class DescriptionMessageGenerator(Protocol):
raise NotImplementedError
class HandlerFunc(Protocol):
def __call__(self, response: Response, /, *args: Any, **kwargs: Any) -> None:
raise NotImplementedError
type HandlerFunc = Callable[..., Any]
+8 -6
View File
@@ -1,5 +1,7 @@
__all__ = ["Orchestrator"]
import os
from dishka import Provider, make_container
from argenta.app import App
@@ -7,13 +9,12 @@ from argenta.di.integration import setup_dishka
from argenta.di.providers import SystemProvider
from argenta.orchestrator.argparser import ArgParser
DEFAULT_ARGPARSER: ArgParser = ArgParser(processed_args=[])
class Orchestrator:
def __init__(
self,
arg_parser: ArgParser = DEFAULT_ARGPARSER,
arg_parser: ArgParser | None = None,
custom_providers: list[Provider] | None = None,
auto_inject_handlers: bool = True,
):
@@ -22,13 +23,14 @@ class Orchestrator:
:param arg_parser: Cmd argument parser and configurator at startup
: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._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
:param app: a running application
@@ -39,4 +41,4 @@ class Orchestrator:
)
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"]
from dishka import Container
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from dishka import Container
from argenta.command import InputFlags
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 typing import Callable
from rich.console import Console
from argenta.app.protocols import HandlerFunc
from argenta.command import Command, InputCommand, InputFlags
from argenta.command.flag import ValidationStatus
@@ -20,7 +18,7 @@ from argenta.router.exceptions import (RepeatedAliasNameException,
class Router:
def __init__(
self,
title: str = "Default title",
title: str = "Title",
*,
disable_redirect_stdout: bool = False,
):
@@ -175,6 +173,7 @@ class Router:
response_arg_annotation = func_annotations.get(response_arg)
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]
Console().print(
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.include_router(router)
app.set_empty_command_handler(lambda: print('Empty input command'))
orchestrator.start_polling(app)
orchestrator.run_repl(app)
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.include_router(router)
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
@@ -85,7 +85,7 @@ def test_mixed_valid_and_unknown_commands_handled_correctly(monkeypatch: pytest.
app = App(override_system_messages=True, printer=print)
app.include_router(router)
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
@@ -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.include_router(router)
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
@@ -137,7 +137,7 @@ def test_unregistered_flag_without_value_is_accessible(monkeypatch: pytest.Monke
app = App(override_system_messages=True, printer=print)
app.include_router(router)
orchestrator.start_polling(app)
orchestrator.run_repl(app)
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.include_router(router)
orchestrator.start_polling(app)
orchestrator.run_repl(app)
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.include_router(router)
orchestrator.start_polling(app)
orchestrator.run_repl(app)
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.include_router(router)
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
@@ -236,7 +236,7 @@ def test_repeated_flags_trigger_repeated_flags_handler(monkeypatch: pytest.Monke
app = App(override_system_messages=True, printer=print)
app.include_router(router)
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
@@ -38,7 +38,7 @@ def test_simple_command_executes_successfully(monkeypatch: pytest.MonkeyPatch, c
app = App(override_system_messages=True, printer=print)
app.include_router(router)
orchestrator.start_polling(app)
orchestrator.run_repl(app)
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.include_router(router)
orchestrator.start_polling(app)
orchestrator.run_repl(app)
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.include_router(router)
orchestrator.start_polling(app)
orchestrator.run_repl(app)
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.include_router(router)
orchestrator.start_polling(app)
orchestrator.run_repl(app)
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.include_router(router)
orchestrator.start_polling(app)
orchestrator.run_repl(app)
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.include_router(router)
orchestrator.start_polling(app)
orchestrator.run_repl(app)
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.include_router(router)
orchestrator.start_polling(app)
orchestrator.run_repl(app)
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.include_router(router)
orchestrator.start_polling(app)
orchestrator.run_repl(app)
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.include_router(router)
orchestrator.start_polling(app)
orchestrator.run_repl(app)
output = capsys.readouterr().out
+119 -159
View File
@@ -10,14 +10,11 @@ from prompt_toolkit.completion import CompleteEvent
from prompt_toolkit.document import Document
from prompt_toolkit.history import InMemoryHistory
from argenta.app.autocompleter.entity import (
AutoCompleter,
CommandLexer,
HistoryCompleter
)
from argenta.app.autocompleter._ext_features_impl import CommandLexer, HistoryCompleter
from argenta.app.autocompleter.entity import AutoCompleter
COMMANDS: set[str] = {"start", "stop", "status"}
_IMPL = "argenta.app.autocompleter._ext_features_impl"
def test_autocompleter_initializes_with_default_params() -> None:
@@ -33,7 +30,7 @@ def test_autocompleter_initializes_with_custom_params() -> None:
history_filename="test.txt",
autocomplete_button="c-space",
command_highlighting=False,
auto_suggestions=False
auto_suggestions=False,
)
assert completer.history_filename == "test.txt"
assert completer.autocomplete_button == "c-space"
@@ -79,13 +76,13 @@ def test_history_completer_returns_matching_commands() -> None:
history = InMemoryHistory()
history.append_string("start server")
history.append_string("stop server")
completer = HistoryCompleter(history, {"status"})
doc = Document("sta")
completions = list(completer.get_completions(doc, CompleteEvent()))
completion_texts = [c.text for c in completions]
assert "start server" in completion_texts
assert "status" in completion_texts
assert "stop server" not in completion_texts
@@ -95,13 +92,13 @@ def test_history_completer_returns_all_when_empty_input() -> None:
history = InMemoryHistory()
history.append_string("start")
history.append_string("stop")
completer = HistoryCompleter(history, {"status"})
doc = Document("")
completions = list(completer.get_completions(doc, CompleteEvent()))
completion_texts = [c.text for c in completions]
assert len(completion_texts) == 3
assert "start" in completion_texts
assert "stop" in completion_texts
@@ -111,10 +108,10 @@ def test_history_completer_returns_all_when_empty_input() -> None:
def test_history_completer_returns_empty_when_no_matches() -> None:
history = InMemoryHistory()
history.append_string("start")
completer = HistoryCompleter(history, {"stop"})
doc = Document("xyz")
completions = list(completer.get_completions(doc, CompleteEvent()))
assert len(completions) == 0
@@ -123,10 +120,10 @@ def test_history_completer_deduplicates_commands() -> None:
history = InMemoryHistory()
history.append_string("start")
history.append_string("start")
completer = HistoryCompleter(history, {"start"})
doc = Document("sta")
completions = list(completer.get_completions(doc, CompleteEvent()))
assert len(completions) == 1
@@ -136,13 +133,13 @@ def test_history_completer_sorts_results() -> None:
history.append_string("stop")
history.append_string("start")
history.append_string("status")
completer = HistoryCompleter(history, set())
doc = Document("st")
completions = list(completer.get_completions(doc, CompleteEvent()))
completion_texts = [c.text for c in completions]
assert completion_texts == ["start", "status", "stop"]
@@ -182,7 +179,7 @@ def test_history_completer_returns_early_when_no_matches() -> None:
history = InMemoryHistory()
completer = HistoryCompleter(history, {"start", "stop"})
doc = Document("xyz")
result = completer.get_completions(doc, CompleteEvent())
completions = list(result)
assert completions == []
@@ -190,28 +187,32 @@ def test_history_completer_returns_early_when_no_matches() -> None:
def test_autocompleter_initial_setup_with_commands() -> None:
completer = AutoCompleter()
with patch.object(sys.stdin, 'isatty', return_value=True), \
patch('argenta.app.autocompleter.entity.PromptSession') as mock_session:
with (
patch.object(sys.stdin, "isatty", return_value=True),
patch(f"{_IMPL}.PromptSession") as mock_session,
):
completer.initial_setup({"start", "stop", "status"})
assert completer._session is not None
assert completer._fallback_mode is False
mock_session.assert_called_once()
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
try:
completer = AutoCompleter(history_filename=history_file)
with patch.object(sys.stdin, 'isatty', return_value=True), \
patch('argenta.app.autocompleter.entity.PromptSession'), \
patch('argenta.app.autocompleter.entity.ThreadedHistory') as mock_threaded_history:
with (
patch.object(sys.stdin, "isatty", return_value=True),
patch(f"{_IMPL}.PromptSession"),
patch(f"{_IMPL}.ThreadedHistory") as mock_threaded_history,
):
completer.initial_setup({"start", "stop"})
assert completer._session is not None
assert completer._fallback_mode is False
mock_threaded_history.assert_called_once()
@@ -222,12 +223,14 @@ def test_autocompleter_initial_setup_with_history_file() -> None:
def test_autocompleter_initial_setup_without_history_file() -> None:
completer = AutoCompleter(history_filename=None)
with patch.object(sys.stdin, 'isatty', return_value=True), \
patch('argenta.app.autocompleter.entity.PromptSession'), \
patch('argenta.app.autocompleter.entity.InMemoryHistory') as mock_in_memory:
with (
patch.object(sys.stdin, "isatty", return_value=True),
patch(f"{_IMPL}.PromptSession"),
patch(f"{_IMPL}.InMemoryHistory") as mock_in_memory,
):
completer.initial_setup({"start", "stop"})
assert completer._session is not None
assert completer._fallback_mode is False
mock_in_memory.assert_called_once()
@@ -235,96 +238,90 @@ def test_autocompleter_initial_setup_without_history_file() -> None:
def test_autocompleter_initial_setup_with_custom_autocomplete_button() -> None:
completer = AutoCompleter(autocomplete_button="c-space")
with patch.object(sys.stdin, 'isatty', return_value=True), \
patch('argenta.app.autocompleter.entity.PromptSession'):
with (
patch.object(sys.stdin, "isatty", return_value=True),
patch(f"{_IMPL}.PromptSession"),
):
completer.initial_setup({"start", "stop"})
assert completer._session is not None
assert completer.autocomplete_button == "c-space"
def test_autocompleter_initial_setup_without_auto_suggestions() -> None:
completer = AutoCompleter(auto_suggestions=False)
with patch.object(sys.stdin, 'isatty', return_value=True), \
patch('argenta.app.autocompleter.entity.PromptSession') as mock_session:
with (
patch.object(sys.stdin, "isatty", return_value=True),
patch(f"{_IMPL}.PromptSession") as mock_session,
):
completer.initial_setup({"start", "stop"})
assert completer._session is not None
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:
completer = AutoCompleter(command_highlighting=False)
with patch.object(sys.stdin, 'isatty', return_value=True), \
patch('argenta.app.autocompleter.entity.PromptSession') as mock_session:
with (
patch.object(sys.stdin, "isatty", return_value=True),
patch(f"{_IMPL}.PromptSession") as mock_session,
):
completer.initial_setup({"start", "stop"})
assert completer._session is not None
call_kwargs = mock_session.call_args[1]
assert call_kwargs['style'] is None
assert call_kwargs['lexer'] is None
assert call_kwargs["style"] is None
assert call_kwargs["lexer"] is None
def test_autocompleter_key_binding_handler_with_complete_state() -> None:
completer = AutoCompleter()
def _setup_captured_handler(completer: AutoCompleter) -> Callable[[Any], None] | None:
"""Вспомогательная функция: поднимает initial_setup и захватывает kb-хендлер."""
captured_handler: Callable[[Any], None] | None = 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:
with (
patch.object(sys.stdin, "isatty", return_value=True),
patch(f"{_IMPL}.PromptSession"),
patch(f"{_IMPL}.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"})
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
mock_event = MagicMock()
mock_buff = MagicMock()
mock_buff.complete_state = True
mock_event.app.current_buffer = mock_buff
captured_handler(mock_event)
mock_buff.complete_next.assert_called_once()
def test_autocompleter_key_binding_handler_no_completions() -> None:
completer = AutoCompleter()
captured_handler: Callable[[Any], None] | None = 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"})
captured_handler = _setup_captured_handler(completer)
assert captured_handler is not None
mock_event = MagicMock()
mock_buff = MagicMock()
mock_buff.complete_state = False
@@ -332,36 +329,18 @@ def test_autocompleter_key_binding_handler_no_completions() -> None:
mock_completer.get_completions.return_value = iter([])
mock_buff.completer = mock_completer
mock_event.app.current_buffer = mock_buff
assert captured_handler is not None
captured_handler(mock_event)
mock_buff.start_completion.assert_not_called()
mock_buff.apply_completion.assert_not_called()
def test_autocompleter_key_binding_handler_single_completion() -> None:
completer = AutoCompleter()
captured_handler: Callable[[Any], None] | None = 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"})
captured_handler = _setup_captured_handler(completer)
assert captured_handler is not None
mock_event = MagicMock()
mock_buff = MagicMock()
mock_buff.complete_state = False
@@ -370,36 +349,18 @@ def test_autocompleter_key_binding_handler_single_completion() -> None:
mock_completer.get_completions.return_value = iter([mock_completion])
mock_buff.completer = mock_completer
mock_event.app.current_buffer = mock_buff
assert captured_handler is not None
captured_handler(mock_event)
mock_buff.apply_completion.assert_called_once_with(mock_completion)
mock_buff.start_completion.assert_not_called()
def test_autocompleter_key_binding_handler_multiple_completions() -> None:
completer = AutoCompleter()
captured_handler: Callable[[Any], None] | None = 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"})
captured_handler = _setup_captured_handler(completer)
assert captured_handler is not None
mock_event = MagicMock()
mock_buff = MagicMock()
mock_buff.complete_state = False
@@ -409,54 +370,53 @@ def test_autocompleter_key_binding_handler_multiple_completions() -> None:
mock_completer.get_completions.return_value = iter([mock_completion1, mock_completion2])
mock_buff.completer = mock_completer
mock_event.app.current_buffer = mock_buff
assert captured_handler is not None
captured_handler(mock_event)
mock_buff.start_completion.assert_called_once_with(select_first=False)
mock_buff.apply_completion.assert_not_called()
def test_autocompleter_prompt_in_fallback_mode_with_string() -> None:
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"})
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(">>> ")
assert result == 'test input'
assert result == "test input"
def test_autocompleter_prompt_in_fallback_mode_with_html() -> None:
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"})
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>"))
assert result == 'test input'
assert result == "test input"
def test_autocompleter_prompt_with_html_in_normal_mode() -> None:
completer = AutoCompleter()
mock_session = MagicMock()
mock_session.prompt.return_value = 'test result'
mock_session.prompt.return_value = "test result"
completer._session = mock_session
completer._fallback_mode = False
html_prompt = HTML("<b>>>> </b>")
result = completer.prompt(html_prompt)
assert result == 'test result'
assert result == "test result"
mock_session.prompt.assert_called_once()
call_args = mock_session.prompt.call_args
assert call_args[0][0] == html_prompt
@@ -464,15 +424,15 @@ def test_autocompleter_prompt_with_html_in_normal_mode() -> None:
def test_autocompleter_prompt_with_string_in_normal_mode() -> None:
completer = AutoCompleter()
mock_session = MagicMock()
mock_session.prompt.return_value = 'test result'
mock_session.prompt.return_value = "test result"
completer._session = mock_session
completer._fallback_mode = False
result = completer.prompt(">>> ")
assert result == 'test result'
assert result == "test result"
mock_session.prompt.assert_called_once()
call_args = mock_session.prompt.call_args
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:
"""Test Orchestrator initialization with default ArgParser"""
def test_orchestrator_initializes_with_no_argparser(mocker: MockerFixture) -> None:
"""Test Orchestrator initialization with no ArgParser"""
mocker.patch('sys.argv', ['test_program'])
orchestrator = Orchestrator()
assert orchestrator._arg_parser is not None
assert isinstance(orchestrator._arg_parser, ArgParser)
assert orchestrator._arg_parser is 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
) -> 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_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.start_polling(sample_app)
orchestrator.run_repl(sample_app)
mock_make_container.assert_called_once()
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
) -> 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]
mocker.patch('argenta.orchestrator.entity.make_container', return_value=mock_container)
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.start_polling(sample_app)
orchestrator.run_repl(sample_app)
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
) -> 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]
mocker.patch('argenta.orchestrator.entity.make_container', return_value=mock_container)
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.start_polling(sample_app)
orchestrator.run_repl(sample_app)
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
) -> 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.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.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
) -> None:
"""Test that start_polling includes custom providers in container"""
"""Test that run_repl includes custom providers in container"""
custom_provider = Provider()
mock_make_container = mocker.patch('argenta.orchestrator.entity.make_container')
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.start_polling(sample_app)
orchestrator.run_repl(sample_app)
# Check that custom_provider was passed to make_container
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"""
mocker.patch('argenta.orchestrator.entity.make_container')
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)
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
@@ -202,10 +201,10 @@ def test_orchestrator_passes_argparser_to_container_context(
"""Test that Orchestrator passes ArgParser instance to container context"""
mock_make_container = mocker.patch('argenta.orchestrator.entity.make_container')
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.start_polling(sample_app)
orchestrator.run_repl(sample_app)
# Verify that ArgParser was passed in context
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
) -> None:
"""Test that Orchestrator propagates exceptions from app.run_polling()"""
mocker.patch('argenta.orchestrator.entity.make_container')
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)
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()
mock_make_container = mocker.patch('argenta.orchestrator.entity.make_container')
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=[provider1, provider2]
)
orchestrator.start_polling(sample_app)
orchestrator.run_repl(sample_app)
call_args = mock_make_container.call_args[0]
assert provider1 in call_args
Generated
+349 -64
View File
@@ -1,6 +1,6 @@
version = 1
revision = 3
requires-python = ">=3.12"
requires-python = ">=3.12, <3.15"
[[package]]
name = "aiosqlite"
@@ -48,12 +48,23 @@ dependencies = [
{ name = "rich" },
]
[package.optional-dependencies]
cli = [
{ name = "nuitka", extra = ["onefile"] },
{ name = "typer" },
]
[package.dev-dependencies]
dev = [
{ name = "cairosvg" },
{ name = "esbonio" },
{ name = "isort" },
{ name = "matplotlib" },
{ name = "mypy" },
{ name = "psutil" },
{ name = "py-cpuinfo" },
{ name = "pyfakefs" },
{ name = "pygal" },
{ name = "pytest" },
{ name = "pytest-cov" },
{ name = "pytest-mock" },
@@ -78,9 +89,11 @@ linters = [
{ name = "wemake-python-styleguide" },
]
metrics = [
{ name = "cairosvg" },
{ name = "matplotlib" },
{ name = "psutil" },
{ name = "py-cpuinfo" },
{ name = "pygal" },
]
tests = [
{ name = "pyfakefs" },
@@ -96,16 +109,24 @@ typecheckers = [
requires-dist = [
{ name = "art", specifier = ">=6.4,<7.0" },
{ 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 = "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]
dev = [
{ name = "cairosvg", specifier = ">=2.8.2" },
{ name = "esbonio", specifier = ">=1.0.0" },
{ name = "isort", specifier = ">=7.0.0" },
{ name = "matplotlib", specifier = ">=3.10.8" },
{ 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 = "pygal", specifier = ">=3.1.0" },
{ name = "pytest", specifier = ">=8.3.2" },
{ name = "pytest-cov", specifier = ">=7.0.0" },
{ name = "pytest-mock", specifier = ">=3.15.1" },
@@ -130,9 +151,11 @@ linters = [
{ name = "wemake-python-styleguide", specifier = ">=0.17.0" },
]
metrics = [
{ name = "cairosvg", specifier = ">=2.8.2" },
{ name = "matplotlib", specifier = ">=3.10.8" },
{ name = "psutil", specifier = ">=7.2.1" },
{ name = "py-cpuinfo", specifier = ">=9.0.0" },
{ name = "pygal", specifier = ">=3.1.0" },
]
tests = [
{ 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" },
]
[[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]]
name = "cattrs"
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" },
]
[[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]]
name = "charset-normalizer"
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" },
]
[[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]]
name = "cycler"
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" },
]
[[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]]
name = "dishka"
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" },
]
[[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]]
name = "iniconfig"
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" },
]
[[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]]
name = "numpy"
version = "2.4.1"
@@ -930,71 +1083,71 @@ wheels = [
[[package]]
name = "pillow"
version = "12.2.0"
version = "12.1.1"
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 = [
{ 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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]]
@@ -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" },
]
[[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]]
name = "pyfakefs"
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" },
]
[[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]]
name = "pygls"
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" },
]
[[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]]
name = "shibuya"
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" },
]
[[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]]
name = "typing-extensions"
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" },
]
[[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]]
name = "websockets"
version = "15.0.1"
@@ -1589,3 +1808,69 @@ sdist = { url = "https://files.pythonhosted.org/packages/b8/08/c0776aa654dc43cb3
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" },
]
[[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" },
]