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