2 Commits

Author SHA1 Message Date
kolo 552e1db6fd Merge pull request #13 from koloideal/dependabot/uv/pillow-12.2.0
Bump pillow from 12.1.1 to 12.2.0
2026-05-21 11:41:43 +03:00
dependabot[bot] c06ea6e196 Bump pillow from 12.1.1 to 12.2.0
Bumps [pillow](https://github.com/python-pillow/Pillow) from 12.1.1 to 12.2.0.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/12.1.1...12.2.0)

---
updated-dependencies:
- dependency-name: pillow
  dependency-version: 12.2.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-08 11:18:39 +00:00
57 changed files with 786 additions and 1810 deletions
+1 -5
View File
@@ -1,9 +1,6 @@
#### joe made this: http://goel.io/joe #### joe made this: http://goel.io/joe
metrics/reports/diagrams metrics/reports/diagrams
*.dist
*build
*.exe
#### python #### #### python ####
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
@@ -324,5 +321,4 @@ http-client.private.env.json
.idea/.cache/.Apifox_Helper .idea/.cache/.Apifox_Helper
.idea/ApifoxUploaderProjectSetting.xml .idea/ApifoxUploaderProjectSetting.xml
.zed .zed
test.py
@@ -1,35 +0,0 @@
<!--
A new scriv changelog fragment.
Uncomment the section that is right (remove the HTML comment wrapper).
For top level release notes, leave all the headers commented out.
-->
### Added
- A cli module that implements the ability to launch applications on Argenta, run application benchmarks on Argenta, create a boilerplate for new projects, and much more.
- A new `info` command has been added to the Argenta CLI, providing a quick overview of the installed package and runtime environment.
### Changed
- Refactoring the initialization order of some modules; heavy imports are now imported only when necessary, which resulted in a boost to importtime.
<!--
### Deprecated
- A bullet item for the Deprecated category.
-->
<!--
### Removed
- A bullet item for the Removed category.
-->
<!--
### Fixed
- A bullet item for the Fixed category.
-->
+2 -2
View File
@@ -12,6 +12,6 @@ orchestrator = Orchestrator(
if __name__ == "__main__": if __name__ == "__main__":
if arg_parser.parsed_argspace.get_by_name("dev"): if arg_parser.parsed_argspace.get_by_name("dev"):
orchestrator.run_repl(App(initial_message="ArgentaDev")) orchestrator.start_polling(App(initial_message="ArgentaDev"))
else: else:
orchestrator.run_repl(App()) orchestrator.start_polling(App())
+1 -1
View File
@@ -22,7 +22,7 @@ def main():
print(f" Host: {host.value}") print(f" Host: {host.value}")
print(f" Port: {port.value}") print(f" Port: {port.value}")
orchestrator.run_repl(app) orchestrator.start_polling(app)
if __name__ == "__main__": if __name__ == "__main__":
+1 -1
View File
@@ -24,4 +24,4 @@ orchestrator = Orchestrator(custom_providers=[ConnectionProvider()])
# 4. Start the application # 4. Start the application
if __name__ == "__main__": if __name__ == "__main__":
orchestrator.run_repl(app) orchestrator.start_polling(app)
@@ -60,7 +60,7 @@ orchestrator = Orchestrator()
def main(): def main():
app.include_router(router) app.include_router(router)
orchestrator.run_repl(app) orchestrator.start_polling(app)
if __name__ == "__main__": if __name__ == "__main__":
+1 -1
View File
@@ -9,7 +9,7 @@ orchestrator: Orchestrator = Orchestrator()
def main() -> None: def main() -> None:
app.include_router(router) app.include_router(router)
orchestrator.run_repl(app) orchestrator.start_polling(app)
if __name__ == "__main__": if __name__ == "__main__":
+1 -1
View File
@@ -30,4 +30,4 @@ app.include_router(main_router)
# 5. Start application # 5. Start application
if __name__ == "__main__": if __name__ == "__main__":
orchestrator.run_repl(app) orchestrator.start_polling(app)
@@ -15,4 +15,4 @@ app.include_router(router)
# 3. Start polling via orchestrator # 3. Start polling via orchestrator
if __name__ == "__main__": if __name__ == "__main__":
orchestrator.run_repl(app) orchestrator.start_polling(app)
+1 -1
View File
@@ -25,7 +25,7 @@ def test_input_incorrect_command(capsys: CaptureFixture[str]):
app.set_unknown_command_handler(lambda command: print(f"Unknown command: {command.trigger}")) app.set_unknown_command_handler(lambda command: print(f"Unknown command: {command.trigger}"))
with patch("builtins.input", side_effect=["help", "q"]): with patch("builtins.input", side_effect=["help", "q"]):
orchestrator.run_repl(app) orchestrator.start_polling(app)
output = capsys.readouterr().out output = capsys.readouterr().out
assert "\nUnknown command: help\n" in output assert "\nUnknown command: help\n" in output
+1 -1
View File
@@ -159,7 +159,7 @@ PredefinedMessages
app.add_message_on_startup(PredefinedMessages.AUTOCOMPLETE) app.add_message_on_startup(PredefinedMessages.AUTOCOMPLETE)
app.add_message_on_startup(PredefinedMessages.HELP) app.add_message_on_startup(PredefinedMessages.HELP)
orchestrator.run_repl(app) orchestrator.start_polling(app)
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+1 -1
View File
@@ -36,7 +36,7 @@ Orchestrator
Основные методы Основные методы
---------------- ----------------
.. py:method:: run_repl(self, app: App) -> None .. py:method:: start_polling(self, app: App) -> None
Это главный метод, который запускает приложение. Он запускает бесконечный цикл ввода -> вывода. Это главный метод, который запускает приложение. Он запускает бесконечный цикл ввода -> вывода.
+1 -1
View File
@@ -45,7 +45,7 @@
E2E-тестирование цикла E2E-тестирование цикла
---------------------- ----------------------
Полный запуск цикла ``run_repl`` можно покрывать через подпроцесс с передачей строк в ``stdin``. Это тяжелее и обычно не требуется. Если всё же необходимо — пример ниже. Полный запуск цикла ``start_polling`` можно покрывать через подпроцесс с передачей строк в ``stdin``. Это тяжелее и обычно не требуется. Если всё же необходимо — пример ниже.
.. danger:: .. danger::
**Важно:** Обязательно передавайте строковый триггер команды выхода последним элементом в списке ``side_effects`` при патче ``input``. **Важно:** Обязательно передавайте строковый триггер команды выхода последним элементом в списке ``side_effects`` при патче ``input``.
+8 -29
View File
@@ -1,57 +1,36 @@
set windows-shell := ["powershell.exe", "-NoLogo", "-Command"] set windows-shell := ["powershell.exe", "-NoLogo", "-Command"]
set shell := ["bash", "-c"] set shell := ["bash", "-c"]
# List all available recipes # Вывести список всех рецептов
default: default:
@just --list @just --list
# ── Testing ─────────────────────────────────────────────────────────────────── # Запустить тесты через pytest
# Run tests via pytest
tests: tests:
python -m pytest tests python -m pytest tests
# Run tests with coverage report # Запустить тесты с отчетом о покрытии
tests-cov: tests-cov:
python -m pytest --cov=argenta tests python -m pytest --cov=argenta tests
# Run tests with coverage HTML report # Запустить тесты с отчетом о покрытии с html репортом
tests-cov-html: tests-cov-html:
python -m pytest --cov=argenta tests --cov-report=html python -m pytest --cov=argenta tests --cov-report=html
# ── Code quality ────────────────────────────────────────────────────────────── # Отформатировать код (Ruff + isort)
# Format code (Ruff + isort)
format: format:
python -m ruff format ./src python -m ruff format ./src
python -m isort ./src python -m isort ./src
# Check types via mypy (strict) # Проверить типы через mypy (strict)
mypy: mypy:
python -m mypy -p argenta --strict python -m mypy -p argenta --strict
# Check style via wemake-python-styleguide # Проверить стиль через wemake-python-styleguide
wps: wps:
python -m flake8 --format=wemake ./src python -m flake8 --format=wemake ./src
# Run Ruff linter # Запустить линтер Ruff
ruff: ruff:
python -m ruff check ./src python -m ruff check ./src
# Run all checks (format, mypy, ruff, wps)
check-format: format mypy ruff wps
# ── Changelog (scriv) ─────────────────────────────────────────────────────────
# Create a new changelog fragment and open it in $EDITOR
frag:
if (-not (Test-Path "./changelog.d")) { New-Item -ItemType Directory -Path "./changelog.d" }
scriv create --add
# Preview collected changelog without writing anything
changelog-preview:
scriv collect --dry-run
# Collect fragments into CHANGELOG.md for release (usage: just release 1.2.3)
release version:
scriv collect --version {{ version }} --add
+6 -6
View File
@@ -1,18 +1,18 @@
from argenta import App, Command, Orchestrator from argenta import App, Orchestrator, Command
from argenta.app import DynamicDividingLine
from .handlers import router from .handlers import router
app = App(initial_message="metrics", exit_command=Command("exit", aliases=["quit"]))
app.include_router(router) app = App(initial_message="metrics", exit_command=Command('exit', aliases=['quit']))
orchestrator = Orchestrator() orchestrator = Orchestrator()
def main() -> None: def main() -> None:
app.include_router(router)
app.set_description_message_pattern( app.set_description_message_pattern(
lambda command, description: f"[bold cyan]▸[/bold cyan] [bold white]{command}[/bold white] [dim]│[/dim] [yellow italic]{description}[/yellow italic]" lambda command, description: f'[bold cyan]▸[/bold cyan] [bold white]{command}[/bold white] [dim]│[/dim] [yellow italic]{description}[/yellow italic]'
) )
orchestrator.run_repl(app) orchestrator.start_polling(app)
if __name__ == "__main__": if __name__ == "__main__":
-2
View File
@@ -13,10 +13,8 @@ class BenchmarksNotFound(Exception):
def __str__(self) -> str: def __str__(self) -> str:
return f"Benchmarks with type '{self.type_}' not found" return f"Benchmarks with type '{self.type_}' not found"
class BenchmarksWithSameNameAlreadyExists(Exception): class BenchmarksWithSameNameAlreadyExists(Exception):
def __init__(self, benchmark_name: str): def __init__(self, benchmark_name: str):
self.benchmark_name = benchmark_name self.benchmark_name = benchmark_name
def __str__(self) -> str: def __str__(self) -> str:
return f"Benchmarks with name '{self.benchmark_name}' already exists" return f"Benchmarks with name '{self.benchmark_name}' already exists"
+30 -25
View File
@@ -1,11 +1,16 @@
__all__ = ["Benchmark", "Benchmarks", "BenchmarkResult", "BenchmarkGroupResult"] __all__ = [
"Benchmark",
"Benchmarks",
"BenchmarkResult",
"BenchmarkGroupResult"
]
import gc
import io import io
import statistics
import time
from contextlib import redirect_stdout from contextlib import redirect_stdout
from dataclasses import dataclass from dataclasses import dataclass
import time
import gc
import statistics
from typing import Callable, override from typing import Callable, override
from .exceptions import BenchmarkNotFound, BenchmarksNotFound, BenchmarksWithSameNameAlreadyExists from .exceptions import BenchmarkNotFound, BenchmarksNotFound, BenchmarksWithSameNameAlreadyExists
@@ -35,7 +40,14 @@ class BenchmarkGroupResult:
class Benchmark: 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.func = func
self.type_ = type_ self.type_ = type_
self.name = name self.name = name
@@ -66,11 +78,11 @@ class Benchmark:
@override @override
def __repr__(self) -> str: def __repr__(self) -> str:
return f"Benchmark<{self.type_=}, {self.name=}, {self.description=}>" return f'Benchmark<{self.type_=}, {self.name=}, {self.description=}>'
@override @override
def __str__(self) -> str: def __str__(self) -> str:
return f"benchmark {self.name} with type {self.type_}" return f'benchmark {self.name} with type {self.type_}'
class Benchmarks: class Benchmarks:
@@ -80,14 +92,16 @@ class Benchmarks:
self._benchmarks_paired_by_name: dict[str, Benchmark] = {} self._benchmarks_paired_by_name: dict[str, Benchmark] = {}
def register( def register(
self, type_: str, description: str = "" self,
type_: str,
description: str = ""
) -> Callable[[FuncForBenchmark], FuncForBenchmark]: ) -> Callable[[FuncForBenchmark], FuncForBenchmark]:
def decorator(func: FuncForBenchmark) -> FuncForBenchmark: def decorator(func: FuncForBenchmark) -> FuncForBenchmark:
benchmark = Benchmark( benchmark = Benchmark(
func, func,
type_=type_, type_=type_,
name=func.__name__, name=func.__name__,
description=description or f"description for {func.__name__} with type {type_}", description=description or f'description for {func.__name__} with type {type_}',
) )
if self._benchmarks_paired_by_name.get(func.__name__): if self._benchmarks_paired_by_name.get(func.__name__):
raise BenchmarksWithSameNameAlreadyExists(func.__name__) raise BenchmarksWithSameNameAlreadyExists(func.__name__)
@@ -96,12 +110,9 @@ class Benchmarks:
self._benchmarks.append(benchmark) self._benchmarks.append(benchmark)
self._benchmarks_grouped_by_type.setdefault(type_, []).append(benchmark) self._benchmarks_grouped_by_type.setdefault(type_, []).append(benchmark)
return func return func
return decorator return decorator
def run_benchmark_by_name( def run_benchmark_by_name(self, name: str, iterations: int = 100, is_gc_disables: bool = False) -> BenchmarkResult:
self, name: str, iterations: int = 100, is_gc_disables: bool = False
) -> BenchmarkResult:
benchmark = self.get_benchmark_by_name(name) benchmark = self.get_benchmark_by_name(name)
if not benchmark: if not benchmark:
raise BenchmarkNotFound(name) raise BenchmarkNotFound(name)
@@ -119,34 +130,28 @@ class Benchmarks:
is_gc_disabled=is_gc_disables, is_gc_disabled=is_gc_disables,
avg_time=avg, avg_time=avg,
median_time=median, median_time=median,
std_dev=std_dev, std_dev=std_dev
) )
def run_benchmarks_by_type( def run_benchmarks_by_type(self, type_: str, iterations: int = 100, is_gc_disabled: bool = False) -> BenchmarkGroupResult:
self, type_: str, iterations: int = 100, is_gc_disabled: bool = False
) -> BenchmarkGroupResult:
benchmarks = self.get_benchmarks_by_type(type_) benchmarks = self.get_benchmarks_by_type(type_)
if not benchmarks: if not benchmarks:
raise BenchmarksNotFound(type_) raise BenchmarksNotFound(type_)
benchmark_results: list[BenchmarkResult] = [] benchmark_results: list[BenchmarkResult] = []
for benchmark in benchmarks: for benchmark in benchmarks:
benchmark_results.append( benchmark_results.append(self.run_benchmark_by_name(benchmark.name, iterations, is_gc_disabled))
self.run_benchmark_by_name(benchmark.name, iterations, is_gc_disabled)
)
return BenchmarkGroupResult( return BenchmarkGroupResult(
type_=type_, type_=type_,
iterations=iterations, iterations=iterations,
is_gc_disabled=is_gc_disabled, is_gc_disabled=is_gc_disabled,
benchmark_results=benchmark_results, benchmark_results=benchmark_results
) )
def run_benchmarks_grouped_by_type( def run_benchmarks_grouped_by_type(self, iterations: int = 100, is_gc_disabled: bool = False) -> list[BenchmarkGroupResult]:
self, iterations: int = 100, is_gc_disabled: bool = False
) -> list[BenchmarkGroupResult]:
results: list[BenchmarkGroupResult] = [] results: list[BenchmarkGroupResult] = []
for type_, _ in self._benchmarks_grouped_by_type.items(): for type_, benchmarks in self._benchmarks_grouped_by_type.items():
results.append(self.run_benchmarks_by_type(type_, iterations, is_gc_disabled)) results.append(self.run_benchmarks_by_type(type_, iterations, is_gc_disabled))
return results return results
+16 -21
View File
@@ -3,11 +3,11 @@ __all__ = [
"benchmark_command_with_flags", "benchmark_command_with_flags",
"benchmark_many_commands", "benchmark_many_commands",
"benchmark_command_with_many_flags", "benchmark_command_with_many_flags",
"benchmark_extreme_router", "benchmark_extreme_router"
] ]
from argenta.command import Flag, Flags
from argenta.command.models import Command, InputCommand from argenta.command.models import Command, InputCommand
from argenta.command import Flag, Flags
from argenta.response import Response from argenta.response import Response
from argenta.router import Router from argenta.router import Router
@@ -18,11 +18,11 @@ from .entity import benchmarks
def benchmark_simple_command() -> None: def benchmark_simple_command() -> None:
router = Router() router = Router()
@router.command(Command("test")) @router.command(Command('test'))
def handler(_res: Response) -> None: def handler(_res: Response) -> None:
pass pass
input_cmd = InputCommand.parse("test") input_cmd = InputCommand.parse('test')
router.finds_appropriate_handler(input_cmd) router.finds_appropriate_handler(input_cmd)
@@ -30,11 +30,11 @@ def benchmark_simple_command() -> None:
def benchmark_command_with_flags() -> None: def benchmark_command_with_flags() -> None:
router = Router() router = Router()
@router.command(Command("test", flags=Flags([Flag("a"), Flag("b"), Flag("c")]))) @router.command(Command('test', flags=Flags([Flag('a'), Flag('b'), Flag('c')])))
def handler(_res: Response) -> None: def handler(_res: Response) -> None:
pass pass
input_cmd = InputCommand.parse("test -a -b -c") input_cmd = InputCommand.parse('test -a -b -c')
router.finds_appropriate_handler(input_cmd) router.finds_appropriate_handler(input_cmd)
@@ -43,43 +43,38 @@ def benchmark_many_commands() -> None:
router = Router() router = Router()
for i in range(50): for i in range(50):
@router.command(Command(f'cmd{i}'))
@router.command(Command(f"cmd{i}"))
def handler(_res: Response) -> None: def handler(_res: Response) -> None:
pass pass
input_cmd = InputCommand.parse("cmd25") input_cmd = InputCommand.parse('cmd25')
router.finds_appropriate_handler(input_cmd) router.finds_appropriate_handler(input_cmd)
@benchmarks.register( @benchmarks.register(type_="finds_appropriate_handler", description="Command with many flags (20 flags)")
type_="finds_appropriate_handler", description="Command with many flags (20 flags)"
)
def benchmark_command_with_many_flags() -> None: def benchmark_command_with_many_flags() -> None:
router = Router() router = Router()
flags = Flags([Flag(f"flag{i}") for i in range(20)]) flags = Flags([Flag(f'flag{i}') for i in range(20)])
@router.command(Command("test", flags=flags)) @router.command(Command('test', flags=flags))
def handler(_res: Response) -> None: def handler(_res: Response) -> None:
pass pass
input_cmd = InputCommand.parse("test " + " ".join(f"-flag{i}" for i in range(10))) input_cmd = InputCommand.parse('test ' + ' '.join(f'-flag{i}' for i in range(10)))
router.finds_appropriate_handler(input_cmd) router.finds_appropriate_handler(input_cmd)
@benchmarks.register( @benchmarks.register(type_="finds_appropriate_handler", description="Extreme (100 commands, 10 flags each)")
type_="finds_appropriate_handler", description="Extreme (100 commands, 10 flags each)"
)
def benchmark_extreme_router() -> None: def benchmark_extreme_router() -> None:
router = Router() router = Router()
for i in range(100): for i in range(100):
flags = Flags([Flag(f"f{i}_{j}") for j in range(10)]) flags = Flags([Flag(f'f{i}_{j}') for j in range(10)])
@router.command(Command(f"cmd{i}", flags=flags)) @router.command(Command(f'cmd{i}', flags=flags))
def handler(_res: Response) -> None: def handler(_res: Response) -> None:
pass pass
input_cmd = InputCommand.parse("cmd50 -f50_0 -f50_1 -f50_2") input_cmd = InputCommand.parse('cmd50 -f50_0 -f50_1 -f50_2')
router.finds_appropriate_handler(input_cmd) router.finds_appropriate_handler(input_cmd)
+29 -13
View File
@@ -7,7 +7,7 @@ __all__ = [
"benchmark_validate_regex_complex", "benchmark_validate_regex_complex",
"benchmark_validate_multiple_flags_10", "benchmark_validate_multiple_flags_10",
"benchmark_validate_multiple_flags_50", "benchmark_validate_multiple_flags_50",
"benchmark_validate_extreme_100_flags", "benchmark_validate_extreme_100_flags"
] ]
import re import re
@@ -58,29 +58,45 @@ def benchmark_validate_regex_complex() -> None:
@benchmarks.register(type_="flag_validation", description="Multiple flags validation (10 flags)") @benchmarks.register(type_="flag_validation", description="Multiple flags validation (10 flags)")
def benchmark_validate_multiple_flags_10() -> None: def benchmark_validate_multiple_flags_10() -> None:
flags = [Flag(f"flag{i}", possible_values=PossibleValues.ALL) for i in range(10)] flags = [
input_flags = [InputFlag(f"flag{i}", input_value=f"value{i}") for i in range(10)] 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): for flag, input_flag in zip(flags, input_flags):
flag.validate_input_flag_value(input_flag.input_value) flag.validate_input_flag_value(input_flag.input_value)
@benchmarks.register(type_="flag_validation", description="Multiple flags validation (50 flags)") @benchmarks.register(type_="flag_validation", description="Multiple flags validation (50 flags)")
def benchmark_validate_multiple_flags_50() -> None: def benchmark_validate_multiple_flags_50() -> None:
flags = [Flag(f"flag{i}", possible_values=PossibleValues.ALL) for i in range(50)] flags = [
input_flags = [InputFlag(f"flag{i}", input_value=f"value{i}") for i in range(50)] 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): for flag, input_flag in zip(flags, input_flags):
flag.validate_input_flag_value(input_flag.input_value) flag.validate_input_flag_value(input_flag.input_value)
@benchmarks.register( @benchmarks.register(type_="flag_validation", description="Extreme (100 flags with regex validation)")
type_="flag_validation", description="Extreme (100 flags with regex validation)"
)
def benchmark_validate_extreme_100_flags() -> None: def benchmark_validate_extreme_100_flags() -> None:
pattern = re.compile(r"^[a-zA-Z0-9_-]+$") pattern = re.compile(r"^[a-zA-Z0-9_-]+$")
flags = [Flag(f"flag{i}", possible_values=pattern) for i in range(100)] flags = [
input_flags = [InputFlag(f"flag{i}", input_value=f"valid_value_{i}") for i in range(100)] 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): for flag, input_flag in zip(flags, input_flags):
flag.validate_input_flag_value(input_flag.input_value) flag.validate_input_flag_value(input_flag.input_value)
+4 -10
View File
@@ -5,7 +5,7 @@ __all__ = [
"benchmark_command_with_mixed_prefixes", "benchmark_command_with_mixed_prefixes",
"benchmark_command_with_long_values", "benchmark_command_with_long_values",
"benchmark_command_with_quoted_values", "benchmark_command_with_quoted_values",
"benchmark_extreme_many_flags", "benchmark_extreme_many_flags"
] ]
from argenta.command.models import InputCommand from argenta.command.models import InputCommand
@@ -23,16 +23,12 @@ def benchmark_command_with_few_flags() -> None:
InputCommand.parse("start -a -b -c") InputCommand.parse("start -a -b -c")
@benchmarks.register( @benchmarks.register(type_="input_command_parse", description="Command with flags and values (5 flags)")
type_="input_command_parse", description="Command with flags and values (5 flags)"
)
def benchmark_command_with_flags_and_values() -> None: def benchmark_command_with_flags_and_values() -> None:
InputCommand.parse("start --host localhost --port 8080 --debug --verbose -c config.json") InputCommand.parse("start --host localhost --port 8080 --debug --verbose -c config.json")
@benchmarks.register( @benchmarks.register(type_="input_command_parse", description="Command with mixed prefixes (-, --, ---)")
type_="input_command_parse", description="Command with mixed prefixes (-, --, ---)"
)
def benchmark_command_with_mixed_prefixes() -> None: def benchmark_command_with_mixed_prefixes() -> None:
InputCommand.parse("cmd -a --bb ---ccc -d value --ee value2 ---fff value3") InputCommand.parse("cmd -a --bb ---ccc -d value --ee value2 ---fff value3")
@@ -44,9 +40,7 @@ def benchmark_command_with_long_values() -> None:
InputCommand.parse(cmd) InputCommand.parse(cmd)
@benchmarks.register( @benchmarks.register(type_="input_command_parse", description="Command with quoted values (5 flags)")
type_="input_command_parse", description="Command with quoted values (5 flags)"
)
def benchmark_command_with_quoted_values() -> None: def benchmark_command_with_quoted_values() -> None:
InputCommand.parse("cmd --text 'hello world' --path '/usr/local/bin' --msg \"test message\"") InputCommand.parse("cmd --text 'hello world' --path '/usr/local/bin' --msg \"test message\"")
+8 -20
View File
@@ -3,7 +3,7 @@ __all__ = [
"benchmark_many_commands_most_similar", "benchmark_many_commands_most_similar",
"benchmark_many_aliases", "benchmark_many_aliases",
"benchmark_partial_match", "benchmark_partial_match",
"benchmark_extreme_commands", "benchmark_extreme_commands"
] ]
from argenta import App from argenta import App
@@ -19,11 +19,9 @@ def setup_app_with_commands(command_count: int, aliases_per_command: int = 0) ->
router = Router() router = Router()
for i in range(command_count): for i in range(command_count):
aliases = ( aliases = {f'alias{i}_{j}' for j in range(aliases_per_command)} if aliases_per_command else set()
{f"alias{i}_{j}" for j in range(aliases_per_command)} if aliases_per_command else set()
)
@router.command(Command(f"command{i}", aliases=aliases)) @router.command(Command(f'command{i}', aliases=aliases))
def handler(_res: Response) -> None: def handler(_res: Response) -> None:
pass pass
@@ -31,41 +29,31 @@ def setup_app_with_commands(command_count: int, aliases_per_command: int = 0) ->
return app return app
@benchmarks.register( @benchmarks.register(type_="most_similar_command", description="Few commands (10 commands, no match)")
type_="most_similar_command", description="Few commands (10 commands, no match)"
)
def benchmark_few_commands() -> None: def benchmark_few_commands() -> None:
app = setup_app_with_commands(10) app = setup_app_with_commands(10)
app._most_similar_command("unknown") app._most_similar_command("unknown")
@benchmarks.register( @benchmarks.register(type_="most_similar_command", description="Many commands (50 commands, no match)")
type_="most_similar_command", description="Many commands (50 commands, no match)"
)
def benchmark_many_commands_most_similar() -> None: def benchmark_many_commands_most_similar() -> None:
app = setup_app_with_commands(50) app = setup_app_with_commands(50)
app._most_similar_command("unknown") app._most_similar_command("unknown")
@benchmarks.register( @benchmarks.register(type_="most_similar_command", description="Many aliases (20 commands, 10 aliases each)")
type_="most_similar_command", description="Many aliases (20 commands, 10 aliases each)"
)
def benchmark_many_aliases() -> None: def benchmark_many_aliases() -> None:
app = setup_app_with_commands(20, aliases_per_command=10) app = setup_app_with_commands(20, aliases_per_command=10)
app._most_similar_command("unknown") app._most_similar_command("unknown")
@benchmarks.register( @benchmarks.register(type_="most_similar_command", description="Partial match (50 commands, prefix match)")
type_="most_similar_command", description="Partial match (50 commands, prefix match)"
)
def benchmark_partial_match() -> None: def benchmark_partial_match() -> None:
app = setup_app_with_commands(50) app = setup_app_with_commands(50)
app._most_similar_command("comm") app._most_similar_command("comm")
@benchmarks.register( @benchmarks.register(type_="most_similar_command", description="Extreme (100 commands, 20 aliases each)")
type_="most_similar_command", description="Extreme (100 commands, 20 aliases each)"
)
def benchmark_extreme_commands() -> None: def benchmark_extreme_commands() -> None:
app = setup_app_with_commands(100, aliases_per_command=20) app = setup_app_with_commands(100, aliases_per_command=20)
app._most_similar_command("comm") app._most_similar_command("comm")
+31 -31
View File
@@ -3,7 +3,7 @@ __all__ = [
"benchmark_with_many_aliases", "benchmark_with_many_aliases",
"benchmark_few_aliases", "benchmark_few_aliases",
"benchmark_extreme_aliases", "benchmark_extreme_aliases",
"benchmark_very_many_aliases", "benchmark_very_many_aliases"
] ]
from argenta import App from argenta import App
@@ -19,16 +19,16 @@ def benchmark_no_aliases() -> None:
app = App(override_system_messages=True) app = App(override_system_messages=True)
router = Router() router = Router()
@router.command(Command("command1")) @router.command(Command('command1'))
def handler1(_res: Response) -> None: def handler1(_res: Response) -> None:
pass pass
@router.command(Command("command2")) @router.command(Command('command2'))
def handler2(_res: Response) -> None: def handler2(_res: Response) -> None:
pass pass
@router.command(Command("command3")) @router.command(Command('command3'))
def handler3(_res: Response) -> None: def handler3(_res: Response) -> None:
pass pass
app.include_router(router) app.include_router(router)
@@ -40,16 +40,16 @@ def benchmark_few_aliases() -> None:
app = App(override_system_messages=True) app = App(override_system_messages=True)
router = Router() router = Router()
@router.command(Command("command1", aliases={"c1", "cmd1"})) @router.command(Command('command1', aliases={'c1', 'cmd1'}))
def handler1(_res: Response) -> None: def handler1(_res: Response) -> None:
pass pass
@router.command(Command("command2", aliases={"c2", "cmd2"})) @router.command(Command('command2', aliases={'c2', 'cmd2'}))
def handler2(_res: Response) -> None: def handler2(_res: Response) -> None:
pass pass
@router.command(Command("command3", aliases={"c3", "cmd3"})) @router.command(Command('command3', aliases={'c3', 'cmd3'}))
def handler3(_res: Response) -> None: def handler3(_res: Response) -> None:
pass pass
app.include_router(router) app.include_router(router)
@@ -61,16 +61,16 @@ def benchmark_with_many_aliases() -> None:
app = App(override_system_messages=True) app = App(override_system_messages=True)
router = Router() router = Router()
@router.command(Command("command1", aliases={"c1", "cmd1", "com1", "first", "one"})) @router.command(Command('command1', aliases={'c1', 'cmd1', 'com1', 'first', 'one'}))
def handler1(_res: Response) -> None: def handler1(_res: Response) -> None:
pass pass
@router.command(Command("command2", aliases={"c2", "cmd2", "com2", "second", "two"})) @router.command(Command('command2', aliases={'c2', 'cmd2', 'com2', 'second', 'two'}))
def handler2(_res: Response) -> None: def handler2(_res: Response) -> None:
pass pass
@router.command(Command("command3", aliases={"c3", "cmd3", "com3", "third", "three"})) @router.command(Command('command3', aliases={'c3', 'cmd3', 'com3', 'third', 'three'}))
def handler3(_res: Response) -> None: def handler3(_res: Response) -> None:
pass pass
app.include_router(router) app.include_router(router)
@@ -82,16 +82,16 @@ def benchmark_very_many_aliases() -> None:
app = App(override_system_messages=True) app = App(override_system_messages=True)
router = Router() router = Router()
@router.command(Command("command1", aliases={f"alias1_{i}" for i in range(20)})) @router.command(Command('command1', aliases={f'alias1_{i}' for i in range(20)}))
def handler1(_res: Response) -> None: def handler1(_res: Response) -> None:
pass pass
@router.command(Command("command2", aliases={f"alias2_{i}" for i in range(20)})) @router.command(Command('command2', aliases={f'alias2_{i}' for i in range(20)}))
def handler2(_res: Response) -> None: def handler2(_res: Response) -> None:
pass pass
@router.command(Command("command3", aliases={f"alias3_{i}" for i in range(20)})) @router.command(Command('command3', aliases={f'alias3_{i}' for i in range(20)}))
def handler3(_res: Response) -> None: def handler3(_res: Response) -> None:
pass pass
app.include_router(router) app.include_router(router)
@@ -103,16 +103,16 @@ def benchmark_extreme_aliases() -> None:
app = App(override_system_messages=True) app = App(override_system_messages=True)
router = Router() router = Router()
@router.command(Command("command1", aliases={f"alias1_{i}" for i in range(100)})) @router.command(Command('command1', aliases={f'alias1_{i}' for i in range(100)}))
def handler1(_res: Response) -> None: def handler1(_res: Response) -> None:
pass pass
@router.command(Command("command2", aliases={f"alias2_{i}" for i in range(100)})) @router.command(Command('command2', aliases={f'alias2_{i}' for i in range(100)}))
def handler2(_res: Response) -> None: def handler2(_res: Response) -> None:
pass pass
@router.command(Command("command3", aliases={f"alias3_{i}" for i in range(100)})) @router.command(Command('command3', aliases={f'alias3_{i}' for i in range(100)}))
def handler3(_res: Response) -> None: def handler3(_res: Response) -> None:
pass pass
app.include_router(router) app.include_router(router)
@@ -3,7 +3,7 @@ __all__ = [
"benchmark_many_routers", "benchmark_many_routers",
"benchmark_many_commands_per_router", "benchmark_many_commands_per_router",
"benchmark_many_aliases_per_command", "benchmark_many_aliases_per_command",
"benchmark_extreme_routers", "benchmark_extreme_routers"
] ]
from argenta import App from argenta import App
@@ -14,17 +14,14 @@ from argenta.router import Router
from .entity import benchmarks from .entity import benchmarks
@benchmarks.register( @benchmarks.register(type_="validate_routers_for_collisions", description="With few routers (3 routers, 1 command each)")
type_="validate_routers_for_collisions",
description="With few routers (3 routers, 1 command each)",
)
def benchmark_few_routers() -> None: def benchmark_few_routers() -> None:
app = App(override_system_messages=True) app = App(override_system_messages=True)
for i in range(3): for i in range(3):
router = Router() router = Router()
@router.command(Command(f"cmd{i}")) @router.command(Command(f'cmd{i}'))
def handler(_res: Response) -> None: def handler(_res: Response) -> None:
pass pass
@@ -34,17 +31,14 @@ def benchmark_few_routers() -> None:
app._validate_routers_for_collisions() app._validate_routers_for_collisions()
@benchmarks.register( @benchmarks.register(type_="validate_routers_for_collisions", description="With many routers (10 routers, 1 command each)")
type_="validate_routers_for_collisions",
description="With many routers (10 routers, 1 command each)",
)
def benchmark_many_routers() -> None: def benchmark_many_routers() -> None:
app = App(override_system_messages=True) app = App(override_system_messages=True)
for i in range(10): for i in range(10):
router = Router() router = Router()
@router.command(Command(f"cmd{i}")) @router.command(Command(f'cmd{i}'))
def handler(_res: Response) -> None: def handler(_res: Response) -> None:
pass pass
@@ -54,10 +48,7 @@ def benchmark_many_routers() -> None:
app._validate_routers_for_collisions() app._validate_routers_for_collisions()
@benchmarks.register( @benchmarks.register(type_="validate_routers_for_collisions", description="With many commands per router (3 routers, 10 commands each)")
type_="validate_routers_for_collisions",
description="With many commands per router (3 routers, 10 commands each)",
)
def benchmark_many_commands_per_router() -> None: def benchmark_many_commands_per_router() -> None:
app = App(override_system_messages=True) app = App(override_system_messages=True)
@@ -65,8 +56,7 @@ def benchmark_many_commands_per_router() -> None:
router = Router() router = Router()
for j in range(10): for j in range(10):
@router.command(Command(f'cmd{i}_{j}'))
@router.command(Command(f"cmd{i}_{j}"))
def handler(_res: Response) -> None: def handler(_res: Response) -> None:
pass pass
@@ -76,10 +66,7 @@ def benchmark_many_commands_per_router() -> None:
app._validate_routers_for_collisions() app._validate_routers_for_collisions()
@benchmarks.register( @benchmarks.register(type_="validate_routers_for_collisions", description="With many aliases (3 routers, 5 commands, 10 aliases each)")
type_="validate_routers_for_collisions",
description="With many aliases (3 routers, 5 commands, 10 aliases each)",
)
def benchmark_many_aliases_per_command() -> None: def benchmark_many_aliases_per_command() -> None:
app = App(override_system_messages=True) app = App(override_system_messages=True)
@@ -87,10 +74,7 @@ def benchmark_many_aliases_per_command() -> None:
router = Router() router = Router()
for j in range(5): for j in range(5):
@router.command(Command(f'cmd{i}_{j}', aliases={f'alias{i}_{j}_{k}' for k in range(10)}))
@router.command(
Command(f"cmd{i}_{j}", aliases={f"alias{i}_{j}_{k}" for k in range(10)})
)
def handler(_res: Response) -> None: def handler(_res: Response) -> None:
pass pass
@@ -100,10 +84,7 @@ def benchmark_many_aliases_per_command() -> None:
app._validate_routers_for_collisions() app._validate_routers_for_collisions()
@benchmarks.register( @benchmarks.register(type_="validate_routers_for_collisions", description="Extreme (20 routers, 10 commands, 20 aliases each)")
type_="validate_routers_for_collisions",
description="Extreme (20 routers, 10 commands, 20 aliases each)",
)
def benchmark_extreme_routers() -> None: def benchmark_extreme_routers() -> None:
app = App(override_system_messages=True) app = App(override_system_messages=True)
@@ -111,10 +92,7 @@ def benchmark_extreme_routers() -> None:
router = Router() router = Router()
for j in range(10): for j in range(10):
@router.command(Command(f'cmd{i}_{j}', aliases={f'alias{i}_{j}_{k}' for k in range(20)}))
@router.command(
Command(f"cmd{i}_{j}", aliases={f"alias{i}_{j}_{k}" for k in range(20)})
)
def handler(_res: Response) -> None: def handler(_res: Response) -> None:
pass pass
+57 -80
View File
@@ -5,18 +5,17 @@ from pathlib import Path
from rich.console import Console from rich.console import Console
from argenta.command import Flag, Flags, PossibleValues from argenta.command import Flag, PossibleValues, Flags
from argenta.command.flag import ValidationStatus from argenta.command.flag import ValidationStatus
from argenta.command.models import Command from argenta.command.models import Command
from argenta.response import Response from argenta.response import Response
from argenta.router import Router from argenta.router import Router
from .benchmarks.core.models import BenchmarkGroupResult from .benchmarks.core.models import BenchmarkGroupResult
from .benchmarks.entity import benchmarks as registered_benchmarks from .benchmarks.entity import benchmarks as registered_benchmarks
from .services.diagram_generator import DiagramGenerator
from .services.release_generator import ReleaseGenerator
from .services.report_table_generator import ReportTableGenerator from .services.report_table_generator import ReportTableGenerator
from .services.system_info_reader import get_system_info from .services.system_info_reader import get_system_info
from .services.diagram_generator import DiagramGenerator
from .services.release_generator import ReleaseGenerator
console = Console() console = Console()
router = Router(title="Metrics commands:", disable_redirect_stdout=True) router = Router(title="Metrics commands:", disable_redirect_stdout=True)
@@ -28,30 +27,22 @@ POSITIVE_INTEGER_PATTERN = re.compile(r"^[1-9]\d*$")
Command( Command(
"run-all", "run-all",
description="Print all benchmarks results", description="Print all benchmarks results",
flags=Flags( flags=Flags([
[ Flag('without-gc', possible_values=PossibleValues.NEITHER),
Flag("without-gc", possible_values=PossibleValues.NEITHER), Flag('without-system-info', possible_values=PossibleValues.NEITHER)
Flag("without-system-info", possible_values=PossibleValues.NEITHER), ])
]
),
) )
) )
def all_print_handler(response: Response) -> None: def all_print_handler(response: Response) -> None:
report_generator = ReportTableGenerator(get_system_info()) report_generator = ReportTableGenerator(get_system_info())
without_system_info = response.input_flags.get_flag_by_name( without_system_info = response.input_flags.get_flag_by_name("without-system-info", with_status=ValidationStatus.VALID)
"without-system-info", with_status=ValidationStatus.VALID
)
if not without_system_info: if not without_system_info:
console.print(report_generator.generate_system_info_header()) console.print(report_generator.generate_system_info_header())
console.print(report_generator.generate_system_info_table()) console.print(report_generator.generate_system_info_table())
is_gc_disabled = response.input_flags.get_flag_by_name( is_gc_disabled = response.input_flags.get_flag_by_name("without-gc", with_status=ValidationStatus.VALID)
"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))
)
type_grouped_benchmarks: list[BenchmarkGroupResult] = (
registered_benchmarks.run_benchmarks_grouped_by_type(is_gc_disabled=bool(is_gc_disabled))
)
for benchmark_group_result in type_grouped_benchmarks: for benchmark_group_result in type_grouped_benchmarks:
console.print(report_generator.generate_benchmark_table_header(benchmark_group_result)) console.print(report_generator.generate_benchmark_table_header(benchmark_group_result))
@@ -61,11 +52,11 @@ def all_print_handler(response: Response) -> None:
@router.command(Command("list-types", description="List all benchmark types")) @router.command(Command("list-types", description="List all benchmark types"))
def list_types_handler(_: Response) -> None: def list_types_handler(_: Response) -> None:
types = registered_benchmarks.get_types() types = registered_benchmarks.get_types()
if not types: if not types:
console.print("[yellow]No benchmark types found[/yellow]") console.print("[yellow]No benchmark types found[/yellow]")
return return
console.print("[bold cyan]Available benchmark types:[/bold cyan]\n") console.print("[bold cyan]Available benchmark types:[/bold cyan]\n")
for type_ in types: for type_ in types:
benchmarks_count = len(registered_benchmarks.get_benchmarks_by_type(type_)) benchmarks_count = len(registered_benchmarks.get_benchmarks_by_type(type_))
@@ -76,25 +67,23 @@ def list_types_handler(_: Response) -> None:
Command( Command(
"run-type", "run-type",
description="Run benchmarks by specific type", description="Run benchmarks by specific type",
flags=Flags( flags=Flags([
[ Flag('type', possible_values=registered_benchmarks.get_types()),
Flag("type", possible_values=registered_benchmarks.get_types()), Flag('without-gc', possible_values=PossibleValues.NEITHER),
Flag("without-gc", possible_values=PossibleValues.NEITHER), Flag('without-system-info', possible_values=PossibleValues.NEITHER)
Flag("without-system-info", possible_values=PossibleValues.NEITHER), ])
]
),
) )
) )
def run_type_handler(response: Response) -> None: def run_type_handler(response: Response) -> None:
type_flag = response.input_flags.get_flag_by_name("type") type_flag = response.input_flags.get_flag_by_name("type")
if not type_flag: if not type_flag:
console.print("[red]Error: --type flag is required[/red]") console.print("[red]Error: --type flag is required[/red]")
console.print("[yellow]Usage: run-type --type <type_name>[/yellow]") console.print("[yellow]Usage: run-type --type <type_name>[/yellow]")
return return
benchmark_type = type_flag.input_value benchmark_type = type_flag.input_value
if not type_flag.status == ValidationStatus.VALID: if not type_flag.status == ValidationStatus.VALID:
console.print(f"[red]Error: No benchmarks found for type '{benchmark_type}'[/red]") console.print(f"[red]Error: No benchmarks found for type '{benchmark_type}'[/red]")
console.print("\n[yellow]Available types:[/yellow]") console.print("\n[yellow]Available types:[/yellow]")
@@ -102,23 +91,17 @@ def run_type_handler(response: Response) -> None:
for t in types: for t in types:
console.print(f"{t}") console.print(f"{t}")
return return
report_generator = ReportTableGenerator(get_system_info()) report_generator = ReportTableGenerator(get_system_info())
without_system_info = response.input_flags.get_flag_by_name( without_system_info = response.input_flags.get_flag_by_name("without-system-info", with_status=ValidationStatus.VALID)
"without-system-info", with_status=ValidationStatus.VALID
)
if not without_system_info: if not without_system_info:
console.print(report_generator.generate_system_info_header()) console.print(report_generator.generate_system_info_header())
console.print(report_generator.generate_system_info_table()) console.print(report_generator.generate_system_info_table())
is_gc_disabled = response.input_flags.get_flag_by_name( is_gc_disabled = response.input_flags.get_flag_by_name("without-gc", with_status=ValidationStatus.VALID, default=False)
"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))
)
benchmark_group_result = registered_benchmarks.run_benchmarks_by_type(
benchmark_type, is_gc_disabled=bool(is_gc_disabled)
)
console.print(report_generator.generate_benchmark_table_header(benchmark_group_result)) console.print(report_generator.generate_benchmark_table_header(benchmark_group_result))
console.print(report_generator.generate_benchmark_report_table(benchmark_group_result)) console.print(report_generator.generate_benchmark_report_table(benchmark_group_result))
@@ -126,25 +109,26 @@ def run_type_handler(response: Response) -> None:
@router.command(Command("release-generate", description="Generate release report")) @router.command(Command("release-generate", description="Generate release report"))
def release_generate_handler(_: Response) -> None: def release_generate_handler(_: Response) -> None:
lib_version = version("argenta") lib_version = version("argenta")
console.print(f"[cyan]Generating release report for version:[/cyan] [bold]{lib_version}[/bold]") console.print(f"[cyan]Generating release report for version:[/cyan] [bold]{lib_version}[/bold]")
console.print("[dim]Running benchmarks (1000 iterations, GC disabled)...[/dim]\n") console.print("[dim]Running benchmarks (1000 iterations, GC disabled)...[/dim]\n")
type_grouped_benchmarks: list[BenchmarkGroupResult] = ( type_grouped_benchmarks: list[BenchmarkGroupResult] = registered_benchmarks.run_benchmarks_grouped_by_type(
registered_benchmarks.run_benchmarks_grouped_by_type(iterations=1000, is_gc_disabled=True) iterations=1000,
is_gc_disabled=True
) )
release_generator = ReleaseGenerator(lib_version) release_generator = ReleaseGenerator(lib_version)
output_dir = release_generator.generate_release(type_grouped_benchmarks) output_dir = release_generator.generate_release(type_grouped_benchmarks)
console.print("[green]✓[/green] Benchmarks completed. Generating release report...\n") console.print(f"[green]✓[/green] Benchmarks completed. Generating release report...\n")
for benchmark_group in type_grouped_benchmarks: for benchmark_group in type_grouped_benchmarks:
console.print(f"[cyan]Generated for:[/cyan] [bold]{benchmark_group.type_}[/bold]") console.print(f"[cyan]Generated for:[/cyan] [bold]{benchmark_group.type_}[/bold]")
console.print(f" [green]✓[/green] {benchmark_group.type_}_comparison.png") console.print(f" [green]✓[/green] {benchmark_group.type_}_comparison.png")
console.print(f" [green]✓[/green] {benchmark_group.type_}.json\n") console.print(f" [green]✓[/green] {benchmark_group.type_}.json\n")
console.print("[bold green]✓ Release report generated successfully[/bold green]") console.print(f"[bold green]✓ Release report generated successfully[/bold green]")
console.print(f"[cyan]Output directory:[/cyan] [bold]{output_dir}[/bold]") console.print(f"[cyan]Output directory:[/cyan] [bold]{output_dir}[/bold]")
@@ -152,33 +136,26 @@ def release_generate_handler(_: Response) -> None:
Command( Command(
"diagrams-generate", "diagrams-generate",
description="Generate diagrams for all benchmarks", description="Generate diagrams for all benchmarks",
flags=Flags( flags=Flags([
[ Flag('without-gc', possible_values=PossibleValues.NEITHER),
Flag("without-gc", possible_values=PossibleValues.NEITHER), Flag('iterations', possible_values=POSITIVE_INTEGER_PATTERN)
Flag("iterations", possible_values=POSITIVE_INTEGER_PATTERN), ])
]
),
) )
) )
def diagrams_generate_handler(response: Response) -> None: def diagrams_generate_handler(response: Response) -> None:
iterations = 100 iterations = 100
iterations_flag = response.input_flags.get_flag_by_name( iterations_flag = response.input_flags.get_flag_by_name("iterations", with_status=ValidationStatus.VALID)
"iterations", with_status=ValidationStatus.VALID
)
if iterations_flag: if iterations_flag:
iterations = int(iterations_flag.input_value) iterations = int(iterations_flag.input_value)
is_gc_disabled = bool( is_gc_disabled = bool(response.input_flags.get_flag_by_name("without-gc", with_status=ValidationStatus.VALID))
response.input_flags.get_flag_by_name("without-gc", with_status=ValidationStatus.VALID)
)
console.print("[cyan]Running all benchmarks...[/cyan]") console.print("[cyan]Running all benchmarks...[/cyan]")
console.print(f"[dim]Iterations: {iterations}, GC Disabled: {is_gc_disabled}[/dim]\n") console.print(f"[dim]Iterations: {iterations}, GC Disabled: {is_gc_disabled}[/dim]\n")
type_grouped_benchmarks: list[BenchmarkGroupResult] = ( type_grouped_benchmarks: list[BenchmarkGroupResult] = registered_benchmarks.run_benchmarks_grouped_by_type(
registered_benchmarks.run_benchmarks_grouped_by_type( iterations=iterations,
iterations=iterations, is_gc_disabled=is_gc_disabled is_gc_disabled=is_gc_disabled
)
) )
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
@@ -186,17 +163,17 @@ def diagrams_generate_handler(response: Response) -> None:
output_dir.mkdir(parents=True, exist_ok=True) output_dir.mkdir(parents=True, exist_ok=True)
diagram_generator = DiagramGenerator(output_dir) diagram_generator = DiagramGenerator(output_dir)
console.print("[green]✓[/green] Benchmarks completed. Generating diagrams...\n") console.print(f"[green]✓[/green] Benchmarks completed. Generating diagrams...\n")
generated_count = 0 generated_count = 0
for benchmark_group in type_grouped_benchmarks: for benchmark_group in type_grouped_benchmarks:
console.print(f"[cyan]Generating diagram for:[/cyan] [bold]{benchmark_group.type_}[/bold]") console.print(f"[cyan]Generating diagram for:[/cyan] [bold]{benchmark_group.type_}[/bold]")
comparison_path = diagram_generator.generate_comparison_diagram(benchmark_group) comparison_path = diagram_generator.generate_comparison_diagram(benchmark_group)
generated_count += 1 generated_count += 1
console.print(f" [green]✓[/green] {comparison_path.name}\n") console.print(f" [green]✓[/green] {comparison_path.name}\n")
console.print(f"[bold green]✓ Successfully generated {generated_count} diagrams[/bold green]") console.print(f"[bold green]✓ Successfully generated {generated_count} diagrams[/bold green]")
console.print(f"[cyan]Output directory:[/cyan] [bold]{output_dir}[/bold]") 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 .diagram_generator import DiagramGenerator
from .release_generator import ReleaseGenerator
from .report_table_generator import ReportTableGenerator from .report_table_generator import ReportTableGenerator
from .system_info_reader import get_system_info from .system_info_reader import get_system_info
from .release_generator import ReleaseGenerator
__all__ = ["DiagramGenerator", "ReportTableGenerator", "get_system_info", "ReleaseGenerator"] __all__ = ["DiagramGenerator", "ReportTableGenerator", "get_system_info", "ReleaseGenerator"]
+75 -58
View File
@@ -2,9 +2,8 @@ __all__ = ["DiagramGenerator"]
from pathlib import Path from pathlib import Path
import cairosvg import matplotlib
import pygal import matplotlib.pyplot as plt
from pygal.style import Style
from ..benchmarks.core.models import BenchmarkGroupResult from ..benchmarks.core.models import BenchmarkGroupResult
@@ -13,26 +12,8 @@ class DiagramGenerator:
def __init__(self, output_dir: Path | str) -> None: def __init__(self, output_dir: Path | str) -> None:
self.output_dir: Path = Path(output_dir) if isinstance(output_dir, str) else output_dir self.output_dir: Path = Path(output_dir) if isinstance(output_dir, str) else output_dir
self._style = Style( matplotlib.use('Agg')
background="white", plt.style.use('seaborn-v0_8-whitegrid')
plot_background="white",
foreground="#2c3e50",
foreground_strong="#000000",
foreground_subtle="#7f8c8d",
opacity=".9",
opacity_hover=".95",
transition="150ms ease-in",
colors=("#2ecc71", "#3498db", "#e74c3c"),
title_font_size=40,
legend_font_size=34,
label_font_size=32, #
major_label_font_size=32,
value_font_size=28,
value_label_font_size=28,
tooltip_font_size=24,
no_data_font_size=28,
font_family="Consolas, 'Courier New', monospace",
)
def generate_comparison_diagram(self, benchmark_group: BenchmarkGroupResult) -> Path: def generate_comparison_diagram(self, benchmark_group: BenchmarkGroupResult) -> Path:
results = benchmark_group.benchmark_results results = benchmark_group.benchmark_results
@@ -46,48 +27,84 @@ class DiagramGenerator:
max_value = max( max_value = max(
max(avg_times) if avg_times else 0, max(avg_times) if avg_times else 0,
max(median_times) if median_times else 0, max(median_times) if median_times else 0,
max(std_devs) if std_devs else 0, max(std_devs) if std_devs else 0
) )
y_limit = max_value / 0.85 if max_value > 0 else 1.0 y_limit = max_value / 0.85 if max_value > 0 else 1.0
title_text = f"{benchmark_group.type_.replace('_', ' ').title()}" items_count = len(descriptions)
metadata_text = ( x_positions: list[int] = list(range(items_count))
f"Iterations: {benchmark_group.iterations} | GC: "
f"{'Disabled' if benchmark_group.is_gc_disabled else 'Enabled'}" 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()
filename = f"{benchmark_group.type_}_comparison.png" filename = f"{benchmark_group.type_}_comparison.png"
output_path = self.output_dir / filename output_path = self.output_dir / filename
self.output_dir.mkdir(parents=True, exist_ok=True) self.output_dir.mkdir(parents=True, exist_ok=True)
dynamic_height = 600 + (len(descriptions) * 150) plt.savefig(output_path, dpi=200, bbox_inches='tight', facecolor='white')
plt.close(fig)
chart = pygal.HorizontalBar(
style=self._style,
width=3100,
height=dynamic_height,
explicit_size=True,
show_legend=True,
legend_at_bottom=True,
print_values=True,
print_values_position="top",
legend_at_bottom_columns=3,
range=(0, y_limit),
zero=0,
)
chart.title = f"{title_text}\n{metadata_text}"
chart.x_title = "Time (ms)"
chart.no_data_text = "No data"
chart.value_formatter = lambda x: f"{x:.3f}"
chart.x_labels = descriptions
chart.add("Std Deviation", std_devs)
chart.add("Average Time", avg_times)
chart.add("Median Time", median_times)
svg_bytes = chart.render()
cairosvg.svg2png(bytestring=svg_bytes, write_to=str(output_path))
return output_path return output_path
+10 -10
View File
@@ -12,20 +12,20 @@ class ReleaseGenerator:
def __init__(self, lib_version: str) -> None: def __init__(self, lib_version: str) -> None:
self.lib_version = lib_version self.lib_version = lib_version
self.output_dir = Path("metrics/reports/releases") / lib_version self.output_dir = Path("metrics/reports/releases") / lib_version
def generate_release(self, benchmark_groups: list[BenchmarkGroupResult]) -> Path: def generate_release(self, benchmark_groups: list[BenchmarkGroupResult]) -> Path:
if self.output_dir.exists(): if self.output_dir.exists():
shutil.rmtree(self.output_dir) shutil.rmtree(self.output_dir)
self.output_dir.mkdir(parents=True, exist_ok=True) self.output_dir.mkdir(parents=True, exist_ok=True)
for benchmark_group in benchmark_groups: for benchmark_group in benchmark_groups:
type_dir = self.output_dir / benchmark_group.type_ type_dir = self.output_dir / benchmark_group.type_
type_dir.mkdir(exist_ok=True) type_dir.mkdir(exist_ok=True)
diagram_generator = DiagramGenerator(type_dir) diagram_generator = DiagramGenerator(type_dir)
diagram_generator.generate_comparison_diagram(benchmark_group) diagram_generator.generate_comparison_diagram(benchmark_group)
json_data = { json_data = {
"type": benchmark_group.type_, "type": benchmark_group.type_,
"iterations": benchmark_group.iterations, "iterations": benchmark_group.iterations,
@@ -36,14 +36,14 @@ class ReleaseGenerator:
"description": br.description, "description": br.description,
"avg_time": br.avg_time, "avg_time": br.avg_time,
"median_time": br.median_time, "median_time": br.median_time,
"std_dev": br.std_dev, "std_dev": br.std_dev
} }
for br in benchmark_group.benchmark_results for br in benchmark_group.benchmark_results
], ]
} }
json_path = type_dir / f"{benchmark_group.type_}.json" json_path = type_dir / f"{benchmark_group.type_}.json"
with open(json_path, "w", encoding="utf-8") as f: with open(json_path, 'w', encoding='utf-8') as f:
json.dump(json_data, f, indent=2, ensure_ascii=False) json.dump(json_data, f, indent=2, ensure_ascii=False)
return self.output_dir return self.output_dir
+14 -22
View File
@@ -3,7 +3,7 @@ from rich.table import Table
from rich.text import Text from rich.text import Text
from ..benchmarks.core.models import BenchmarkGroupResult from ..benchmarks.core.models import BenchmarkGroupResult
from .system_info_reader import SystemInfo from metrics.services.system_info_reader import SystemInfo
class ReportTableGenerator: class ReportTableGenerator:
@@ -12,15 +12,11 @@ class ReportTableGenerator:
self._cached_benchmark_tables: dict[int, Table] = {} self._cached_benchmark_tables: dict[int, Table] = {}
self._cached_system_info_table: Table | None = None self._cached_system_info_table: Table | None = None
def generate_benchmark_report_table( def generate_benchmark_report_table(self, benchmark_group_result: BenchmarkGroupResult) -> Table:
self, benchmark_group_result: BenchmarkGroupResult
) -> Table:
if cached_result := self._cached_benchmark_tables.get(id(benchmark_group_result)): if cached_result := self._cached_benchmark_tables.get(id(benchmark_group_result)):
return cached_result return cached_result
table = Table( table = Table(show_header=True, header_style="bold cyan", border_style="blue", show_lines=True)
show_header=True, header_style="bold cyan", border_style="blue", show_lines=True
)
table.add_column("Description", style="dim") table.add_column("Description", style="dim")
table.add_column("Avg Time", justify="right", style="bold yellow") table.add_column("Avg Time", justify="right", style="bold yellow")
table.add_column("Median Time", justify="right", style="bold yellow") table.add_column("Median Time", justify="right", style="bold yellow")
@@ -38,22 +34,18 @@ class ReportTableGenerator:
@staticmethod @staticmethod
def generate_benchmark_table_header(benchmark_group_result: BenchmarkGroupResult) -> Panel: def generate_benchmark_table_header(benchmark_group_result: BenchmarkGroupResult) -> Panel:
header_text = Text( header_text = Text(f"TYPE: {benchmark_group_result.type_.upper()} ; "
f"TYPE: {benchmark_group_result.type_.upper()} ; " f"ITERATIONS: {benchmark_group_result.iterations} ; "
f"ITERATIONS: {benchmark_group_result.iterations} ; " f"GC {"DISABLED" if benchmark_group_result.is_gc_disabled else "ENABLED"} ; "
f"GC {'DISABLED' if benchmark_group_result.is_gc_disabled else 'ENABLED'} ; " f"ALL TIME IN MS",
f"ALL TIME IN MS", style="bold magenta")
style="bold magenta",
)
return Panel(header_text, expand=False, border_style="magenta") return Panel(header_text, expand=False, border_style="magenta")
def generate_system_info_table(self) -> Table: def generate_system_info_table(self) -> Table:
if self._cached_system_info_table is not None: if self._cached_system_info_table is not None:
return self._cached_system_info_table return self._cached_system_info_table
table = Table( table = Table(show_header=True, header_style="bold cyan", border_style="blue", show_lines=True)
show_header=True, header_style="bold cyan", border_style="blue", show_lines=True
)
table.add_column("Parameter", style="green") table.add_column("Parameter", style="green")
table.add_column("Value", style="yellow") table.add_column("Value", style="yellow")
@@ -63,10 +55,10 @@ class ReportTableGenerator:
table.add_row("CPU", self.system_info.cpu_info.name) table.add_row("CPU", self.system_info.cpu_info.name)
table.add_row("CPU Physical Cores", str(self.system_info.cpu_info.physical_cores)) table.add_row("CPU Physical Cores", str(self.system_info.cpu_info.physical_cores))
table.add_row("CPU Logical Cores", str(self.system_info.cpu_info.logical_cores)) table.add_row("CPU Logical Cores", str(self.system_info.cpu_info.logical_cores))
table.add_row("CPU Max Frequency", str(self.system_info.cpu_info.max_frequency) + " GHz") table.add_row("CPU Max Frequency", str(self.system_info.cpu_info.max_frequency) + ' GHz')
table.add_row("Total RAM", str(self.system_info.memory_info.total_ram) + " GB") table.add_row("Total RAM", str(self.system_info.memory_info.total_ram) + ' GB')
table.add_row("Used RAM", str(self.system_info.memory_info.used_ram) + " GB") table.add_row("Used RAM", str(self.system_info.memory_info.used_ram) + ' GB')
table.add_row("Available RAM", str(self.system_info.memory_info.available_ram) + " GB") table.add_row("Available RAM", str(self.system_info.memory_info.available_ram) + ' GB')
table.add_row("Python Version", self.system_info.python_info.version) table.add_row("Python Version", self.system_info.python_info.version)
table.add_row("Python Implementation", self.system_info.python_info.implementation) table.add_row("Python Implementation", self.system_info.python_info.implementation)
table.add_row("Python Compiler", self.system_info.python_info.compiler) table.add_row("Python Compiler", self.system_info.python_info.compiler)
@@ -77,4 +69,4 @@ class ReportTableGenerator:
@staticmethod @staticmethod
def generate_system_info_header() -> Panel: def generate_system_info_header() -> Panel:
header_text = Text("SYSTEM INFO", style="bold magenta") 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")
+41 -36
View File
@@ -1,19 +1,28 @@
__all__ = ["SystemInfo", "get_system_info"] __all__ = [
"SystemInfo",
"get_system_info"
]
from dataclasses import dataclass
import platform import platform
import sys import sys
from dataclasses import dataclass
import cpuinfo import cpuinfo
import psutil import psutil
@dataclass(frozen=True, slots=True)
class SystemInfo:
os_info: OSInfo
cpu_info: CPUInfo
memory_info: MemoryInfo
python_info: PythonInfo
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class OSInfo: class OSInfo:
name: str name: str
kernel_version: str kernel_version: str
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class CPUInfo: class CPUInfo:
name: str name: str
@@ -22,13 +31,11 @@ class CPUInfo:
logical_cores: int logical_cores: int
max_frequency: float max_frequency: float
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class MemoryInfo: class MemoryInfo:
total_ram: float # in GB total_ram: float # in GB
used_ram: float # in GB used_ram: float # in GB
available_ram: float # in GB available_ram: float # in GB
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class PythonInfo: class PythonInfo:
@@ -37,6 +44,18 @@ class PythonInfo:
compiler: str compiler: str
def get_system_info() -> SystemInfo:
os_info = get_os_info()
cpu_info = get_cpu_info()
memory_info = get_memory_info()
python_info = get_python_info()
return SystemInfo(
os_info=os_info,
cpu_info=cpu_info,
memory_info=memory_info,
python_info=python_info,
)
def get_os_info() -> OSInfo: def get_os_info() -> OSInfo:
system = platform.system() system = platform.system()
@@ -54,17 +73,22 @@ def get_os_info() -> OSInfo:
kernel_version=kernel_version, kernel_version=kernel_version,
) )
elif system == "Darwin": 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: else:
return OSInfo(kernel_version=platform.release(), name=platform.system()) return OSInfo(
kernel_version=platform.release(),
name=platform.system()
)
def get_cpu_info() -> CPUInfo: def get_cpu_info() -> CPUInfo:
cpu_info = cpuinfo.get_cpu_info() cpu_info = cpuinfo.get_cpu_info()
cpu_name = cpu_info["brand_raw"] cpu_name = cpu_info["brand_raw"]
cpu_architecture = cpu_info["arch"] cpu_architecture = cpu_info["arch"]
cpu_physical_cores = psutil.cpu_count(logical=False) or 0 cpu_physical_cores = psutil.cpu_count(logical=False)
cpu_logical_cores = psutil.cpu_count(logical=True) or 0 cpu_logical_cores = psutil.cpu_count(logical=True)
cpu_freq = psutil.cpu_freq() cpu_freq = psutil.cpu_freq()
cpu_max_frequency = cpu_freq.max cpu_max_frequency = cpu_freq.max
@@ -74,10 +98,9 @@ def get_cpu_info() -> CPUInfo:
architecture=cpu_architecture, architecture=cpu_architecture,
physical_cores=cpu_physical_cores, physical_cores=cpu_physical_cores,
logical_cores=cpu_logical_cores, logical_cores=cpu_logical_cores,
max_frequency=cpu_max_frequency, max_frequency=cpu_max_frequency
) )
def get_memory_info() -> MemoryInfo: def get_memory_info() -> MemoryInfo:
mem = psutil.virtual_memory() mem = psutil.virtual_memory()
total_ram = round(mem.total / (1024**3), 2) total_ram = round(mem.total / (1024**3), 2)
@@ -90,32 +113,14 @@ def get_memory_info() -> MemoryInfo:
available_ram=available_ram, available_ram=available_ram,
) )
def get_python_info() -> PythonInfo: def get_python_info() -> PythonInfo:
python_version = platform.python_version() python_version = platform.python_version()
python_implementation = platform.python_implementation() python_implementation = platform.python_implementation()
python_compiler = platform.python_compiler() python_compiler = platform.python_compiler()
return PythonInfo( return PythonInfo(
version=python_version, 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
@@ -0,0 +1,18 @@
from argenta import App, Command, Response, Router
app = App(override_system_messages=True)
router = Router()
@router.command(Command('command'))
def handler(_res: Response) -> None:
pass
@router.command(Command('command_other'))
def handler2(_res: Response) -> None:
pass
app.include_routers(router)
app._pre_cycle_setup()
assert app._most_similar_command('command_') == 'command'
+1 -1
View File
@@ -9,7 +9,7 @@ orchestrator: Orchestrator = Orchestrator()
def main() -> None: def main() -> None:
app.include_router(router) app.include_router(router)
orchestrator.run_repl(app) orchestrator.start_polling(app)
if __name__ == '__main__': if __name__ == '__main__':
main() main()
+2 -3
View File
@@ -3,13 +3,12 @@ from prompt_toolkit import HTML
from argenta import App, Orchestrator from argenta import App, Orchestrator
from argenta.app import PredefinedMessages, StaticDividingLine, AutoCompleter from argenta.app import PredefinedMessages, StaticDividingLine, AutoCompleter
from argenta.app.dividing_line.models import DynamicDividingLine from argenta.app.dividing_line.models import DynamicDividingLine
from argenta.orchestrator import ArgParser
from mock.mock_app.routers import work_router from mock.mock_app.routers import work_router
app: App = App( app: App = App(
dividing_line=StaticDividingLine('~') dividing_line=StaticDividingLine('~')
) )
orchestrator: Orchestrator = Orchestrator(arg_parser=ArgParser(processed_args=[])) orchestrator: Orchestrator = Orchestrator()
def main(): def main():
@@ -19,7 +18,7 @@ def main():
app.add_message_on_startup(PredefinedMessages.AUTOCOMPLETE) app.add_message_on_startup(PredefinedMessages.AUTOCOMPLETE)
app.add_message_on_startup(PredefinedMessages.HELP) app.add_message_on_startup(PredefinedMessages.HELP)
orchestrator.run_repl(app) orchestrator.start_polling(app)
if __name__ == "__main__": if __name__ == "__main__":
main() main()
-13
View File
@@ -13,19 +13,12 @@ dependencies = [
"prompt-toolkit>=3.0.52", "prompt-toolkit>=3.0.52",
] ]
[project.optional-dependencies]
cli = [
"nuitka[onefile]>=4.0.5",
"typer>=0.9,!=0.12,<=0.21.1",
]
[dependency-groups] [dependency-groups]
dev = [ dev = [
{include-group = "linters"}, {include-group = "linters"},
{include-group = "typecheckers"}, {include-group = "typecheckers"},
{include-group = "docs"}, {include-group = "docs"},
{include-group = "tests"}, {include-group = "tests"},
{include-group = "metrics"},
"scriv>=1.8.0", "scriv>=1.8.0",
] ]
linters = [ linters = [
@@ -53,19 +46,13 @@ metrics = [
"matplotlib>=3.10.8", "matplotlib>=3.10.8",
"psutil>=7.2.1", "psutil>=7.2.1",
"py-cpuinfo>=9.0.0", "py-cpuinfo>=9.0.0",
"cairosvg>=2.8.2",
"pygal>=3.1.0",
] ]
[project.scripts]
argenta = "argenta._cli.__main__:main"
[tool.ruff] [tool.ruff]
line-length=100 line-length=100
[tool.pyright] [tool.pyright]
typeCheckingMode = "strict" typeCheckingMode = "strict"
reportMissingTypeStubs = false
[[tool.pyright.executionEnvironments]] [[tool.pyright.executionEnvironments]]
root = "tests/" root = "tests/"
View File
-60
View File
@@ -1,60 +0,0 @@
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
@@ -1,6 +0,0 @@
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
@@ -1,58 +0,0 @@
__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
@@ -1,36 +0,0 @@
__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
@@ -1,107 +0,0 @@
__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
@@ -1,115 +0,0 @@
__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
@@ -1,82 +0,0 @@
__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
@@ -1,24 +0,0 @@
__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()
@@ -1,7 +0,0 @@
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
@@ -1,136 +0,0 @@
__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)
@@ -1,26 +0,0 @@
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"
@@ -1,122 +0,0 @@
__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,
)
+114 -22
View File
@@ -1,21 +1,86 @@
from __future__ import annotations
__all__ = ["AutoCompleter"] __all__ = ["AutoCompleter"]
import sys import sys
from typing import TYPE_CHECKING from typing import Callable, Iterable
if TYPE_CHECKING: from prompt_toolkit import HTML, PromptSession
from prompt_toolkit import PromptSession, HTML 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
class AutoCompleter: class AutoCompleter:
def __init__( def __init__(
self, self,
history_filename: str | None = None, history_filename: str | None = None,
autocomplete_button: str = "tab", autocomplete_button: str = "tab",
command_highlighting: bool = True, command_highlighting: bool = True,
auto_suggestions: bool = True, auto_suggestions: bool = True,
) -> None: ) -> None:
self.history_filename: str | None = history_filename self.history_filename: str | None = history_filename
self.autocomplete_button: str = autocomplete_button self.autocomplete_button: str = autocomplete_button
@@ -29,15 +94,42 @@ class AutoCompleter:
self._session = None self._session = None
self._fallback_mode = True self._fallback_mode = True
return return
from ._ext_features_impl import build_session
self._session = build_session( kb = KeyBindings()
self.history_filename,
self.autocomplete_button, def _(event: KeyPressEvent) -> None:
self.command_highlighting, buff = event.app.current_buffer
self.auto_suggestions, if buff.complete_state:
all_commands 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,
) )
def prompt(self, prompt_text: str | HTML = ">>> ") -> str: def prompt(self, prompt_text: str | HTML = ">>> ") -> str:
@@ -45,7 +137,7 @@ class AutoCompleter:
return input(prompt_text if isinstance(prompt_text, str) else ">>> ") return input(prompt_text if isinstance(prompt_text, str) else ">>> ")
if self._session is None: if self._session is None:
raise RuntimeError("Call initial_setup() before using prompt()") raise RuntimeError("Call initial_setup() before using prompt()")
return self._session.prompt(
from ._ext_features_impl import do_prompt HTML(prompt_text) if isinstance(prompt_text, str) else prompt_text,
cursor=CursorShape.BLINKING_BEAM
return do_prompt(self._session, prompt_text) )
+5 -4
View File
@@ -3,6 +3,8 @@ __all__ = ["App"]
import difflib import difflib
from typing import Never, TypeAlias from typing import Never, TypeAlias
from rich.console import Console
from argenta.app.autocompleter import AutoCompleter from argenta.app.autocompleter import AutoCompleter
from argenta.app.behavior_handlers.models import (BehaviorHandlersFabric, from argenta.app.behavior_handlers.models import (BehaviorHandlersFabric,
BehaviorHandlersSettersMixin) BehaviorHandlersSettersMixin)
@@ -142,7 +144,7 @@ class BaseApp(BehaviorHandlersSettersMixin):
is_stdout_redirected_by_router=processing_router.is_redirect_stdout_disabled is_stdout_redirected_by_router=processing_router.is_redirect_stdout_disabled
) )
def _run_repl(self) -> None: def _run_polling(self) -> None:
self._viewer.view_initial_message(self._initial_message) self._viewer.view_initial_message(self._initial_message)
self._pre_cycle_setup() self._pre_cycle_setup()
while True: while True:
@@ -187,7 +189,7 @@ class App(BaseApp):
repeat_command_groups_printing: bool = False, repeat_command_groups_printing: bool = False,
override_system_messages: bool = False, override_system_messages: bool = False,
autocompleter: AutoCompleter | None = None, autocompleter: AutoCompleter | None = None,
printer: Printer | None = None, printer: Printer = Console().print,
) -> None: ) -> None:
""" """
Public. The essence of the application itself. Public. The essence of the application itself.
@@ -204,7 +206,6 @@ class App(BaseApp):
:param printer: system messages text output function :param printer: system messages text output function
:return: None :return: None
""" """
from rich.console import Console
super().__init__( super().__init__(
prompt=prompt, prompt=prompt,
initial_message=initial_message, initial_message=initial_message,
@@ -215,7 +216,7 @@ class App(BaseApp):
repeat_command_groups_printing=repeat_command_groups_printing, repeat_command_groups_printing=repeat_command_groups_printing,
override_system_messages=override_system_messages, override_system_messages=override_system_messages,
autocompleter=autocompleter or AutoCompleter(), autocompleter=autocompleter or AutoCompleter(),
printer=printer or Console().print, printer=printer,
) )
def include_router(self, router: Router) -> None: def include_router(self, router: Router) -> None:
+5 -2
View File
@@ -7,8 +7,9 @@ __all__ = [
"HandlerFunc", "HandlerFunc",
] ]
from typing import Any, Protocol, TypeVar, Callable from typing import Any, Protocol, TypeVar
from argenta.response import Response
T = TypeVar("T", contravariant=True) T = TypeVar("T", contravariant=True)
@@ -38,4 +39,6 @@ class DescriptionMessageGenerator(Protocol):
raise NotImplementedError raise NotImplementedError
type HandlerFunc = Callable[..., Any] class HandlerFunc(Protocol):
def __call__(self, response: Response, /, *args: Any, **kwargs: Any) -> None:
raise NotImplementedError
+6 -8
View File
@@ -1,7 +1,5 @@
__all__ = ["Orchestrator"] __all__ = ["Orchestrator"]
import os
from dishka import Provider, make_container from dishka import Provider, make_container
from argenta.app import App from argenta.app import App
@@ -9,12 +7,13 @@ from argenta.di.integration import setup_dishka
from argenta.di.providers import SystemProvider from argenta.di.providers import SystemProvider
from argenta.orchestrator.argparser import ArgParser from argenta.orchestrator.argparser import ArgParser
DEFAULT_ARGPARSER: ArgParser = ArgParser(processed_args=[])
class Orchestrator: class Orchestrator:
def __init__( def __init__(
self, self,
arg_parser: ArgParser | None = None, arg_parser: ArgParser = DEFAULT_ARGPARSER,
custom_providers: list[Provider] | None = None, custom_providers: list[Provider] | None = None,
auto_inject_handlers: bool = True, auto_inject_handlers: bool = True,
): ):
@@ -23,14 +22,13 @@ class Orchestrator:
:param arg_parser: Cmd argument parser and configurator at startup :param arg_parser: Cmd argument parser and configurator at startup
:return: None :return: None
""" """
self._arg_parser: ArgParser | None = arg_parser if not os.getenv('RUN_FROM_ARGENTA_RUNNER') else None self._arg_parser: ArgParser = arg_parser
self._custom_providers: list[Provider] = custom_providers or [] self._custom_providers: list[Provider] = custom_providers or []
self._auto_inject_handlers: bool = auto_inject_handlers self._auto_inject_handlers: bool = auto_inject_handlers
if self._arg_parser is not None: self._arg_parser._parse_args() # pyright: ignore[reportPrivateUsage]
self._arg_parser._parse_args() # pyright: ignore[reportPrivateUsage]
def run_repl(self, app: App) -> None: def start_polling(self, app: App) -> None:
""" """
Public. Starting the user input processing cycle Public. Starting the user input processing cycle
:param app: a running application :param app: a running application
@@ -41,4 +39,4 @@ class Orchestrator:
) )
setup_dishka(app, container, auto_inject=self._auto_inject_handlers) setup_dishka(app, container, auto_inject=self._auto_inject_handlers)
app._run_repl() # pyright: ignore[reportPrivateUsage] app._run_polling()
+1 -6
View File
@@ -1,11 +1,6 @@
from __future__ import annotations
__all__ = ["Response"] __all__ = ["Response"]
from typing import TYPE_CHECKING from dishka import Container
if TYPE_CHECKING:
from dishka import Container
from argenta.command import InputFlags from argenta.command import InputFlags
from argenta.response.status import ResponseStatus from argenta.response.status import ResponseStatus
+3 -2
View File
@@ -3,6 +3,8 @@ __all__ = ["Router"]
from inspect import get_annotations, getfullargspec, getsourcefile, getsourcelines from inspect import get_annotations, getfullargspec, getsourcefile, getsourcelines
from typing import Callable from typing import Callable
from rich.console import Console
from argenta.app.protocols import HandlerFunc from argenta.app.protocols import HandlerFunc
from argenta.command import Command, InputCommand, InputFlags from argenta.command import Command, InputCommand, InputFlags
from argenta.command.flag import ValidationStatus from argenta.command.flag import ValidationStatus
@@ -18,7 +20,7 @@ from argenta.router.exceptions import (RepeatedAliasNameException,
class Router: class Router:
def __init__( def __init__(
self, self,
title: str = "Title", title: str = "Default title",
*, *,
disable_redirect_stdout: bool = False, disable_redirect_stdout: bool = False,
): ):
@@ -173,7 +175,6 @@ class Router:
response_arg_annotation = func_annotations.get(response_arg) response_arg_annotation = func_annotations.get(response_arg)
if response_arg_annotation is not None and response_arg_annotation is not Response: if response_arg_annotation is not None and response_arg_annotation is not Response:
from rich.console import Console
source_line: int = getsourcelines(func)[1] source_line: int = getsourcelines(func)[1]
Console().print( Console().print(
f'\nFile "{getsourcefile(func)}", line {source_line}\n[b red]WARNING:[/b red] [i]The typehint ' f'\nFile "{getsourcefile(func)}", line {source_line}\n[b red]WARNING:[/b red] [i]The typehint '
@@ -38,7 +38,7 @@ def test_empty_input_triggers_empty_command_handler(monkeypatch: pytest.MonkeyPa
app = App(override_system_messages=True, printer=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
app.set_empty_command_handler(lambda: print('Empty input command')) app.set_empty_command_handler(lambda: print('Empty input command'))
orchestrator.run_repl(app) orchestrator.start_polling(app)
output = capsys.readouterr().out output = capsys.readouterr().out
@@ -64,7 +64,7 @@ def test_unknown_command_triggers_unknown_command_handler(monkeypatch: pytest.Mo
app = App(override_system_messages=True, printer=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.trigger}')) app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.trigger}'))
orchestrator.run_repl(app) orchestrator.start_polling(app)
output = capsys.readouterr().out output = capsys.readouterr().out
@@ -85,7 +85,7 @@ def test_mixed_valid_and_unknown_commands_handled_correctly(monkeypatch: pytest.
app = App(override_system_messages=True, printer=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.trigger}')) app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.trigger}'))
orchestrator.run_repl(app) orchestrator.start_polling(app)
output = capsys.readouterr().out output = capsys.readouterr().out
@@ -110,7 +110,7 @@ def test_multiple_commands_with_unknown_command_in_between(monkeypatch: pytest.M
app = App(override_system_messages=True, printer=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.trigger}')) app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.trigger}'))
orchestrator.run_repl(app) orchestrator.start_polling(app)
output = capsys.readouterr().out output = capsys.readouterr().out
@@ -137,7 +137,7 @@ def test_unregistered_flag_without_value_is_accessible(monkeypatch: pytest.Monke
app = App(override_system_messages=True, printer=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
orchestrator.run_repl(app) orchestrator.start_polling(app)
output = capsys.readouterr().out output = capsys.readouterr().out
@@ -161,7 +161,7 @@ def test_unregistered_flag_with_value_is_accessible(monkeypatch: pytest.MonkeyPa
app = App(override_system_messages=True, printer=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
orchestrator.run_repl(app) orchestrator.start_polling(app)
output = capsys.readouterr().out output = capsys.readouterr().out
@@ -184,7 +184,7 @@ def test_registered_and_unregistered_flags_coexist(monkeypatch: pytest.MonkeyPat
app = App(override_system_messages=True, printer=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
orchestrator.run_repl(app) orchestrator.start_polling(app)
output = capsys.readouterr().out output = capsys.readouterr().out
@@ -210,7 +210,7 @@ def test_flag_without_value_triggers_incorrect_syntax_handler(monkeypatch: pytes
app = App(override_system_messages=True, printer=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
app.set_incorrect_input_syntax_handler(lambda command: print(f'Incorrect flag syntax: "{command}"')) app.set_incorrect_input_syntax_handler(lambda command: print(f'Incorrect flag syntax: "{command}"'))
orchestrator.run_repl(app) orchestrator.start_polling(app)
output = capsys.readouterr().out output = capsys.readouterr().out
@@ -236,7 +236,7 @@ def test_repeated_flags_trigger_repeated_flags_handler(monkeypatch: pytest.Monke
app = App(override_system_messages=True, printer=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
app.set_repeated_input_flags_handler(lambda command: print(f'Repeated input flags: "{command}"')) app.set_repeated_input_flags_handler(lambda command: print(f'Repeated input flags: "{command}"'))
orchestrator.run_repl(app) orchestrator.start_polling(app)
output = capsys.readouterr().out output = capsys.readouterr().out
@@ -38,7 +38,7 @@ def test_simple_command_executes_successfully(monkeypatch: pytest.MonkeyPatch, c
app = App(override_system_messages=True, printer=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
orchestrator.run_repl(app) orchestrator.start_polling(app)
output = capsys.readouterr().out output = capsys.readouterr().out
@@ -62,7 +62,7 @@ def test_two_commands_execute_sequentially(monkeypatch: pytest.MonkeyPatch, caps
app = App(override_system_messages=True, printer=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
orchestrator.run_repl(app) orchestrator.start_polling(app)
output = capsys.readouterr().out output = capsys.readouterr().out
@@ -90,7 +90,7 @@ def test_three_commands_execute_sequentially(monkeypatch: pytest.MonkeyPatch, ca
app = App(override_system_messages=True, printer=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
orchestrator.run_repl(app) orchestrator.start_polling(app)
output = capsys.readouterr().out output = capsys.readouterr().out
@@ -118,7 +118,7 @@ def test_custom_flag_without_value_is_recognized(monkeypatch: pytest.MonkeyPatch
app = App(override_system_messages=True, printer=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
orchestrator.run_repl(app) orchestrator.start_polling(app)
output = capsys.readouterr().out output = capsys.readouterr().out
@@ -141,7 +141,7 @@ def test_custom_flag_with_regex_validation_accepts_valid_value(monkeypatch: pyte
app = App(override_system_messages=True, repeat_command_groups_printing=True, printer=print) app = App(override_system_messages=True, repeat_command_groups_printing=True, printer=print)
app.include_router(router) app.include_router(router)
orchestrator.run_repl(app) orchestrator.start_polling(app)
output = capsys.readouterr().out output = capsys.readouterr().out
@@ -169,7 +169,7 @@ def test_predefined_short_help_flag_is_recognized(monkeypatch: pytest.MonkeyPatc
app = App(override_system_messages=True, printer=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
orchestrator.run_repl(app) orchestrator.start_polling(app)
output = capsys.readouterr().out output = capsys.readouterr().out
@@ -192,7 +192,7 @@ def test_predefined_info_flag_is_recognized(monkeypatch: pytest.MonkeyPatch, cap
app = App(override_system_messages=True, printer=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
orchestrator.run_repl(app) orchestrator.start_polling(app)
output = capsys.readouterr().out output = capsys.readouterr().out
@@ -215,7 +215,7 @@ def test_predefined_host_flag_with_value_is_recognized(monkeypatch: pytest.Monke
app = App(override_system_messages=True, printer=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
orchestrator.run_repl(app) orchestrator.start_polling(app)
output = capsys.readouterr().out output = capsys.readouterr().out
@@ -244,7 +244,7 @@ def test_two_predefined_flags_are_recognized_together(monkeypatch: pytest.Monkey
app = App(override_system_messages=True, printer=print) app = App(override_system_messages=True, printer=print)
app.include_router(router) app.include_router(router)
orchestrator.run_repl(app) orchestrator.start_polling(app)
output = capsys.readouterr().out output = capsys.readouterr().out
+159 -119
View File
@@ -10,11 +10,14 @@ from prompt_toolkit.completion import CompleteEvent
from prompt_toolkit.document import Document from prompt_toolkit.document import Document
from prompt_toolkit.history import InMemoryHistory from prompt_toolkit.history import InMemoryHistory
from argenta.app.autocompleter._ext_features_impl import CommandLexer, HistoryCompleter from argenta.app.autocompleter.entity import (
from argenta.app.autocompleter.entity import AutoCompleter AutoCompleter,
CommandLexer,
HistoryCompleter
)
COMMANDS: set[str] = {"start", "stop", "status"} COMMANDS: set[str] = {"start", "stop", "status"}
_IMPL = "argenta.app.autocompleter._ext_features_impl"
def test_autocompleter_initializes_with_default_params() -> None: def test_autocompleter_initializes_with_default_params() -> None:
@@ -30,7 +33,7 @@ def test_autocompleter_initializes_with_custom_params() -> None:
history_filename="test.txt", history_filename="test.txt",
autocomplete_button="c-space", autocomplete_button="c-space",
command_highlighting=False, command_highlighting=False,
auto_suggestions=False, auto_suggestions=False
) )
assert completer.history_filename == "test.txt" assert completer.history_filename == "test.txt"
assert completer.autocomplete_button == "c-space" assert completer.autocomplete_button == "c-space"
@@ -76,13 +79,13 @@ def test_history_completer_returns_matching_commands() -> None:
history = InMemoryHistory() history = InMemoryHistory()
history.append_string("start server") history.append_string("start server")
history.append_string("stop server") history.append_string("stop server")
completer = HistoryCompleter(history, {"status"}) completer = HistoryCompleter(history, {"status"})
doc = Document("sta") doc = Document("sta")
completions = list(completer.get_completions(doc, CompleteEvent())) completions = list(completer.get_completions(doc, CompleteEvent()))
completion_texts = [c.text for c in completions] completion_texts = [c.text for c in completions]
assert "start server" in completion_texts assert "start server" in completion_texts
assert "status" in completion_texts assert "status" in completion_texts
assert "stop server" not in completion_texts assert "stop server" not in completion_texts
@@ -92,13 +95,13 @@ def test_history_completer_returns_all_when_empty_input() -> None:
history = InMemoryHistory() history = InMemoryHistory()
history.append_string("start") history.append_string("start")
history.append_string("stop") history.append_string("stop")
completer = HistoryCompleter(history, {"status"}) completer = HistoryCompleter(history, {"status"})
doc = Document("") doc = Document("")
completions = list(completer.get_completions(doc, CompleteEvent())) completions = list(completer.get_completions(doc, CompleteEvent()))
completion_texts = [c.text for c in completions] completion_texts = [c.text for c in completions]
assert len(completion_texts) == 3 assert len(completion_texts) == 3
assert "start" in completion_texts assert "start" in completion_texts
assert "stop" in completion_texts assert "stop" in completion_texts
@@ -108,10 +111,10 @@ def test_history_completer_returns_all_when_empty_input() -> None:
def test_history_completer_returns_empty_when_no_matches() -> None: def test_history_completer_returns_empty_when_no_matches() -> None:
history = InMemoryHistory() history = InMemoryHistory()
history.append_string("start") history.append_string("start")
completer = HistoryCompleter(history, {"stop"}) completer = HistoryCompleter(history, {"stop"})
doc = Document("xyz") doc = Document("xyz")
completions = list(completer.get_completions(doc, CompleteEvent())) completions = list(completer.get_completions(doc, CompleteEvent()))
assert len(completions) == 0 assert len(completions) == 0
@@ -120,10 +123,10 @@ def test_history_completer_deduplicates_commands() -> None:
history = InMemoryHistory() history = InMemoryHistory()
history.append_string("start") history.append_string("start")
history.append_string("start") history.append_string("start")
completer = HistoryCompleter(history, {"start"}) completer = HistoryCompleter(history, {"start"})
doc = Document("sta") doc = Document("sta")
completions = list(completer.get_completions(doc, CompleteEvent())) completions = list(completer.get_completions(doc, CompleteEvent()))
assert len(completions) == 1 assert len(completions) == 1
@@ -133,13 +136,13 @@ def test_history_completer_sorts_results() -> None:
history.append_string("stop") history.append_string("stop")
history.append_string("start") history.append_string("start")
history.append_string("status") history.append_string("status")
completer = HistoryCompleter(history, set()) completer = HistoryCompleter(history, set())
doc = Document("st") doc = Document("st")
completions = list(completer.get_completions(doc, CompleteEvent())) completions = list(completer.get_completions(doc, CompleteEvent()))
completion_texts = [c.text for c in completions] completion_texts = [c.text for c in completions]
assert completion_texts == ["start", "status", "stop"] assert completion_texts == ["start", "status", "stop"]
@@ -179,7 +182,7 @@ def test_history_completer_returns_early_when_no_matches() -> None:
history = InMemoryHistory() history = InMemoryHistory()
completer = HistoryCompleter(history, {"start", "stop"}) completer = HistoryCompleter(history, {"start", "stop"})
doc = Document("xyz") doc = Document("xyz")
result = completer.get_completions(doc, CompleteEvent()) result = completer.get_completions(doc, CompleteEvent())
completions = list(result) completions = list(result)
assert completions == [] assert completions == []
@@ -187,32 +190,28 @@ def test_history_completer_returns_early_when_no_matches() -> None:
def test_autocompleter_initial_setup_with_commands() -> None: def test_autocompleter_initial_setup_with_commands() -> None:
completer = AutoCompleter() completer = AutoCompleter()
with ( with patch.object(sys.stdin, 'isatty', return_value=True), \
patch.object(sys.stdin, "isatty", return_value=True), patch('argenta.app.autocompleter.entity.PromptSession') as mock_session:
patch(f"{_IMPL}.PromptSession") as mock_session,
):
completer.initial_setup({"start", "stop", "status"}) completer.initial_setup({"start", "stop", "status"})
assert completer._session is not None assert completer._session is not None
assert completer._fallback_mode is False assert completer._fallback_mode is False
mock_session.assert_called_once() mock_session.assert_called_once()
def test_autocompleter_initial_setup_with_history_file() -> None: def test_autocompleter_initial_setup_with_history_file() -> None:
with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".txt") as f: with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt') as f:
history_file = f.name history_file = f.name
try: try:
completer = AutoCompleter(history_filename=history_file) completer = AutoCompleter(history_filename=history_file)
with ( with patch.object(sys.stdin, 'isatty', return_value=True), \
patch.object(sys.stdin, "isatty", return_value=True), patch('argenta.app.autocompleter.entity.PromptSession'), \
patch(f"{_IMPL}.PromptSession"), patch('argenta.app.autocompleter.entity.ThreadedHistory') as mock_threaded_history:
patch(f"{_IMPL}.ThreadedHistory") as mock_threaded_history,
):
completer.initial_setup({"start", "stop"}) completer.initial_setup({"start", "stop"})
assert completer._session is not None assert completer._session is not None
assert completer._fallback_mode is False assert completer._fallback_mode is False
mock_threaded_history.assert_called_once() mock_threaded_history.assert_called_once()
@@ -223,14 +222,12 @@ def test_autocompleter_initial_setup_with_history_file() -> None:
def test_autocompleter_initial_setup_without_history_file() -> None: def test_autocompleter_initial_setup_without_history_file() -> None:
completer = AutoCompleter(history_filename=None) completer = AutoCompleter(history_filename=None)
with ( with patch.object(sys.stdin, 'isatty', return_value=True), \
patch.object(sys.stdin, "isatty", return_value=True), patch('argenta.app.autocompleter.entity.PromptSession'), \
patch(f"{_IMPL}.PromptSession"), patch('argenta.app.autocompleter.entity.InMemoryHistory') as mock_in_memory:
patch(f"{_IMPL}.InMemoryHistory") as mock_in_memory,
):
completer.initial_setup({"start", "stop"}) completer.initial_setup({"start", "stop"})
assert completer._session is not None assert completer._session is not None
assert completer._fallback_mode is False assert completer._fallback_mode is False
mock_in_memory.assert_called_once() mock_in_memory.assert_called_once()
@@ -238,90 +235,96 @@ def test_autocompleter_initial_setup_without_history_file() -> None:
def test_autocompleter_initial_setup_with_custom_autocomplete_button() -> None: def test_autocompleter_initial_setup_with_custom_autocomplete_button() -> None:
completer = AutoCompleter(autocomplete_button="c-space") completer = AutoCompleter(autocomplete_button="c-space")
with ( with patch.object(sys.stdin, 'isatty', return_value=True), \
patch.object(sys.stdin, "isatty", return_value=True), patch('argenta.app.autocompleter.entity.PromptSession'):
patch(f"{_IMPL}.PromptSession"),
):
completer.initial_setup({"start", "stop"}) completer.initial_setup({"start", "stop"})
assert completer._session is not None assert completer._session is not None
assert completer.autocomplete_button == "c-space" assert completer.autocomplete_button == "c-space"
def test_autocompleter_initial_setup_without_auto_suggestions() -> None: def test_autocompleter_initial_setup_without_auto_suggestions() -> None:
completer = AutoCompleter(auto_suggestions=False) completer = AutoCompleter(auto_suggestions=False)
with ( with patch.object(sys.stdin, 'isatty', return_value=True), \
patch.object(sys.stdin, "isatty", return_value=True), patch('argenta.app.autocompleter.entity.PromptSession') as mock_session:
patch(f"{_IMPL}.PromptSession") as mock_session,
):
completer.initial_setup({"start", "stop"}) completer.initial_setup({"start", "stop"})
assert completer._session is not None assert completer._session is not None
call_kwargs = mock_session.call_args[1] call_kwargs = mock_session.call_args[1]
assert call_kwargs["auto_suggest"] is None assert call_kwargs['auto_suggest'] is None
def test_autocompleter_initial_setup_without_command_highlighting() -> None: def test_autocompleter_initial_setup_without_command_highlighting() -> None:
completer = AutoCompleter(command_highlighting=False) completer = AutoCompleter(command_highlighting=False)
with ( with patch.object(sys.stdin, 'isatty', return_value=True), \
patch.object(sys.stdin, "isatty", return_value=True), patch('argenta.app.autocompleter.entity.PromptSession') as mock_session:
patch(f"{_IMPL}.PromptSession") as mock_session,
):
completer.initial_setup({"start", "stop"}) completer.initial_setup({"start", "stop"})
assert completer._session is not None assert completer._session is not None
call_kwargs = mock_session.call_args[1] call_kwargs = mock_session.call_args[1]
assert call_kwargs["style"] is None assert call_kwargs['style'] is None
assert call_kwargs["lexer"] is None assert call_kwargs['lexer'] is None
def _setup_captured_handler(completer: AutoCompleter) -> Callable[[Any], None] | None: def test_autocompleter_key_binding_handler_with_complete_state() -> None:
"""Вспомогательная функция: поднимает initial_setup и захватывает kb-хендлер.""" completer = AutoCompleter()
captured_handler: Callable[[Any], None] | None = None captured_handler: Callable[[Any], None] | None = None
def capture_kb_add(key: str) -> Callable[[Callable[[Any], None]], Callable[[Any], None]]: def capture_kb_add(key: str) -> Callable[[Callable[[Any], None]], Callable[[Any], None]]:
def decorator(func: Callable[[Any], None]) -> Callable[[Any], None]: def decorator(func: Callable[[Any], None]) -> Callable[[Any], None]:
nonlocal captured_handler nonlocal captured_handler
captured_handler = func captured_handler = func
return func return func
return decorator return decorator
with ( with patch.object(sys.stdin, 'isatty', return_value=True), \
patch.object(sys.stdin, "isatty", return_value=True), patch('argenta.app.autocompleter.entity.PromptSession'), \
patch(f"{_IMPL}.PromptSession"), patch('argenta.app.autocompleter.entity.KeyBindings') as mock_kb_class:
patch(f"{_IMPL}.KeyBindings") as mock_kb_class,
):
mock_kb = MagicMock() mock_kb = MagicMock()
mock_kb.add = capture_kb_add mock_kb.add = capture_kb_add
mock_kb_class.return_value = mock_kb mock_kb_class.return_value = mock_kb
completer.initial_setup({"start", "stop"}) completer.initial_setup({"start", "stop"})
return captured_handler
def test_autocompleter_key_binding_handler_with_complete_state() -> None:
completer = AutoCompleter()
captured_handler = _setup_captured_handler(completer)
assert captured_handler is not None assert captured_handler is not None
mock_event = MagicMock() mock_event = MagicMock()
mock_buff = MagicMock() mock_buff = MagicMock()
mock_buff.complete_state = True mock_buff.complete_state = True
mock_event.app.current_buffer = mock_buff mock_event.app.current_buffer = mock_buff
captured_handler(mock_event) captured_handler(mock_event)
mock_buff.complete_next.assert_called_once() mock_buff.complete_next.assert_called_once()
def test_autocompleter_key_binding_handler_no_completions() -> None: def test_autocompleter_key_binding_handler_no_completions() -> None:
completer = AutoCompleter() completer = AutoCompleter()
captured_handler = _setup_captured_handler(completer)
assert captured_handler is not None 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"})
mock_event = MagicMock() mock_event = MagicMock()
mock_buff = MagicMock() mock_buff = MagicMock()
mock_buff.complete_state = False mock_buff.complete_state = False
@@ -329,18 +332,36 @@ def test_autocompleter_key_binding_handler_no_completions() -> None:
mock_completer.get_completions.return_value = iter([]) mock_completer.get_completions.return_value = iter([])
mock_buff.completer = mock_completer mock_buff.completer = mock_completer
mock_event.app.current_buffer = mock_buff mock_event.app.current_buffer = mock_buff
assert captured_handler is not None
captured_handler(mock_event) captured_handler(mock_event)
mock_buff.start_completion.assert_not_called() mock_buff.start_completion.assert_not_called()
mock_buff.apply_completion.assert_not_called() mock_buff.apply_completion.assert_not_called()
def test_autocompleter_key_binding_handler_single_completion() -> None: def test_autocompleter_key_binding_handler_single_completion() -> None:
completer = AutoCompleter() completer = AutoCompleter()
captured_handler = _setup_captured_handler(completer)
assert captured_handler is not None 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"})
mock_event = MagicMock() mock_event = MagicMock()
mock_buff = MagicMock() mock_buff = MagicMock()
mock_buff.complete_state = False mock_buff.complete_state = False
@@ -349,18 +370,36 @@ def test_autocompleter_key_binding_handler_single_completion() -> None:
mock_completer.get_completions.return_value = iter([mock_completion]) mock_completer.get_completions.return_value = iter([mock_completion])
mock_buff.completer = mock_completer mock_buff.completer = mock_completer
mock_event.app.current_buffer = mock_buff mock_event.app.current_buffer = mock_buff
assert captured_handler is not None
captured_handler(mock_event) captured_handler(mock_event)
mock_buff.apply_completion.assert_called_once_with(mock_completion) mock_buff.apply_completion.assert_called_once_with(mock_completion)
mock_buff.start_completion.assert_not_called() mock_buff.start_completion.assert_not_called()
def test_autocompleter_key_binding_handler_multiple_completions() -> None: def test_autocompleter_key_binding_handler_multiple_completions() -> None:
completer = AutoCompleter() completer = AutoCompleter()
captured_handler = _setup_captured_handler(completer)
assert captured_handler is not None 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"})
mock_event = MagicMock() mock_event = MagicMock()
mock_buff = MagicMock() mock_buff = MagicMock()
mock_buff.complete_state = False mock_buff.complete_state = False
@@ -370,53 +409,54 @@ def test_autocompleter_key_binding_handler_multiple_completions() -> None:
mock_completer.get_completions.return_value = iter([mock_completion1, mock_completion2]) mock_completer.get_completions.return_value = iter([mock_completion1, mock_completion2])
mock_buff.completer = mock_completer mock_buff.completer = mock_completer
mock_event.app.current_buffer = mock_buff mock_event.app.current_buffer = mock_buff
assert captured_handler is not None
captured_handler(mock_event) captured_handler(mock_event)
mock_buff.start_completion.assert_called_once_with(select_first=False) mock_buff.start_completion.assert_called_once_with(select_first=False)
mock_buff.apply_completion.assert_not_called() mock_buff.apply_completion.assert_not_called()
def test_autocompleter_prompt_in_fallback_mode_with_string() -> None: def test_autocompleter_prompt_in_fallback_mode_with_string() -> None:
completer = AutoCompleter() completer = AutoCompleter()
with patch.object(sys.stdin, "isatty", return_value=False): with patch.object(sys.stdin, 'isatty', return_value=False):
completer.initial_setup({"start", "stop"}) completer.initial_setup({"start", "stop"})
assert completer._fallback_mode is True assert completer._fallback_mode is True
with patch("builtins.input", return_value="test input"): with patch('builtins.input', return_value='test input'):
result = completer.prompt(">>> ") result = completer.prompt(">>> ")
assert result == "test input" assert result == 'test input'
def test_autocompleter_prompt_in_fallback_mode_with_html() -> None: def test_autocompleter_prompt_in_fallback_mode_with_html() -> None:
completer = AutoCompleter() completer = AutoCompleter()
with patch.object(sys.stdin, "isatty", return_value=False): with patch.object(sys.stdin, 'isatty', return_value=False):
completer.initial_setup({"start", "stop"}) completer.initial_setup({"start", "stop"})
assert completer._fallback_mode is True assert completer._fallback_mode is True
with patch("builtins.input", return_value="test input"): with patch('builtins.input', return_value='test input'):
result = completer.prompt(HTML("<b>>>> </b>")) result = completer.prompt(HTML("<b>>>> </b>"))
assert result == "test input" assert result == 'test input'
def test_autocompleter_prompt_with_html_in_normal_mode() -> None: def test_autocompleter_prompt_with_html_in_normal_mode() -> None:
completer = AutoCompleter() completer = AutoCompleter()
mock_session = MagicMock() mock_session = MagicMock()
mock_session.prompt.return_value = "test result" mock_session.prompt.return_value = 'test result'
completer._session = mock_session completer._session = mock_session
completer._fallback_mode = False completer._fallback_mode = False
html_prompt = HTML("<b>>>> </b>") html_prompt = HTML("<b>>>> </b>")
result = completer.prompt(html_prompt) result = completer.prompt(html_prompt)
assert result == "test result" assert result == 'test result'
mock_session.prompt.assert_called_once() mock_session.prompt.assert_called_once()
call_args = mock_session.prompt.call_args call_args = mock_session.prompt.call_args
assert call_args[0][0] == html_prompt assert call_args[0][0] == html_prompt
@@ -424,15 +464,15 @@ def test_autocompleter_prompt_with_html_in_normal_mode() -> None:
def test_autocompleter_prompt_with_string_in_normal_mode() -> None: def test_autocompleter_prompt_with_string_in_normal_mode() -> None:
completer = AutoCompleter() completer = AutoCompleter()
mock_session = MagicMock() mock_session = MagicMock()
mock_session.prompt.return_value = "test result" mock_session.prompt.return_value = 'test result'
completer._session = mock_session completer._session = mock_session
completer._fallback_mode = False completer._fallback_mode = False
result = completer.prompt(">>> ") result = completer.prompt(">>> ")
assert result == "test result" assert result == 'test result'
mock_session.prompt.assert_called_once() mock_session.prompt.assert_called_once()
call_args = mock_session.prompt.call_args call_args = mock_session.prompt.call_args
assert isinstance(call_args[0][0], HTML) assert isinstance(call_args[0][0], HTML)
+36 -35
View File
@@ -45,11 +45,12 @@ def sample_router() -> Router:
# ============================================================================ # ============================================================================
def test_orchestrator_initializes_with_no_argparser(mocker: MockerFixture) -> None: def test_orchestrator_initializes_with_default_argparser(mocker: MockerFixture) -> None:
"""Test Orchestrator initialization with no ArgParser""" """Test Orchestrator initialization with default ArgParser"""
mocker.patch('sys.argv', ['test_program']) mocker.patch('sys.argv', ['test_program'])
orchestrator = Orchestrator() orchestrator = Orchestrator()
assert orchestrator._arg_parser is None assert orchestrator._arg_parser is not None
assert isinstance(orchestrator._arg_parser, ArgParser)
def test_orchestrator_initializes_with_custom_argparser(mock_argparser: ArgParser) -> None: def test_orchestrator_initializes_with_custom_argparser(mock_argparser: ArgParser) -> None:
@@ -88,80 +89,80 @@ def test_orchestrator_parses_args_on_initialization(mocker: MockerFixture, mock_
# ============================================================================ # ============================================================================
# Tests for run_repl method # Tests for start_polling method
# ============================================================================ # ============================================================================
def test_run_repl_creates_dishka_container( def test_start_polling_creates_dishka_container(
mocker: MockerFixture, mock_argparser: ArgParser, sample_app: App mocker: MockerFixture, mock_argparser: ArgParser, sample_app: App
) -> None: ) -> None:
"""Test that run_repl creates a dishka container""" """Test that start_polling creates a dishka container"""
mock_make_container = mocker.patch('argenta.orchestrator.entity.make_container') mock_make_container = mocker.patch('argenta.orchestrator.entity.make_container')
_mock_setup_dishka = mocker.patch('argenta.orchestrator.entity.setup_dishka') _mock_setup_dishka = mocker.patch('argenta.orchestrator.entity.setup_dishka')
mocker.patch.object(sample_app, '_run_repl') mocker.patch.object(sample_app, '_run_polling')
orchestrator = Orchestrator(arg_parser=mock_argparser) orchestrator = Orchestrator(arg_parser=mock_argparser)
orchestrator.run_repl(sample_app) orchestrator.start_polling(sample_app)
mock_make_container.assert_called_once() mock_make_container.assert_called_once()
assert mock_make_container.call_args[1]['context'] == {ArgParser: mock_argparser} assert mock_make_container.call_args[1]['context'] == {ArgParser: mock_argparser}
def test_run_repl_calls_setup_dishka_with_auto_inject_enabled( def test_start_polling_calls_setup_dishka_with_auto_inject_enabled(
mocker: MockerFixture, mock_argparser: ArgParser, sample_app: App mocker: MockerFixture, mock_argparser: ArgParser, sample_app: App
) -> None: ) -> None:
"""Test that run_repl calls setup_dishka with auto_inject=True""" """Test that start_polling calls setup_dishka with auto_inject=True"""
mock_container = mocker.MagicMock() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType] mock_container = mocker.MagicMock() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
mocker.patch('argenta.orchestrator.entity.make_container', return_value=mock_container) mocker.patch('argenta.orchestrator.entity.make_container', return_value=mock_container)
mock_setup_dishka = mocker.patch('argenta.orchestrator.entity.setup_dishka') mock_setup_dishka = mocker.patch('argenta.orchestrator.entity.setup_dishka')
mocker.patch.object(sample_app, '_run_repl') mocker.patch.object(sample_app, '_run_polling')
orchestrator = Orchestrator(arg_parser=mock_argparser, auto_inject_handlers=True) orchestrator = Orchestrator(arg_parser=mock_argparser, auto_inject_handlers=True)
orchestrator.run_repl(sample_app) orchestrator.start_polling(sample_app)
mock_setup_dishka.assert_called_once_with(sample_app, mock_container, auto_inject=True) mock_setup_dishka.assert_called_once_with(sample_app, mock_container, auto_inject=True)
def test_run_repl_calls_setup_dishka_with_auto_inject_disabled( def test_start_polling_calls_setup_dishka_with_auto_inject_disabled(
mocker: MockerFixture, mock_argparser: ArgParser, sample_app: App mocker: MockerFixture, mock_argparser: ArgParser, sample_app: App
) -> None: ) -> None:
"""Test that run_repl calls setup_dishka with auto_inject=False""" """Test that start_polling calls setup_dishka with auto_inject=False"""
mock_container = mocker.MagicMock() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType] mock_container = mocker.MagicMock() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
mocker.patch('argenta.orchestrator.entity.make_container', return_value=mock_container) mocker.patch('argenta.orchestrator.entity.make_container', return_value=mock_container)
mock_setup_dishka = mocker.patch('argenta.orchestrator.entity.setup_dishka') mock_setup_dishka = mocker.patch('argenta.orchestrator.entity.setup_dishka')
mocker.patch.object(sample_app, '_run_repl') mocker.patch.object(sample_app, '_run_polling')
orchestrator = Orchestrator(arg_parser=mock_argparser, auto_inject_handlers=False) orchestrator = Orchestrator(arg_parser=mock_argparser, auto_inject_handlers=False)
orchestrator.run_repl(sample_app) orchestrator.start_polling(sample_app)
mock_setup_dishka.assert_called_once_with(sample_app, mock_container, auto_inject=False) mock_setup_dishka.assert_called_once_with(sample_app, mock_container, auto_inject=False)
def test_run_repl_calls_app_run_repl( def test_start_polling_calls_app_run_polling(
mocker: MockerFixture, mock_argparser: ArgParser, sample_app: App mocker: MockerFixture, mock_argparser: ArgParser, sample_app: App
) -> None: ) -> None:
"""Test that run_repl calls app.run_polling()""" """Test that start_polling calls app.run_polling()"""
mocker.patch('argenta.orchestrator.entity.make_container') mocker.patch('argenta.orchestrator.entity.make_container')
mocker.patch('argenta.orchestrator.entity.setup_dishka') mocker.patch('argenta.orchestrator.entity.setup_dishka')
mock_run_repl = mocker.patch.object(sample_app, '_run_repl') mock_run_polling = mocker.patch.object(sample_app, '_run_polling')
orchestrator = Orchestrator(arg_parser=mock_argparser) orchestrator = Orchestrator(arg_parser=mock_argparser)
orchestrator.run_repl(sample_app) orchestrator.start_polling(sample_app)
mock_run_repl.assert_called_once() mock_run_polling.assert_called_once()
def test_run_repl_includes_custom_providers_in_container( def test_start_polling_includes_custom_providers_in_container(
mocker: MockerFixture, mock_argparser: ArgParser, sample_app: App mocker: MockerFixture, mock_argparser: ArgParser, sample_app: App
) -> None: ) -> None:
"""Test that run_repl includes custom providers in container""" """Test that start_polling includes custom providers in container"""
custom_provider = Provider() custom_provider = Provider()
mock_make_container = mocker.patch('argenta.orchestrator.entity.make_container') mock_make_container = mocker.patch('argenta.orchestrator.entity.make_container')
mocker.patch('argenta.orchestrator.entity.setup_dishka') mocker.patch('argenta.orchestrator.entity.setup_dishka')
mocker.patch.object(sample_app, '_run_repl') mocker.patch.object(sample_app, '_run_polling')
orchestrator = Orchestrator(arg_parser=mock_argparser, custom_providers=[custom_provider]) orchestrator = Orchestrator(arg_parser=mock_argparser, custom_providers=[custom_provider])
orchestrator.run_repl(sample_app) orchestrator.start_polling(sample_app)
# Check that custom_provider was passed to make_container # Check that custom_provider was passed to make_container
call_args = mock_make_container.call_args[0] call_args = mock_make_container.call_args[0]
@@ -179,14 +180,14 @@ def test_orchestrator_integrates_with_app_with_router(
"""Test that Orchestrator properly integrates with App that has routers""" """Test that Orchestrator properly integrates with App that has routers"""
mocker.patch('argenta.orchestrator.entity.make_container') mocker.patch('argenta.orchestrator.entity.make_container')
mocker.patch('argenta.orchestrator.entity.setup_dishka') mocker.patch('argenta.orchestrator.entity.setup_dishka')
mock_run_repl = mocker.patch.object(sample_app, '_run_repl') mock_run_polling = mocker.patch.object(sample_app, '_run_polling')
sample_app.include_router(sample_router) sample_app.include_router(sample_router)
orchestrator = Orchestrator(arg_parser=mock_argparser) orchestrator = Orchestrator(arg_parser=mock_argparser)
orchestrator.run_repl(sample_app) orchestrator.start_polling(sample_app)
mock_run_repl.assert_called_once() mock_run_polling.assert_called_once()
assert len(sample_app.registered_routers.registered_routers) == 1 assert len(sample_app.registered_routers.registered_routers) == 1
@@ -201,10 +202,10 @@ def test_orchestrator_passes_argparser_to_container_context(
"""Test that Orchestrator passes ArgParser instance to container context""" """Test that Orchestrator passes ArgParser instance to container context"""
mock_make_container = mocker.patch('argenta.orchestrator.entity.make_container') mock_make_container = mocker.patch('argenta.orchestrator.entity.make_container')
mocker.patch('argenta.orchestrator.entity.setup_dishka') mocker.patch('argenta.orchestrator.entity.setup_dishka')
mocker.patch.object(sample_app, '_run_repl') mocker.patch.object(sample_app, '_run_polling')
orchestrator = Orchestrator(arg_parser=mock_argparser) orchestrator = Orchestrator(arg_parser=mock_argparser)
orchestrator.run_repl(sample_app) orchestrator.start_polling(sample_app)
# Verify that ArgParser was passed in context # Verify that ArgParser was passed in context
call_kwargs = mock_make_container.call_args[1] call_kwargs = mock_make_container.call_args[1]
@@ -218,18 +219,18 @@ def test_orchestrator_passes_argparser_to_container_context(
# ============================================================================ # ============================================================================
def test_orchestrator_handles_app_run_repl_exception( def test_orchestrator_handles_app_run_polling_exception(
mocker: MockerFixture, mock_argparser: ArgParser, sample_app: App mocker: MockerFixture, mock_argparser: ArgParser, sample_app: App
) -> None: ) -> None:
"""Test that Orchestrator propagates exceptions from app.run_polling()""" """Test that Orchestrator propagates exceptions from app.run_polling()"""
mocker.patch('argenta.orchestrator.entity.make_container') mocker.patch('argenta.orchestrator.entity.make_container')
mocker.patch('argenta.orchestrator.entity.setup_dishka') mocker.patch('argenta.orchestrator.entity.setup_dishka')
mocker.patch.object(sample_app, '_run_repl', side_effect=RuntimeError("Test error")) mocker.patch.object(sample_app, '_run_polling', side_effect=RuntimeError("Test error"))
orchestrator = Orchestrator(arg_parser=mock_argparser) orchestrator = Orchestrator(arg_parser=mock_argparser)
with pytest.raises(RuntimeError, match="Test error"): with pytest.raises(RuntimeError, match="Test error"):
orchestrator.run_repl(sample_app) orchestrator.start_polling(sample_app)
# ============================================================================ # ============================================================================
@@ -245,13 +246,13 @@ def test_orchestrator_accepts_multiple_custom_providers(
provider2 = Provider() provider2 = Provider()
mock_make_container = mocker.patch('argenta.orchestrator.entity.make_container') mock_make_container = mocker.patch('argenta.orchestrator.entity.make_container')
mocker.patch('argenta.orchestrator.entity.setup_dishka') mocker.patch('argenta.orchestrator.entity.setup_dishka')
mocker.patch.object(sample_app, '_run_repl') mocker.patch.object(sample_app, '_run_polling')
orchestrator = Orchestrator( orchestrator = Orchestrator(
arg_parser=mock_argparser, arg_parser=mock_argparser,
custom_providers=[provider1, provider2] custom_providers=[provider1, provider2]
) )
orchestrator.run_repl(sample_app) orchestrator.start_polling(sample_app)
call_args = mock_make_container.call_args[0] call_args = mock_make_container.call_args[0]
assert provider1 in call_args assert provider1 in call_args
Generated
+64 -349
View File
@@ -1,6 +1,6 @@
version = 1 version = 1
revision = 3 revision = 3
requires-python = ">=3.12, <3.15" requires-python = ">=3.12"
[[package]] [[package]]
name = "aiosqlite" name = "aiosqlite"
@@ -48,23 +48,12 @@ dependencies = [
{ name = "rich" }, { name = "rich" },
] ]
[package.optional-dependencies]
cli = [
{ name = "nuitka", extra = ["onefile"] },
{ name = "typer" },
]
[package.dev-dependencies] [package.dev-dependencies]
dev = [ dev = [
{ name = "cairosvg" },
{ name = "esbonio" }, { name = "esbonio" },
{ name = "isort" }, { name = "isort" },
{ name = "matplotlib" },
{ name = "mypy" }, { name = "mypy" },
{ name = "psutil" },
{ name = "py-cpuinfo" },
{ name = "pyfakefs" }, { name = "pyfakefs" },
{ name = "pygal" },
{ name = "pytest" }, { name = "pytest" },
{ name = "pytest-cov" }, { name = "pytest-cov" },
{ name = "pytest-mock" }, { name = "pytest-mock" },
@@ -89,11 +78,9 @@ linters = [
{ name = "wemake-python-styleguide" }, { name = "wemake-python-styleguide" },
] ]
metrics = [ metrics = [
{ name = "cairosvg" },
{ name = "matplotlib" }, { name = "matplotlib" },
{ name = "psutil" }, { name = "psutil" },
{ name = "py-cpuinfo" }, { name = "py-cpuinfo" },
{ name = "pygal" },
] ]
tests = [ tests = [
{ name = "pyfakefs" }, { name = "pyfakefs" },
@@ -109,24 +96,16 @@ typecheckers = [
requires-dist = [ requires-dist = [
{ name = "art", specifier = ">=6.4,<7.0" }, { name = "art", specifier = ">=6.4,<7.0" },
{ name = "dishka", specifier = ">=1.7.2" }, { name = "dishka", specifier = ">=1.7.2" },
{ name = "nuitka", extras = ["onefile"], marker = "extra == 'cli'", specifier = ">=4.0.5" },
{ name = "prompt-toolkit", specifier = ">=3.0.52" }, { name = "prompt-toolkit", specifier = ">=3.0.52" },
{ name = "rich", specifier = ">=14.0.0,<15.0.0" }, { name = "rich", specifier = ">=14.0.0,<15.0.0" },
{ name = "typer", marker = "extra == 'cli'", specifier = ">=0.9,!=0.12,<=0.21.1" },
] ]
provides-extras = ["cli"]
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [ dev = [
{ name = "cairosvg", specifier = ">=2.8.2" },
{ name = "esbonio", specifier = ">=1.0.0" }, { name = "esbonio", specifier = ">=1.0.0" },
{ name = "isort", specifier = ">=7.0.0" }, { name = "isort", specifier = ">=7.0.0" },
{ name = "matplotlib", specifier = ">=3.10.8" },
{ name = "mypy", specifier = ">=1.14.1" }, { name = "mypy", specifier = ">=1.14.1" },
{ name = "psutil", specifier = ">=7.2.1" },
{ name = "py-cpuinfo", specifier = ">=9.0.0" },
{ name = "pyfakefs", specifier = ">=5.5.0" }, { name = "pyfakefs", specifier = ">=5.5.0" },
{ name = "pygal", specifier = ">=3.1.0" },
{ name = "pytest", specifier = ">=8.3.2" }, { name = "pytest", specifier = ">=8.3.2" },
{ name = "pytest-cov", specifier = ">=7.0.0" }, { name = "pytest-cov", specifier = ">=7.0.0" },
{ name = "pytest-mock", specifier = ">=3.15.1" }, { name = "pytest-mock", specifier = ">=3.15.1" },
@@ -151,11 +130,9 @@ linters = [
{ name = "wemake-python-styleguide", specifier = ">=0.17.0" }, { name = "wemake-python-styleguide", specifier = ">=0.17.0" },
] ]
metrics = [ metrics = [
{ name = "cairosvg", specifier = ">=2.8.2" },
{ name = "matplotlib", specifier = ">=3.10.8" }, { name = "matplotlib", specifier = ">=3.10.8" },
{ name = "psutil", specifier = ">=7.2.1" }, { name = "psutil", specifier = ">=7.2.1" },
{ name = "py-cpuinfo", specifier = ">=9.0.0" }, { name = "py-cpuinfo", specifier = ">=9.0.0" },
{ name = "pygal", specifier = ">=3.1.0" },
] ]
tests = [ tests = [
{ name = "pyfakefs", specifier = ">=5.5.0" }, { name = "pyfakefs", specifier = ">=5.5.0" },
@@ -192,34 +169,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" },
] ]
[[package]]
name = "cairocffi"
version = "1.7.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi" },
]
sdist = { url = "https://files.pythonhosted.org/packages/70/c5/1a4dc131459e68a173cbdab5fad6b524f53f9c1ef7861b7698e998b837cc/cairocffi-1.7.1.tar.gz", hash = "sha256:2e48ee864884ec4a3a34bfa8c9ab9999f688286eb714a15a43ec9d068c36557b", size = 88096, upload-time = "2024-06-18T10:56:06.741Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/93/d8/ba13451aa6b745c49536e87b6bf8f629b950e84bd0e8308f7dc6883b67e2/cairocffi-1.7.1-py3-none-any.whl", hash = "sha256:9803a0e11f6c962f3b0ae2ec8ba6ae45e957a146a004697a1ac1bbf16b073b3f", size = 75611, upload-time = "2024-06-18T10:55:59.489Z" },
]
[[package]]
name = "cairosvg"
version = "2.8.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cairocffi" },
{ name = "cssselect2" },
{ name = "defusedxml" },
{ name = "pillow" },
{ name = "tinycss2" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ab/b9/5106168bd43d7cd8b7cc2a2ee465b385f14b63f4c092bb89eee2d48c8e67/cairosvg-2.8.2.tar.gz", hash = "sha256:07cbf4e86317b27a92318a4cac2a4bb37a5e9c1b8a27355d06874b22f85bef9f", size = 8398590, upload-time = "2025-05-15T06:56:32.653Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/67/48/816bd4aaae93dbf9e408c58598bc32f4a8c65f4b86ab560864cb3ee60adb/cairosvg-2.8.2-py3-none-any.whl", hash = "sha256:eab46dad4674f33267a671dce39b64be245911c901c70d65d2b7b0821e852bf5", size = 45773, upload-time = "2025-05-15T06:56:28.552Z" },
]
[[package]] [[package]]
name = "cattrs" name = "cattrs"
version = "25.2.0" version = "25.2.0"
@@ -242,63 +191,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" }, { url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" },
] ]
[[package]]
name = "cffi"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
{ url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
{ url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
{ url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
{ url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
{ url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
{ url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
{ url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
{ url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
{ url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
{ url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
{ url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
{ url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
{ url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
{ url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
{ url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
{ url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
{ url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
{ url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
{ url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
{ url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
{ url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
{ url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
{ url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
{ url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
{ url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
{ url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
{ url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
{ url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
{ url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
{ url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
{ url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
{ url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
{ url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
{ url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
{ url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
{ url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
{ url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
{ url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
{ url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
{ url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
{ url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
{ url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
{ url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
{ url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
]
[[package]] [[package]]
name = "charset-normalizer" name = "charset-normalizer"
version = "3.4.4" version = "3.4.4"
@@ -529,19 +421,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5f/04/642c1d8a448ae5ea1369eac8495740a79eb4e581a9fb0cbdce56bbf56da1/coverage-7.11.0-py3-none-any.whl", hash = "sha256:4b7589765348d78fb4e5fb6ea35d07564e387da2fc5efff62e0222971f155f68", size = 207761, upload-time = "2025-10-15T15:15:06.439Z" }, { url = "https://files.pythonhosted.org/packages/5f/04/642c1d8a448ae5ea1369eac8495740a79eb4e581a9fb0cbdce56bbf56da1/coverage-7.11.0-py3-none-any.whl", hash = "sha256:4b7589765348d78fb4e5fb6ea35d07564e387da2fc5efff62e0222971f155f68", size = 207761, upload-time = "2025-10-15T15:15:06.439Z" },
] ]
[[package]]
name = "cssselect2"
version = "0.9.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "tinycss2" },
{ name = "webencodings" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e0/20/92eaa6b0aec7189fa4b75c890640e076e9e793095721db69c5c81142c2e1/cssselect2-0.9.0.tar.gz", hash = "sha256:759aa22c216326356f65e62e791d66160a0f9c91d1424e8d8adc5e74dddfc6fb", size = 35595, upload-time = "2026-02-12T17:16:39.614Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/21/0e/8459ca4413e1a21a06c97d134bfaf18adfd27cea068813dc0faae06cbf00/cssselect2-0.9.0-py3-none-any.whl", hash = "sha256:6a99e5f91f9a016a304dd929b0966ca464bcfda15177b6fb4a118fc0fb5d9563", size = 15453, upload-time = "2026-02-12T17:16:38.317Z" },
]
[[package]] [[package]]
name = "cycler" name = "cycler"
version = "0.12.1" version = "0.12.1"
@@ -551,15 +430,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" },
] ]
[[package]]
name = "defusedxml"
version = "0.7.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" },
]
[[package]] [[package]]
name = "dishka" name = "dishka"
version = "1.7.2" version = "1.7.2"
@@ -676,18 +546,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" }, { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" },
] ]
[[package]]
name = "importlib-metadata"
version = "8.7.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "zipp" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" },
]
[[package]] [[package]]
name = "iniconfig" name = "iniconfig"
version = "2.1.0" version = "2.1.0"
@@ -991,17 +849,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
] ]
[[package]]
name = "nuitka"
version = "4.0.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f8/40/f73e578922084f9e465b30abdb9963aadcc087b5a9399033472d9ef641ab/nuitka-4.0.5.tar.gz", hash = "sha256:45e7d90266e76fe64eeb8d196c17666d7cd7cffbf68d6a24f233c3c03b6feaa8", size = 4420878, upload-time = "2026-03-12T10:03:39.562Z" }
[package.optional-dependencies]
onefile = [
{ name = "zstandard" },
]
[[package]] [[package]]
name = "numpy" name = "numpy"
version = "2.4.1" version = "2.4.1"
@@ -1083,71 +930,71 @@ wheels = [
[[package]] [[package]]
name = "pillow" name = "pillow"
version = "12.1.1" version = "12.2.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
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" } 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" }
wheels = [ wheels = [
{ 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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" }, { 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" },
] ]
[[package]] [[package]]
@@ -1226,15 +1073,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" }, { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" },
] ]
[[package]]
name = "pycparser"
version = "3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
]
[[package]] [[package]]
name = "pyfakefs" name = "pyfakefs"
version = "5.10.0" version = "5.10.0"
@@ -1253,18 +1091,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" }, { url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" },
] ]
[[package]]
name = "pygal"
version = "3.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "importlib-metadata" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b0/b6/04176faeb312c84d7f9bc1a810f96ee38d15597e226bb9bda59f3a5cb122/pygal-3.1.0.tar.gz", hash = "sha256:fbdee7351a7423e7907fb8a9c3b77305f6b5678cb2e6fd0db36a8825e42955ec", size = 81006, upload-time = "2025-12-09T10:29:19.587Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/4c/2862dd25352fe4b22ec7760d4fa12cb692587cd7ec3e378cbf644fc0d2a8/pygal-3.1.0-py3-none-any.whl", hash = "sha256:4e923490f3490c90c481f4535fa3adcda20ff374257ab9d8ae897f91b632c0bb", size = 130171, upload-time = "2025-12-09T10:29:16.721Z" },
]
[[package]] [[package]]
name = "pygls" name = "pygls"
version = "2.0.0" version = "2.0.0"
@@ -1443,15 +1269,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/02/e7/062480ede84ecb56ee0f8f2e5b5a3b2a5bceeb73bbdf909d3c13f5438749/scriv-1.8.0-py3-none-any.whl", hash = "sha256:f00f51325b2f4bc96b16fbb1239d4ab577cc2422301a5dd4f5f9378aae2549e0", size = 39085, upload-time = "2025-12-30T00:01:08.599Z" }, { url = "https://files.pythonhosted.org/packages/02/e7/062480ede84ecb56ee0f8f2e5b5a3b2a5bceeb73bbdf909d3c13f5438749/scriv-1.8.0-py3-none-any.whl", hash = "sha256:f00f51325b2f4bc96b16fbb1239d4ab577cc2422301a5dd4f5f9378aae2549e0", size = 39085, upload-time = "2025-12-30T00:01:08.599Z" },
] ]
[[package]]
name = "shellingham"
version = "1.5.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
]
[[package]] [[package]]
name = "shibuya" name = "shibuya"
version = "2025.9.25" version = "2025.9.25"
@@ -1618,33 +1435,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/be/72/2db2f49247d0a18b4f1bb9a5a39a0162869acf235f3a96418363947b3d46/starlette-0.48.0-py3-none-any.whl", hash = "sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659", size = 73736, upload-time = "2025-09-13T08:41:03.869Z" }, { url = "https://files.pythonhosted.org/packages/be/72/2db2f49247d0a18b4f1bb9a5a39a0162869acf235f3a96418363947b3d46/starlette-0.48.0-py3-none-any.whl", hash = "sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659", size = 73736, upload-time = "2025-09-13T08:41:03.869Z" },
] ]
[[package]]
name = "tinycss2"
version = "1.5.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "webencodings" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a3/ae/2ca4913e5c0f09781d75482874c3a95db9105462a92ddd303c7d285d3df2/tinycss2-1.5.1.tar.gz", hash = "sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957", size = 88195, upload-time = "2025-11-23T10:29:10.082Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl", hash = "sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661", size = 28404, upload-time = "2025-11-23T10:29:08.676Z" },
]
[[package]]
name = "typer"
version = "0.21.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "rich" },
{ name = "shellingham" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/36/bf/8825b5929afd84d0dabd606c67cd57b8388cb3ec385f7ef19c5cc2202069/typer-0.21.1.tar.gz", hash = "sha256:ea835607cd752343b6b2b7ce676893e5a0324082268b48f27aa058bdb7d2145d", size = 110371, upload-time = "2026-01-06T11:21:10.989Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-py3-none-any.whl", hash = "sha256:7985e89081c636b88d172c2ee0cfe33c253160994d47bdfdc302defd7d1f1d01", size = 47381, upload-time = "2026-01-06T11:21:09.824Z" },
]
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.13.2" version = "4.13.2"
@@ -1755,15 +1545,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/18/0e/a5f0257ab47492b7afb5fb60347d14ba19445e2773fc8352d4be6bd2f6f8/wcwidth-0.3.0-py3-none-any.whl", hash = "sha256:073a1acb250e4add96cfd5ef84e0036605cd6e0d0782c8c15c80e42202348458", size = 85520, upload-time = "2026-01-21T17:44:08.002Z" }, { url = "https://files.pythonhosted.org/packages/18/0e/a5f0257ab47492b7afb5fb60347d14ba19445e2773fc8352d4be6bd2f6f8/wcwidth-0.3.0-py3-none-any.whl", hash = "sha256:073a1acb250e4add96cfd5ef84e0036605cd6e0d0782c8c15c80e42202348458", size = 85520, upload-time = "2026-01-21T17:44:08.002Z" },
] ]
[[package]]
name = "webencodings"
version = "0.5.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" },
]
[[package]] [[package]]
name = "websockets" name = "websockets"
version = "15.0.1" version = "15.0.1"
@@ -1808,69 +1589,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/b8/08/c0776aa654dc43cb3
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/9b/58/98c4aa00e3de8e45726029799d8facbdcd75347b2f48b285857577e8efd8/wemake_python_styleguide-1.4.0-py3-none-any.whl", hash = "sha256:c0727475a20a1b7d59f1d806040e84768bdb0935d1147023453aa44c14b65c95", size = 215985, upload-time = "2025-08-25T10:15:06.713Z" }, { url = "https://files.pythonhosted.org/packages/9b/58/98c4aa00e3de8e45726029799d8facbdcd75347b2f48b285857577e8efd8/wemake_python_styleguide-1.4.0-py3-none-any.whl", hash = "sha256:c0727475a20a1b7d59f1d806040e84768bdb0935d1147023453aa44c14b65c95", size = 215985, upload-time = "2025-08-25T10:15:06.713Z" },
] ]
[[package]]
name = "zipp"
version = "3.23.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" },
]
[[package]]
name = "zstandard"
version = "0.25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738, upload-time = "2025-09-14T22:16:56.237Z" },
{ url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436, upload-time = "2025-09-14T22:16:57.774Z" },
{ url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019, upload-time = "2025-09-14T22:16:59.302Z" },
{ url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012, upload-time = "2025-09-14T22:17:01.156Z" },
{ url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148, upload-time = "2025-09-14T22:17:03.091Z" },
{ url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652, upload-time = "2025-09-14T22:17:04.979Z" },
{ url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993, upload-time = "2025-09-14T22:17:06.781Z" },
{ url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806, upload-time = "2025-09-14T22:17:08.415Z" },
{ url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659, upload-time = "2025-09-14T22:17:10.164Z" },
{ url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933, upload-time = "2025-09-14T22:17:11.857Z" },
{ url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008, upload-time = "2025-09-14T22:17:13.627Z" },
{ url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517, upload-time = "2025-09-14T22:17:16.103Z" },
{ url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292, upload-time = "2025-09-14T22:17:17.827Z" },
{ url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237, upload-time = "2025-09-14T22:17:19.954Z" },
{ url = "https://files.pythonhosted.org/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", size = 436922, upload-time = "2025-09-14T22:17:24.398Z" },
{ url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276, upload-time = "2025-09-14T22:17:21.429Z" },
{ url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679, upload-time = "2025-09-14T22:17:23.147Z" },
{ url = "https://files.pythonhosted.org/packages/35/0b/8df9c4ad06af91d39e94fa96cc010a24ac4ef1378d3efab9223cc8593d40/zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", size = 795735, upload-time = "2025-09-14T22:17:26.042Z" },
{ url = "https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", size = 640440, upload-time = "2025-09-14T22:17:27.366Z" },
{ url = "https://files.pythonhosted.org/packages/d9/14/933d27204c2bd404229c69f445862454dcc101cd69ef8c6068f15aaec12c/zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", size = 5343070, upload-time = "2025-09-14T22:17:28.896Z" },
{ url = "https://files.pythonhosted.org/packages/6d/db/ddb11011826ed7db9d0e485d13df79b58586bfdec56e5c84a928a9a78c1c/zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", size = 5063001, upload-time = "2025-09-14T22:17:31.044Z" },
{ url = "https://files.pythonhosted.org/packages/db/00/87466ea3f99599d02a5238498b87bf84a6348290c19571051839ca943777/zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", size = 5394120, upload-time = "2025-09-14T22:17:32.711Z" },
{ url = "https://files.pythonhosted.org/packages/2b/95/fc5531d9c618a679a20ff6c29e2b3ef1d1f4ad66c5e161ae6ff847d102a9/zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", size = 5451230, upload-time = "2025-09-14T22:17:34.41Z" },
{ url = "https://files.pythonhosted.org/packages/63/4b/e3678b4e776db00f9f7b2fe58e547e8928ef32727d7a1ff01dea010f3f13/zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", size = 5547173, upload-time = "2025-09-14T22:17:36.084Z" },
{ url = "https://files.pythonhosted.org/packages/4e/d5/ba05ed95c6b8ec30bd468dfeab20589f2cf709b5c940483e31d991f2ca58/zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", size = 5046736, upload-time = "2025-09-14T22:17:37.891Z" },
{ url = "https://files.pythonhosted.org/packages/50/d5/870aa06b3a76c73eced65c044b92286a3c4e00554005ff51962deef28e28/zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", size = 5576368, upload-time = "2025-09-14T22:17:40.206Z" },
{ url = "https://files.pythonhosted.org/packages/5d/35/398dc2ffc89d304d59bc12f0fdd931b4ce455bddf7038a0a67733a25f550/zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", size = 4954022, upload-time = "2025-09-14T22:17:41.879Z" },
{ url = "https://files.pythonhosted.org/packages/9a/5c/36ba1e5507d56d2213202ec2b05e8541734af5f2ce378c5d1ceaf4d88dc4/zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", size = 5267889, upload-time = "2025-09-14T22:17:43.577Z" },
{ url = "https://files.pythonhosted.org/packages/70/e8/2ec6b6fb7358b2ec0113ae202647ca7c0e9d15b61c005ae5225ad0995df5/zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", size = 5433952, upload-time = "2025-09-14T22:17:45.271Z" },
{ url = "https://files.pythonhosted.org/packages/7b/01/b5f4d4dbc59ef193e870495c6f1275f5b2928e01ff5a81fecb22a06e22fb/zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", size = 5814054, upload-time = "2025-09-14T22:17:47.08Z" },
{ url = "https://files.pythonhosted.org/packages/b2/e5/fbd822d5c6f427cf158316d012c5a12f233473c2f9c5fe5ab1ae5d21f3d8/zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", size = 5360113, upload-time = "2025-09-14T22:17:48.893Z" },
{ url = "https://files.pythonhosted.org/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", size = 436936, upload-time = "2025-09-14T22:17:52.658Z" },
{ url = "https://files.pythonhosted.org/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", size = 506232, upload-time = "2025-09-14T22:17:50.402Z" },
{ url = "https://files.pythonhosted.org/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", size = 462671, upload-time = "2025-09-14T22:17:51.533Z" },
{ url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887, upload-time = "2025-09-14T22:17:54.198Z" },
{ url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658, upload-time = "2025-09-14T22:17:55.423Z" },
{ url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849, upload-time = "2025-09-14T22:17:57.372Z" },
{ url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095, upload-time = "2025-09-14T22:17:59.498Z" },
{ url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751, upload-time = "2025-09-14T22:18:01.618Z" },
{ url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818, upload-time = "2025-09-14T22:18:03.769Z" },
{ url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402, upload-time = "2025-09-14T22:18:05.954Z" },
{ url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108, upload-time = "2025-09-14T22:18:07.68Z" },
{ url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248, upload-time = "2025-09-14T22:18:09.753Z" },
{ url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330, upload-time = "2025-09-14T22:18:11.966Z" },
{ url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123, upload-time = "2025-09-14T22:18:13.907Z" },
{ url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591, upload-time = "2025-09-14T22:18:16.465Z" },
{ url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513, upload-time = "2025-09-14T22:18:20.61Z" },
{ url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" },
{ url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" },
]