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