mirror of
https://github.com/koloideal/Argenta.git
synced 2026-06-10 18:15:28 +03:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| de7972c14f | |||
| 688dec6591 | |||
| 8d68cdc40d | |||
| 2785779583 | |||
| 0d8871a719 | |||
| 5eece75c40 | |||
| a3d7630219 | |||
| 7ffc6cd987 | |||
| db94cc8c9e | |||
| b9b83540e2 | |||
| 1cd5c3759e | |||
| 44f7b42302 | |||
| b2f5a1b163 | |||
| 1023d05419 | |||
| 732a4456b7 | |||
| de6d35205c | |||
| 6ed1d35e8a | |||
| 18a8376469 | |||
| 1211518c40 | |||
| 70f1327a0d | |||
| b732036e87 | |||
| e9dd7af905 |
@@ -1,6 +1,9 @@
|
||||
#### joe made this: http://goel.io/joe
|
||||
|
||||
metrics/reports/diagrams
|
||||
*.dist
|
||||
*build
|
||||
*.exe
|
||||
|
||||
#### python ####
|
||||
# Byte-compiled / optimized / DLL files
|
||||
@@ -322,3 +325,4 @@ http-client.private.env.json
|
||||
.idea/ApifoxUploaderProjectSetting.xml
|
||||
|
||||
.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,14 +58,8 @@ 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)
|
||||
@@ -73,30 +67,20 @@ def benchmark_validate_multiple_flags_10() -> None:
|
||||
|
||||
@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,15 +19,15 @@ def benchmark_no_aliases() -> None:
|
||||
app = App(override_system_messages=True)
|
||||
router = Router()
|
||||
|
||||
@router.command(Command('command1'))
|
||||
@router.command(Command("command1"))
|
||||
def handler1(_res: Response) -> None:
|
||||
pass
|
||||
|
||||
@router.command(Command('command2'))
|
||||
@router.command(Command("command2"))
|
||||
def handler2(_res: Response) -> None:
|
||||
pass
|
||||
|
||||
@router.command(Command('command3'))
|
||||
@router.command(Command("command3"))
|
||||
def handler3(_res: Response) -> None:
|
||||
pass
|
||||
|
||||
@@ -40,15 +40,15 @@ def benchmark_few_aliases() -> None:
|
||||
app = App(override_system_messages=True)
|
||||
router = Router()
|
||||
|
||||
@router.command(Command('command1', aliases={'c1', 'cmd1'}))
|
||||
@router.command(Command("command1", aliases={"c1", "cmd1"}))
|
||||
def handler1(_res: Response) -> None:
|
||||
pass
|
||||
|
||||
@router.command(Command('command2', aliases={'c2', 'cmd2'}))
|
||||
@router.command(Command("command2", aliases={"c2", "cmd2"}))
|
||||
def handler2(_res: Response) -> None:
|
||||
pass
|
||||
|
||||
@router.command(Command('command3', aliases={'c3', 'cmd3'}))
|
||||
@router.command(Command("command3", aliases={"c3", "cmd3"}))
|
||||
def handler3(_res: Response) -> None:
|
||||
pass
|
||||
|
||||
@@ -61,15 +61,15 @@ def benchmark_with_many_aliases() -> None:
|
||||
app = App(override_system_messages=True)
|
||||
router = Router()
|
||||
|
||||
@router.command(Command('command1', aliases={'c1', 'cmd1', 'com1', 'first', 'one'}))
|
||||
@router.command(Command("command1", aliases={"c1", "cmd1", "com1", "first", "one"}))
|
||||
def handler1(_res: Response) -> None:
|
||||
pass
|
||||
|
||||
@router.command(Command('command2', aliases={'c2', 'cmd2', 'com2', 'second', 'two'}))
|
||||
@router.command(Command("command2", aliases={"c2", "cmd2", "com2", "second", "two"}))
|
||||
def handler2(_res: Response) -> None:
|
||||
pass
|
||||
|
||||
@router.command(Command('command3', aliases={'c3', 'cmd3', 'com3', 'third', 'three'}))
|
||||
@router.command(Command("command3", aliases={"c3", "cmd3", "com3", "third", "three"}))
|
||||
def handler3(_res: Response) -> None:
|
||||
pass
|
||||
|
||||
@@ -82,15 +82,15 @@ 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)}))
|
||||
@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)}))
|
||||
@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)}))
|
||||
@router.command(Command("command3", aliases={f"alias3_{i}" for i in range(20)}))
|
||||
def handler3(_res: Response) -> None:
|
||||
pass
|
||||
|
||||
@@ -103,15 +103,15 @@ 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)}))
|
||||
@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)}))
|
||||
@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)}))
|
||||
@router.command(Command("command3", aliases={f"alias3_{i}" for i in range(100)}))
|
||||
def handler3(_res: Response) -> None:
|
||||
pass
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+56
-33
@@ -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))
|
||||
@@ -67,11 +76,13 @@ 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:
|
||||
@@ -94,13 +105,19 @@ def run_type_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, 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))
|
||||
@@ -113,22 +130,21 @@ def release_generate_handler(_: Response) -> None:
|
||||
console.print(f"[cyan]Generating release report for version:[/cyan] [bold]{lib_version}[/bold]")
|
||||
console.print("[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")
|
||||
@@ -164,7 +187,7 @@ def diagrams_generate_handler(response: Response) -> None:
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -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'
|
||||
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'}"
|
||||
)
|
||||
|
||||
ax.set_ylabel('Time (ms)', fontsize=14, fontweight='bold', labelpad=10)
|
||||
|
||||
title_text = f'{benchmark_group.type_.replace("_", " ").title()}'
|
||||
metadata_text = f'Iterations: {benchmark_group.iterations} | GC: {"Disabled" if benchmark_group.is_gc_disabled else "Enabled"}'
|
||||
|
||||
ax.text(0.5, 1.08, title_text, transform=ax.transAxes,
|
||||
fontsize=18, fontweight='bold', ha='center', color='#2c3e50')
|
||||
ax.text(0.5, 1.03, metadata_text, transform=ax.transAxes,
|
||||
fontsize=12, ha='center', color='#7f8c8d', style='italic')
|
||||
|
||||
ax.set_xticks(x_positions)
|
||||
ax.set_xticklabels([])
|
||||
|
||||
for i, (pos, desc) in enumerate(zip(x_positions, descriptions)):
|
||||
text_x_pos = pos - bar_width - (bar_width / 2)
|
||||
ax.text(
|
||||
text_x_pos,
|
||||
y_limit * 0.02,
|
||||
desc,
|
||||
rotation=90, va='bottom', ha='right', fontsize=10,
|
||||
color='#2c3e50'
|
||||
)
|
||||
|
||||
ax.set_ylim(0, y_limit)
|
||||
|
||||
legend = ax.legend(loc='upper left', fontsize=12, framealpha=0.95,
|
||||
edgecolor='#34495e', fancybox=True, shadow=True)
|
||||
legend.get_frame().set_facecolor('#ecf0f1')
|
||||
|
||||
ax.grid(axis='y', alpha=0.4, linestyle='--', linewidth=0.8)
|
||||
ax.set_axisbelow(True)
|
||||
|
||||
ax.spines['top'].set_visible(False)
|
||||
ax.spines['right'].set_visible(False)
|
||||
ax.spines['left'].set_color('#7f8c8d')
|
||||
ax.spines['bottom'].set_color('#7f8c8d')
|
||||
|
||||
plt.tight_layout()
|
||||
|
||||
filename = f"{benchmark_group.type_}_comparison.png"
|
||||
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
|
||||
|
||||
@@ -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()} ; "
|
||||
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"GC {'DISABLED' if benchmark_group_result.is_gc_disabled else 'ENABLED'} ; "
|
||||
f"ALL TIME IN MS",
|
||||
style="bold magenta")
|
||||
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)
|
||||
|
||||
@@ -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,12 +22,14 @@ 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
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PythonInfo:
|
||||
version: str
|
||||
@@ -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,77 +1,12 @@
|
||||
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:
|
||||
@@ -95,41 +30,14 @@ class AutoCompleter:
|
||||
self._fallback_mode = True
|
||||
return
|
||||
|
||||
kb = KeyBindings()
|
||||
from ._ext_features_impl import build_session
|
||||
|
||||
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
|
||||
|
||||
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"
|
||||
@@ -191,8 +188,10 @@ 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
|
||||
@@ -201,15 +200,17 @@ def test_autocompleter_initial_setup_with_commands() -> None:
|
||||
|
||||
|
||||
def test_autocompleter_initial_setup_with_history_file() -> None:
|
||||
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt') as f:
|
||||
with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".txt") as f:
|
||||
history_file = f.name
|
||||
|
||||
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
|
||||
@@ -223,9 +224,11 @@ 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
|
||||
@@ -236,8 +239,10 @@ 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
|
||||
@@ -247,31 +252,34 @@ def test_autocompleter_initial_setup_with_custom_autocomplete_button() -> None:
|
||||
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]]:
|
||||
@@ -281,16 +289,22 @@ def test_autocompleter_key_binding_handler_with_complete_state() -> None:
|
||||
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()
|
||||
@@ -305,25 +319,8 @@ def test_autocompleter_key_binding_handler_with_complete_state() -> None:
|
||||
|
||||
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()
|
||||
@@ -333,7 +330,6 @@ def test_autocompleter_key_binding_handler_no_completions() -> None:
|
||||
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()
|
||||
@@ -342,25 +338,8 @@ def test_autocompleter_key_binding_handler_no_completions() -> None:
|
||||
|
||||
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()
|
||||
@@ -371,7 +350,6 @@ def test_autocompleter_key_binding_handler_single_completion() -> None:
|
||||
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)
|
||||
@@ -380,25 +358,8 @@ def test_autocompleter_key_binding_handler_single_completion() -> None:
|
||||
|
||||
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()
|
||||
@@ -410,7 +371,6 @@ def test_autocompleter_key_binding_handler_multiple_completions() -> None:
|
||||
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)
|
||||
@@ -420,43 +380,43 @@ def test_autocompleter_key_binding_handler_multiple_completions() -> None:
|
||||
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
|
||||
@@ -466,13 +426,13 @@ 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"
|
||||
@@ -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"
|
||||
@@ -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