From 6aa6b0f1791502fe8c967a6c4b716a231eb56dff Mon Sep 17 00:00:00 2001 From: kolo Date: Sat, 17 Jan 2026 00:42:00 +0300 Subject: [PATCH 01/31] benchs --- metrics/__init__.py | 1 - metrics/__main__.py | 49 ++-------- metrics/benchmarks/__init__.py | 2 +- metrics/benchmarks/core/__init__.py | 1 + .../benchmarks/{ => core}/pre_cycle_setup.py | 4 +- metrics/{registry.py => benchmarks/models.py} | 12 +++ metrics/benchmarks/utils.py | 89 +++++++++++++++++++ metrics/handlers.py | 70 +++++++++++++++ metrics/utils.py | 44 --------- pyproject.toml | 3 + src/argenta/app/protocols.py | 4 +- uv.lock | 13 +++ 12 files changed, 201 insertions(+), 91 deletions(-) create mode 100644 metrics/benchmarks/core/__init__.py rename metrics/benchmarks/{ => core}/pre_cycle_setup.py (99%) rename metrics/{registry.py => benchmarks/models.py} (91%) create mode 100644 metrics/benchmarks/utils.py create mode 100644 metrics/handlers.py delete mode 100644 metrics/utils.py diff --git a/metrics/__init__.py b/metrics/__init__.py index 63f99dd..e69de29 100644 --- a/metrics/__init__.py +++ b/metrics/__init__.py @@ -1 +0,0 @@ -from .benchmarks import * \ No newline at end of file diff --git a/metrics/__main__.py b/metrics/__main__.py index 6e7f0c4..dcaa5d6 100644 --- a/metrics/__main__.py +++ b/metrics/__main__.py @@ -1,48 +1,15 @@ -from concurrent.futures import ProcessPoolExecutor -import os +from argenta import App, Orchestrator +from argenta.app import DynamicDividingLine -from rich.console import Console -from rich.table import Table -from rich.panel import Panel -from rich.text import Text +from .handlers import router -from metrics.utils import run_benchmark, BenchmarkResult -from .registry import Benchmarks, Benchmark +app = App(initial_message="metrics", prompt=">>> ", dividing_line=DynamicDividingLine('~')) +orchestrator = Orchestrator() -def main(): - console = Console() - all_benchmarks: list[Benchmark] = Benchmarks.get_benchmarks() - - workers = os.cpu_count() or 1 - with ProcessPoolExecutor(max_workers=workers) as executor: - results = executor.map(run_benchmark, all_benchmarks) - - type_paired_benchmarks: dict[str, list[BenchmarkResult]] = {} - - for result in results: - type_paired_benchmarks.setdefault(result.type_, []).append(result) - - for type_, benchmarks in type_paired_benchmarks.items(): - header_text = Text(f"TYPE: {type_.upper()}", style="bold magenta") - console.print(Panel(header_text, expand=False, border_style="magenta")) - - table = Table(show_header=True, header_style="bold cyan", border_style="blue", show_lines=True) - table.add_column("Name", style="green") - table.add_column("Description", style="dim") - table.add_column("Iterations", justify="right") - table.add_column("Avg Time (ms)", justify="right", style="bold yellow") - - for benchmark in benchmarks: - table.add_row( - benchmark.name, - benchmark.description, - str(benchmark.iterations), - str(benchmark.avg_time) - ) - - console.print(table) - console.print() +def main() -> None: + app.include_router(router) + orchestrator.start_polling(app) if __name__ == "__main__": diff --git a/metrics/benchmarks/__init__.py b/metrics/benchmarks/__init__.py index 64424de..a8ce586 100644 --- a/metrics/benchmarks/__init__.py +++ b/metrics/benchmarks/__init__.py @@ -1 +1 @@ -from .pre_cycle_setup import * \ No newline at end of file +from .core import * \ No newline at end of file diff --git a/metrics/benchmarks/core/__init__.py b/metrics/benchmarks/core/__init__.py new file mode 100644 index 0000000..64424de --- /dev/null +++ b/metrics/benchmarks/core/__init__.py @@ -0,0 +1 @@ +from .pre_cycle_setup import * \ No newline at end of file diff --git a/metrics/benchmarks/pre_cycle_setup.py b/metrics/benchmarks/core/pre_cycle_setup.py similarity index 99% rename from metrics/benchmarks/pre_cycle_setup.py rename to metrics/benchmarks/core/pre_cycle_setup.py index 4d0e386..d56cb95 100644 --- a/metrics/benchmarks/pre_cycle_setup.py +++ b/metrics/benchmarks/core/pre_cycle_setup.py @@ -7,12 +7,12 @@ __all__ = [ ] from argenta import App -from argenta.router import Router from argenta.command.models import Command from argenta.response import Response +from argenta.router import Router +from ..models import benchmark from ..utils import get_time_of_pre_cycle_setup -from ..registry import benchmark @benchmark(type_="pre_cycle_setup", description="With no aliases") diff --git a/metrics/registry.py b/metrics/benchmarks/models.py similarity index 91% rename from metrics/registry.py rename to metrics/benchmarks/models.py index 40c9537..efe7ead 100644 --- a/metrics/registry.py +++ b/metrics/benchmarks/models.py @@ -1,14 +1,26 @@ __all__ = [ "Benchmark", "Benchmarks", + "BenchmarkResult", "benchmark" ] +from dataclasses import dataclass +from decimal import Decimal from typing import Callable, ClassVar, overload, override BenchmarkAsFunc = Callable[[], float] +@dataclass(frozen=True) +class BenchmarkResult: + type_: str + name: str + description: str + iterations: int + avg_time: Decimal + + class Benchmark: def __init__( self, diff --git a/metrics/benchmarks/utils.py b/metrics/benchmarks/utils.py new file mode 100644 index 0000000..5ad0f78 --- /dev/null +++ b/metrics/benchmarks/utils.py @@ -0,0 +1,89 @@ +__all__ = [ + "get_time_of_pre_cycle_setup", + "attempts_to_average", + "run_benchmark", + "run_all_benchmarks", + "get_kernel_version" +] + +import io +import os +import platform +import sys +import time +from concurrent.futures import ProcessPoolExecutor +from contextlib import redirect_stdout +from decimal import ROUND_HALF_UP, Decimal + +from argenta import App +from .models import Benchmark, BenchmarkResult, Benchmarks + + +def get_time_of_pre_cycle_setup(app: App) -> float: + start = time.perf_counter() + with redirect_stdout(io.StringIO()): + app._pre_cycle_setup() # pyright: ignore[reportPrivateUsage] + end = time.perf_counter() + return (end - start) * 1000 # as milliseconds + + +def get_kernel_version() -> dict[str, str]: + system = platform.system() + + if system == "Windows": + ver = sys.getwindowsversion() + kernel_version = f"{ver.major}.{ver.minor}.{ver.build}" + + if ver.build >= 22000: + product_name = "Windows 11" + else: + product_name = "Windows 10" + + return { + 'kernel_version': kernel_version, + 'product_name': product_name + } + + elif system == "Linux": + return { + 'kernel_version': platform.release(), + 'product_name': platform.system() + } + + elif system == "Darwin": + return { + 'kernel_version': platform.release(), + 'product_name': f"macOS {platform.mac_ver()[0]}" + } + else: + return { + 'kernel_version': platform.release(), + 'product_name': platform.system(), + } + + +def attempts_to_average(bench_attempts: list[float], iterations: int) -> Decimal: + return Decimal(sum(bench_attempts) / iterations).quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP) + + +def run_all_benchmarks() -> dict[str, list[BenchmarkResult]]: + all_benchmarks: list[Benchmark] = Benchmarks.get_benchmarks() + + workers = os.cpu_count() or 1 + with ProcessPoolExecutor(max_workers=workers) as executor: + results = executor.map(run_benchmark, all_benchmarks) + + type_paired_benchmarks: dict[str, list[BenchmarkResult]] = {} + + for result in results: + type_paired_benchmarks.setdefault(result.type_, []).append(result) + + return type_paired_benchmarks + + +def run_benchmark(benchmark: Benchmark) -> BenchmarkResult: + bench_attempts: list[float] = [] + for _ in range(benchmark.iterations): + bench_attempts.append(benchmark.run()) + avg = attempts_to_average(bench_attempts, benchmark.iterations) + return BenchmarkResult(benchmark.type_, benchmark.name, benchmark.description, benchmark.iterations, avg) diff --git a/metrics/handlers.py b/metrics/handlers.py new file mode 100644 index 0000000..aed1984 --- /dev/null +++ b/metrics/handlers.py @@ -0,0 +1,70 @@ +import platform +import cpuinfo + +from rich.console import Console +from rich.panel import Panel +from rich.table import Table +from rich.text import Text + +from argenta.command.models import Command +from argenta.response import Response +from argenta.router import Router +from .benchmarks.models import BenchmarkResult +from .benchmarks.utils import run_all_benchmarks, get_kernel_version + +console = Console() +router = Router(title="Metrics commands:") + + +@router.command(Command("all-print", description="Print all benchmarks results")) +def all_print_handler(_: Response) -> None: + cpu_info = cpuinfo.get_cpu_info() + os_info = get_kernel_version() + + table = Table(show_header=True, header_style="bold cyan", border_style="blue", show_lines=True) + table.add_column("Parameter", style="green") + table.add_column("Value", style="yellow") + + table.add_row("OS", platform.system()) + table.add_row("OS Name", os_info['product_name']) + table.add_row("OS Kernel Version", os_info['kernel_version']) + table.add_row("Architecture", cpu_info['arch']) + table.add_row("Processor", cpu_info['brand_raw']) + table.add_row("Python Version", cpu_info['python_version']) + table.add_row("Python Implementation", platform.python_implementation()) + + header_text = Text("SYSTEM INFO", style="bold magenta") + console.print(Panel(header_text, expand=False, border_style="magenta")) + console.print(table, end="\n\n") + + type_paired_benchmarks: dict[str, list[BenchmarkResult]] = run_all_benchmarks() + + for type_, benchmarks in type_paired_benchmarks.items(): + header_text = Text(f"TYPE: {type_.upper()}", style="bold magenta") + console.print(Panel(header_text, expand=False, border_style="magenta")) + + table = Table(show_header=True, header_style="bold cyan", border_style="blue", show_lines=True) + table.add_column("Name", style="green") + table.add_column("Description", style="dim") + table.add_column("Iterations", justify="right") + table.add_column("Avg Time (ms)", justify="right", style="bold yellow") + + for benchmark in benchmarks: + table.add_row( + benchmark.name, + benchmark.description, + str(benchmark.iterations), + str(benchmark.avg_time) + ) + + console.print(table) + + +@router.command(Command("release-generate", description="Generate release report")) +def release_generate_handler(_: Response) -> None: + console.print("[yellow]Release report generation not implemented yet[/yellow]") + + +@router.command(Command("diagrams-generate", description="Generate diagrams")) +def diagrams_generate_handler(_: Response) -> None: + console.print("[yellow]Diagrams generation not implemented yet[/yellow]") diff --git a/metrics/utils.py b/metrics/utils.py deleted file mode 100644 index 2153323..0000000 --- a/metrics/utils.py +++ /dev/null @@ -1,44 +0,0 @@ -__all__ = [ - "get_time_of_pre_cycle_setup", - "attempts_to_average", - "run_benchmark", - "BenchmarkResult" -] - -import io -from contextlib import redirect_stdout -import time -from dataclasses import dataclass -from decimal import Decimal, ROUND_HALF_UP - -from argenta import App -from metrics.registry import Benchmark - - -def get_time_of_pre_cycle_setup(app: App) -> float: - start = time.perf_counter() - with redirect_stdout(io.StringIO()): - app._pre_cycle_setup() # pyright: ignore[reportPrivateUsage] - end = time.perf_counter() - return (end - start) * 1000 # as milliseconds - - -def attempts_to_average(bench_attempts: list[float], iterations: int) -> Decimal: - return Decimal(sum(bench_attempts) / iterations).quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP) - - -@dataclass(frozen=True) -class BenchmarkResult: - type_: str - name: str - description: str - iterations: int - avg_time: Decimal - - -def run_benchmark(benchmark: Benchmark) -> BenchmarkResult: - bench_attempts: list[float] = [] - for _ in range(benchmark.iterations): - bench_attempts.append(benchmark.run()) - avg = attempts_to_average(bench_attempts, benchmark.iterations) - return BenchmarkResult(benchmark.type_, benchmark.name, benchmark.description, benchmark.iterations, avg) diff --git a/pyproject.toml b/pyproject.toml index eeabf62..560c6d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,9 @@ tests = [ "pytest-cov>=7.0.0", "pytest-mock>=3.15.1", ] +metrics = [ + "py-cpuinfo>=9.0.0", +] [tool.ruff] exclude = [ diff --git a/src/argenta/app/protocols.py b/src/argenta/app/protocols.py index c5232f6..d68614d 100644 --- a/src/argenta/app/protocols.py +++ b/src/argenta/app/protocols.py @@ -1,6 +1,6 @@ __all__ = ["NonStandardBehaviorHandler", "EmptyCommandHandler", "Printer", "DescriptionMessageGenerator", "HandlerFunc"] -from typing import ParamSpec, Protocol, TypeVar +from typing import Any, ParamSpec, Protocol, TypeVar from argenta.response import Response T = TypeVar("T", contravariant=True) @@ -28,5 +28,5 @@ class DescriptionMessageGenerator(Protocol): class HandlerFunc(Protocol): - def __call__(self, response: Response) -> None: + def __call__(self, response: Response, /, *args: Any, **kwargs: Any) -> None: raise NotImplementedError diff --git a/uv.lock b/uv.lock index d5fb31f..f3ad6ca 100644 --- a/uv.lock +++ b/uv.lock @@ -61,6 +61,9 @@ linters = [ { name = "ruff" }, { name = "wemake-python-styleguide" }, ] +metrics = [ + { name = "py-cpuinfo" }, +] tests = [ { name = "pyfakefs" }, { name = "pytest" }, @@ -92,6 +95,7 @@ linters = [ { name = "ruff", specifier = ">=0.12.12" }, { name = "wemake-python-styleguide", specifier = ">=0.17.0" }, ] +metrics = [{ name = "py-cpuinfo", specifier = ">=9.0.0" }] tests = [ { name = "pyfakefs", specifier = ">=5.5.0" }, { name = "pytest", specifier = ">=8.3.2" }, @@ -589,6 +593,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "py-cpuinfo" +version = "9.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/a8/d832f7293ebb21690860d2e01d8115e5ff6f2ae8bbdc953f0eb0fa4bd2c7/py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690", size = 104716, upload-time = "2022-10-25T20:38:06.303Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5", size = 22335, upload-time = "2022-10-25T20:38:27.636Z" }, +] + [[package]] name = "pycodestyle" version = "2.14.0" From 2e5b19f4d80406b8b3f86d2cc041520af51abe80 Mon Sep 17 00:00:00 2001 From: kolo Date: Sat, 17 Jan 2026 01:21:39 +0300 Subject: [PATCH 02/31] benchs --- metrics/benchmarks/core/__init__.py | 5 +- .../core/finds_appropriate_handler.py | 81 +++++++++++++++ .../benchmarks/core/most_similar_command.py | 65 ++++++++++++ .../core/validate_routers_for_collisions.py | 99 +++++++++++++++++++ metrics/benchmarks/utils.py | 29 ++++++ src/argenta/app/models.py | 8 +- 6 files changed, 282 insertions(+), 5 deletions(-) create mode 100644 metrics/benchmarks/core/finds_appropriate_handler.py create mode 100644 metrics/benchmarks/core/most_similar_command.py create mode 100644 metrics/benchmarks/core/validate_routers_for_collisions.py diff --git a/metrics/benchmarks/core/__init__.py b/metrics/benchmarks/core/__init__.py index 64424de..bddae4e 100644 --- a/metrics/benchmarks/core/__init__.py +++ b/metrics/benchmarks/core/__init__.py @@ -1 +1,4 @@ -from .pre_cycle_setup import * \ No newline at end of file +from .pre_cycle_setup import * +from .validate_routers_for_collisions import * +from .most_similar_command import * +from .finds_appropriate_handler import * \ No newline at end of file diff --git a/metrics/benchmarks/core/finds_appropriate_handler.py b/metrics/benchmarks/core/finds_appropriate_handler.py new file mode 100644 index 0000000..9c22ae4 --- /dev/null +++ b/metrics/benchmarks/core/finds_appropriate_handler.py @@ -0,0 +1,81 @@ +__all__ = [ + "benchmark_simple_command", + "benchmark_command_with_flags", + "benchmark_many_commands", + "benchmark_command_with_many_flags", + "benchmark_extreme_router" +] + +from argenta.command.models import Command, InputCommand +from argenta.command import Flag, Flags +from argenta.response import Response +from argenta.router import Router + +from ..models import benchmark +from ..utils import get_time_of_finds_appropriate_handler + + +@benchmark(type_="finds_appropriate_handler", description="Simple command (no flags)") +def benchmark_simple_command() -> float: + router = Router() + + @router.command(Command('test')) + def handler(_res: Response) -> None: + pass + + input_cmd = InputCommand.parse('test') + return get_time_of_finds_appropriate_handler(router, input_cmd) + + +@benchmark(type_="finds_appropriate_handler", description="Command with flags (3 flags)") +def benchmark_command_with_flags() -> float: + router = Router() + + @router.command(Command('test', flags=Flags([Flag('a'), Flag('b'), Flag('c')]))) + def handler(_res: Response) -> None: + pass + + input_cmd = InputCommand.parse('test -a -b -c') + return get_time_of_finds_appropriate_handler(router, input_cmd) + + +@benchmark(type_="finds_appropriate_handler", description="Many commands (50 commands)") +def benchmark_many_commands() -> float: + router = Router() + + for i in range(50): + @router.command(Command(f'cmd{i}')) + def handler(_res: Response) -> None: + pass + + input_cmd = InputCommand.parse('cmd25') + return get_time_of_finds_appropriate_handler(router, input_cmd) + + +@benchmark(type_="finds_appropriate_handler", description="Command with many flags (20 flags)") +def benchmark_command_with_many_flags() -> float: + router = Router() + + flags = Flags([Flag(f'flag{i}') for i in range(20)]) + + @router.command(Command('test', flags=flags)) + def handler(_res: Response) -> None: + pass + + input_cmd = InputCommand.parse('test ' + ' '.join(f'-flag{i}' for i in range(10))) + return get_time_of_finds_appropriate_handler(router, input_cmd) + + +@benchmark(type_="finds_appropriate_handler", description="Extreme (100 commands, 10 flags each)") +def benchmark_extreme_router() -> float: + router = Router() + + for i in range(100): + flags = Flags([Flag(f'f{i}_{j}') for j in range(10)]) + + @router.command(Command(f'cmd{i}', flags=flags)) + def handler(_res: Response) -> None: + pass + + input_cmd = InputCommand.parse('cmd50 -f50_0 -f50_1 -f50_2') + return get_time_of_finds_appropriate_handler(router, input_cmd) diff --git a/metrics/benchmarks/core/most_similar_command.py b/metrics/benchmarks/core/most_similar_command.py new file mode 100644 index 0000000..ffe513c --- /dev/null +++ b/metrics/benchmarks/core/most_similar_command.py @@ -0,0 +1,65 @@ +__all__ = [ + "benchmark_few_commands", + "benchmark_many_commands", + "benchmark_many_aliases", + "benchmark_partial_match", + "benchmark_extreme_commands" +] + +import io +from contextlib import redirect_stdout + +from argenta import App +from argenta.command.models import Command +from argenta.response import Response +from argenta.router import Router + +from ..models import benchmark +from ..utils import get_time_of_most_similar_command + + +def setup_app_with_commands(command_count: int, aliases_per_command: int = 0) -> App: + app = App(override_system_messages=True) + router = Router() + + for i in range(command_count): + aliases = {f'alias{i}_{j}' for j in range(aliases_per_command)} if aliases_per_command else set() + + @router.command(Command(f'command{i}', aliases=aliases)) + def handler(_res: Response) -> None: + pass + + app.include_router(router) + with redirect_stdout(io.StringIO()): + app._pre_cycle_setup() # pyright: ignore[reportPrivateUsage] + return app + + +@benchmark(type_="most_similar_command", description="Few commands (10 commands, no match)") +def benchmark_few_commands() -> float: + app = setup_app_with_commands(10) + return get_time_of_most_similar_command(app, "unknown") + + +@benchmark(type_="most_similar_command", description="Many commands (50 commands, no match)") +def benchmark_many_commands() -> float: + app = setup_app_with_commands(50) + return get_time_of_most_similar_command(app, "unknown") + + +@benchmark(type_="most_similar_command", description="Many aliases (20 commands, 10 aliases each)") +def benchmark_many_aliases() -> float: + app = setup_app_with_commands(20, aliases_per_command=10) + return get_time_of_most_similar_command(app, "unknown") + + +@benchmark(type_="most_similar_command", description="Partial match (50 commands, prefix match)") +def benchmark_partial_match() -> float: + app = setup_app_with_commands(50) + return get_time_of_most_similar_command(app, "comm") + + +@benchmark(type_="most_similar_command", description="Extreme (100 commands, 20 aliases each)") +def benchmark_extreme_commands() -> float: + app = setup_app_with_commands(100, aliases_per_command=20) + return get_time_of_most_similar_command(app, "comm") diff --git a/metrics/benchmarks/core/validate_routers_for_collisions.py b/metrics/benchmarks/core/validate_routers_for_collisions.py new file mode 100644 index 0000000..65c8c1d --- /dev/null +++ b/metrics/benchmarks/core/validate_routers_for_collisions.py @@ -0,0 +1,99 @@ +__all__ = [ + "benchmark_few_routers", + "benchmark_many_routers", + "benchmark_many_commands_per_router", + "benchmark_many_aliases_per_command", + "benchmark_extreme_routers" +] + +from argenta import App +from argenta.command.models import Command +from argenta.response import Response +from argenta.router import Router + +from ..utils import get_time_of_validate_routers_for_collisions + +from ..models import benchmark + + +@benchmark(type_="validate_routers_for_collisions", description="With few routers (3 routers, 1 command each)") +def benchmark_few_routers() -> float: + app = App(override_system_messages=True) + + for i in range(3): + router = Router() + + @router.command(Command(f'cmd{i}')) + def handler(_res: Response) -> None: + pass + + app.include_router(router) + + return get_time_of_validate_routers_for_collisions(app) + + +@benchmark(type_="validate_routers_for_collisions", description="With many routers (10 routers, 1 command each)") +def benchmark_many_routers() -> float: + app = App(override_system_messages=True) + + for i in range(10): + router = Router() + + @router.command(Command(f'cmd{i}')) + def handler(_res: Response) -> None: + pass + + app.include_router(router) + + return get_time_of_validate_routers_for_collisions(app) + + +@benchmark(type_="validate_routers_for_collisions", description="With many commands per router (3 routers, 10 commands each)") +def benchmark_many_commands_per_router() -> float: + app = App(override_system_messages=True) + + for i in range(3): + router = Router() + + for j in range(10): + @router.command(Command(f'cmd{i}_{j}')) + def handler(_res: Response) -> None: + pass + + app.include_router(router) + + return get_time_of_validate_routers_for_collisions(app) + + +@benchmark(type_="validate_routers_for_collisions", description="With many aliases (3 routers, 5 commands, 10 aliases each)") +def benchmark_many_aliases_per_command() -> float: + app = App(override_system_messages=True) + + for i in range(3): + router = Router() + + for j in range(5): + @router.command(Command(f'cmd{i}_{j}', aliases={f'alias{i}_{j}_{k}' for k in range(10)})) + def handler(_res: Response) -> None: + pass + + app.include_router(router) + + return get_time_of_validate_routers_for_collisions(app) + + +@benchmark(type_="validate_routers_for_collisions", description="Extreme (20 routers, 10 commands, 20 aliases each)") +def benchmark_extreme_routers() -> float: + app = App(override_system_messages=True) + + for i in range(20): + router = Router() + + for j in range(10): + @router.command(Command(f'cmd{i}_{j}', aliases={f'alias{i}_{j}_{k}' for k in range(20)})) + def handler(_res: Response) -> None: + pass + + app.include_router(router) + + return get_time_of_validate_routers_for_collisions(app) diff --git a/metrics/benchmarks/utils.py b/metrics/benchmarks/utils.py index 5ad0f78..bb10000 100644 --- a/metrics/benchmarks/utils.py +++ b/metrics/benchmarks/utils.py @@ -1,5 +1,8 @@ __all__ = [ "get_time_of_pre_cycle_setup", + "get_time_of_validate_routers_for_collisions", + "get_time_of_most_similar_command", + "get_time_of_finds_appropriate_handler", "attempts_to_average", "run_benchmark", "run_all_benchmarks", @@ -16,6 +19,8 @@ from contextlib import redirect_stdout from decimal import ROUND_HALF_UP, Decimal from argenta import App +from argenta.router import Router +from argenta.command.models import InputCommand from .models import Benchmark, BenchmarkResult, Benchmarks @@ -26,6 +31,30 @@ def get_time_of_pre_cycle_setup(app: App) -> float: end = time.perf_counter() return (end - start) * 1000 # as milliseconds +def get_time_of_validate_routers_for_collisions(app: App) -> float: + app._setup_system_router() # pyright: ignore[reportPrivateUsage] + start = time.perf_counter() + with redirect_stdout(io.StringIO()): + app._validate_routers_for_collisions() # pyright: ignore[reportPrivateUsage] + end = time.perf_counter() + return (end - start) * 1000 + + +def get_time_of_most_similar_command(app: App, unknown_command: str) -> float: + start = time.perf_counter() + with redirect_stdout(io.StringIO()): + app._most_similar_command(unknown_command) # pyright: ignore[reportPrivateUsage] + end = time.perf_counter() + return (end - start) * 1000 + + +def get_time_of_finds_appropriate_handler(router: "Router", input_command: "InputCommand") -> float: + start = time.perf_counter() + with redirect_stdout(io.StringIO()): + router.finds_appropriate_handler(input_command) + end = time.perf_counter() + return (end - start) * 1000 + def get_kernel_version() -> dict[str, str]: system = platform.system() diff --git a/src/argenta/app/models.py b/src/argenta/app/models.py index 2bf37f8..5f03f88 100644 --- a/src/argenta/app/models.py +++ b/src/argenta/app/models.py @@ -168,9 +168,9 @@ class BaseApp: max_length_line = max([len(line) for line in clear_text.split("\n")]) max_length_line = ( max_length_line - if 10 <= max_length_line <= 80 - else 80 - if max_length_line > 80 + if 10 <= max_length_line <= 100 + else 100 + if max_length_line > 100 else 10 ) @@ -306,7 +306,7 @@ class BaseApp: Private. Sets up default app view :return: None """ - self._prompt = f"[italic dim bold]{self._prompt}" + self._prompt = f"[italic dim bold]{self._prompt}[/italic dim bold]" self._initial_message = ( "\n" + f"[bold red]{text2art(self._initial_message, font='tarty1')}" + "\n" ) From 1648a8206a6440c9f46438884187de757158b723 Mon Sep 17 00:00:00 2001 From: kolo Date: Sat, 17 Jan 2026 23:43:59 +0300 Subject: [PATCH 03/31] bbbbbbenchh --- metrics/benchmarks/exceptions.py | 14 +++ metrics/benchmarks/models.py | 163 +++++++++++++++++++------------ metrics/benchmarks/utils.py | 46 ++++----- metrics/handlers.py | 6 +- pyproject.toml | 1 + uv.lock | 15 ++- 6 files changed, 153 insertions(+), 92 deletions(-) create mode 100644 metrics/benchmarks/exceptions.py diff --git a/metrics/benchmarks/exceptions.py b/metrics/benchmarks/exceptions.py new file mode 100644 index 0000000..497ee02 --- /dev/null +++ b/metrics/benchmarks/exceptions.py @@ -0,0 +1,14 @@ +class BenchmarkNotFound(Exception): + def __init__(self, benchmark_name: str): + self.benchmark_name = benchmark_name + + def __str__(self): + return f"Benchmark with name '{self.benchmark_name}' not found" + + +class BenchmarksNotFound(Exception): + def __init__(self, type_: str): + self.type_ = type_ + + def __str__(self): + return f"Benchmarks with type '{self.type_}' not found" diff --git a/metrics/benchmarks/models.py b/metrics/benchmarks/models.py index efe7ead..9726d9c 100644 --- a/metrics/benchmarks/models.py +++ b/metrics/benchmarks/models.py @@ -1,26 +1,39 @@ __all__ = [ "Benchmark", "Benchmarks", - "BenchmarkResult", - "benchmark" + "BenchmarkResult" ] from dataclasses import dataclass -from decimal import Decimal -from typing import Callable, ClassVar, overload, override +import time +import gc +import statistics +from typing import Callable, override + +from .exceptions import BenchmarkNotFound, BenchmarksNotFound + BenchmarkAsFunc = Callable[[], float] -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class BenchmarkResult: type_: str name: str description: str iterations: int - avg_time: Decimal + is_gc_disabled: bool + avg_time: float + median_time: float + std_dev: float + + +@dataclass(frozen=True, slots=True) +class BenchmarkGroupResult: + type_: str + benchmark_results: list[BenchmarkResult] + - class Benchmark: def __init__( self, @@ -28,83 +41,109 @@ class Benchmark: *, type_: str, name: str, - description: str, - iterations: int + description: str ) -> None: self.func = func self.type_ = type_ self.name = name self.description = description - self.iterations = iterations - def run(self) -> float: - return self.func() + def single_run(self, is_gc_disabled: bool = False) -> float: + if is_gc_disabled: + was_gc_enabled = gc.isenabled() + gc.disable() + + start = time.perf_counter() + self.func() + end = time.perf_counter() + + if was_gc_enabled: + gc.enable() + gc.collect() + + return end - start + else: + start = time.perf_counter() + self.func() + end = time.perf_counter() + return end - start + + def multiple_runs(self, iterations: int, is_gc_disabled: bool = False) -> tuple[float]: + run_attempts: list[float] = [] + for _ in range(iterations): + run_attempts.append(self.single_run(is_gc_disabled)) + return tuple(*run_attempts) @override def __repr__(self) -> str: - return f'Benchmark<{self.type_=}, {self.name=}, {self.description=}, {self.iterations=}>' + return f'Benchmark<{self.type_=}, {self.name=}, {self.description=}>' @override def __str__(self) -> str: - return f'Benchmark({self.type_=}, {self.name=}, {self.description=}, {self.iterations=})' + return f'benchmark {self.name} with type {self.type_}' class Benchmarks: - _benchmarks: ClassVar[list[Benchmark]] = [] + def __init__(self, *benchmarks: Benchmark) -> None: + self._benchmarks: list[Benchmark] = list(benchmarks) + self._benchmarks_grouped_by_type: dict[str, list[Benchmark]] = {} + self._benchmarks_paired_by_name: dict[str, Benchmark] = {} - @overload - @classmethod def register( - cls, - call: BenchmarkAsFunc, - *, - type_: str = "", - description: str = "", - iterations: int = 100, - ) -> BenchmarkAsFunc: - ... - - @overload - @classmethod - def register( - cls, - call: None = None, - *, - type_: str = "", - description: str = "", - iterations: int = 100, + self, + type_: str, + description: str = "" ) -> Callable[[BenchmarkAsFunc], BenchmarkAsFunc]: - ... - - @classmethod - def register( - cls, - call: BenchmarkAsFunc | None = None, - *, - type_: str = "", - description: str = "", - iterations: int = 100, - ) -> Callable[[BenchmarkAsFunc], BenchmarkAsFunc] | BenchmarkAsFunc: def decorator(func: BenchmarkAsFunc) -> BenchmarkAsFunc: - cls._benchmarks.append( - Benchmark( - func, - type_=type_, - name=func.__name__, - description=description or f'description for {func.__name__} with {iterations} iterations', - iterations=iterations - ) + benchmark = Benchmark( + func, + type_=type_, + name=func.__name__, + description=description or f'description for {func.__name__} with type {type_}', ) + self._benchmarks.append(benchmark) + self._benchmarks_paired_by_name[type_] = benchmark + self._benchmarks_grouped_by_type.setdefault(type_, []).append(benchmark) return func + return decorator - if call is None: - return decorator - else: - return decorator(call) + def run_benchmark_by_name(self, name: str, iterations: int = 100, is_gc_disables: bool = False) -> BenchmarkResult: + benchmark = self.get_benchmark_by_name(name) + if not benchmark: + raise BenchmarkNotFound(name) + run_attempts: tuple[float] = benchmark.multiple_runs(iterations, is_gc_disables) - @classmethod - def get_benchmarks(cls) -> list[Benchmark]: - return cls._benchmarks + avg = statistics.mean(run_attempts) + median = statistics.median(run_attempts) + std_dev = statistics.stdev(run_attempts) if len(run_attempts) > 1 else 0 + return BenchmarkResult( + type_=benchmark.type_, + name=benchmark.name, + description=benchmark.description, + iterations=iterations, + is_gc_disabled=is_gc_disables, + avg_time=avg, + median_time=median, + std_dev=std_dev + ) -benchmark = Benchmarks.register + def run_benchmarks_by_type(self, type_: str, iterations: int = 100, is_gc_disabled: bool = False) -> BenchmarkGroupResult: + benchmarks = self.get_benchmarks_by_type(type_) + if not benchmarks: + raise BenchmarksNotFound(type_) + benchmark_results: list[BenchmarkResult] = [] + + for benchmark in benchmarks: + benchmark_results.append(self.run_benchmark_by_name(benchmark.name, iterations, is_gc_disabled)) + + return BenchmarkGroupResult( + type_=type_, + benchmark_results=benchmark_results + ) + + def get_benchmarks_by_type(self, type_: str) -> list[Benchmark]: + return self._benchmarks_grouped_by_type.get(type_, []) + + def get_benchmark_by_name(self, name: str) -> Benchmark | None: + return self._benchmarks_paired_by_name.get(name) diff --git a/metrics/benchmarks/utils.py b/metrics/benchmarks/utils.py index bb10000..95af193 100644 --- a/metrics/benchmarks/utils.py +++ b/metrics/benchmarks/utils.py @@ -3,10 +3,8 @@ __all__ = [ "get_time_of_validate_routers_for_collisions", "get_time_of_most_similar_command", "get_time_of_finds_appropriate_handler", - "attempts_to_average", - "run_benchmark", - "run_all_benchmarks", - "get_kernel_version" + "get_kernel_version", + "get_gpu_info" ] import io @@ -18,6 +16,8 @@ from concurrent.futures import ProcessPoolExecutor from contextlib import redirect_stdout from decimal import ROUND_HALF_UP, Decimal +import pynvml + from argenta import App from argenta.router import Router from argenta.command.models import InputCommand @@ -48,7 +48,7 @@ def get_time_of_most_similar_command(app: App, unknown_command: str) -> float: return (end - start) * 1000 -def get_time_of_finds_appropriate_handler(router: "Router", input_command: "InputCommand") -> float: +def get_time_of_finds_appropriate_handler(router: Router, input_command: InputCommand) -> float: start = time.perf_counter() with redirect_stdout(io.StringIO()): router.finds_appropriate_handler(input_command) @@ -90,29 +90,21 @@ def get_kernel_version() -> dict[str, str]: 'product_name': platform.system(), } +def get_gpu_info() -> str: + try: + pynvml.nvmlInit() + device_count = pynvml.nvmlDeviceGetCount() + if device_count == 0: + return "N/A" -def attempts_to_average(bench_attempts: list[float], iterations: int) -> Decimal: - return Decimal(sum(bench_attempts) / iterations).quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP) + handle = pynvml.nvmlDeviceGetHandleByIndex(0) + name = pynvml.nvmlDeviceGetName(handle) + if isinstance(name, bytes): + name = name.decode("utf-8") -def run_all_benchmarks() -> dict[str, list[BenchmarkResult]]: - all_benchmarks: list[Benchmark] = Benchmarks.get_benchmarks() + pynvml.nvmlShutdown() + return name + except pynvml.NVMLError: + return "N/A" - workers = os.cpu_count() or 1 - with ProcessPoolExecutor(max_workers=workers) as executor: - results = executor.map(run_benchmark, all_benchmarks) - - type_paired_benchmarks: dict[str, list[BenchmarkResult]] = {} - - for result in results: - type_paired_benchmarks.setdefault(result.type_, []).append(result) - - return type_paired_benchmarks - - -def run_benchmark(benchmark: Benchmark) -> BenchmarkResult: - bench_attempts: list[float] = [] - for _ in range(benchmark.iterations): - bench_attempts.append(benchmark.run()) - avg = attempts_to_average(bench_attempts, benchmark.iterations) - return BenchmarkResult(benchmark.type_, benchmark.name, benchmark.description, benchmark.iterations, avg) diff --git a/metrics/handlers.py b/metrics/handlers.py index aed1984..9516bc0 100644 --- a/metrics/handlers.py +++ b/metrics/handlers.py @@ -10,7 +10,7 @@ from argenta.command.models import Command from argenta.response import Response from argenta.router import Router from .benchmarks.models import BenchmarkResult -from .benchmarks.utils import run_all_benchmarks, get_kernel_version +from .benchmarks.utils import run_all_benchmarks, get_kernel_version, get_gpu_info console = Console() router = Router(title="Metrics commands:") @@ -19,6 +19,7 @@ router = Router(title="Metrics commands:") @router.command(Command("all-print", description="Print all benchmarks results")) def all_print_handler(_: Response) -> None: cpu_info = cpuinfo.get_cpu_info() + gpu_info = get_gpu_info() os_info = get_kernel_version() table = Table(show_header=True, header_style="bold cyan", border_style="blue", show_lines=True) @@ -29,7 +30,8 @@ def all_print_handler(_: Response) -> None: table.add_row("OS Name", os_info['product_name']) table.add_row("OS Kernel Version", os_info['kernel_version']) table.add_row("Architecture", cpu_info['arch']) - table.add_row("Processor", cpu_info['brand_raw']) + table.add_row("CPU", cpu_info['brand_raw']) + table.add_row("GPU", gpu_info) table.add_row("Python Version", cpu_info['python_version']) table.add_row("Python Implementation", platform.python_implementation()) diff --git a/pyproject.toml b/pyproject.toml index 560c6d9..d92d3b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ tests = [ "pytest-mock>=3.15.1", ] metrics = [ + "nvidia-ml-py>=13.590.44", "py-cpuinfo>=9.0.0", ] diff --git a/uv.lock b/uv.lock index f3ad6ca..5fceb6e 100644 --- a/uv.lock +++ b/uv.lock @@ -62,6 +62,7 @@ linters = [ { name = "wemake-python-styleguide" }, ] metrics = [ + { name = "nvidia-ml-py" }, { name = "py-cpuinfo" }, ] tests = [ @@ -95,7 +96,10 @@ linters = [ { name = "ruff", specifier = ">=0.12.12" }, { name = "wemake-python-styleguide", specifier = ">=0.17.0" }, ] -metrics = [{ name = "py-cpuinfo", specifier = ">=9.0.0" }] +metrics = [ + { name = "nvidia-ml-py", specifier = ">=13.590.44" }, + { name = "py-cpuinfo", specifier = ">=9.0.0" }, +] tests = [ { name = "pyfakefs", specifier = ">=5.5.0" }, { name = "pytest", specifier = ">=8.3.2" }, @@ -557,6 +561,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] +[[package]] +name = "nvidia-ml-py" +version = "13.590.44" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/23/3871537f204aee823c574ba25cbeb08cae779979d4d43c01adddda00bab9/nvidia_ml_py-13.590.44.tar.gz", hash = "sha256:b358c7614b0fdeea4b95f046f1c90123bfe25d148ab93bb1c00248b834703373", size = 49737, upload-time = "2025-12-08T14:41:10.872Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/47/4c822bd37a008e72fd5a0eae33524ae3ac97b13f7030f63bae1728b8957e/nvidia_ml_py-13.590.44-py3-none-any.whl", hash = "sha256:18feb54eca7d0e3cdc8d1a040a771eda72d9ec3148e5443087970dbfd7377ecc", size = 50683, upload-time = "2025-12-08T14:41:09.597Z" }, +] + [[package]] name = "packaging" version = "25.0" From 69e871a639a336496bc2a685d01875eb4322d9bf Mon Sep 17 00:00:00 2001 From: kolo Date: Sun, 18 Jan 2026 01:59:55 +0300 Subject: [PATCH 04/31] bbbbbbenchh --- .../en/LC_MESSAGES/root/api/app/index.po | 2 +- .../LC_MESSAGES/root/dependency_injection.po | 2 +- docs/locales/en/LC_MESSAGES/root/flags.po | 2 +- metrics/__main__.py | 4 +- metrics/benchmarks/__init__.py | 5 +- metrics/benchmarks/core/__init__.py | 4 - metrics/benchmarks/{ => core}/exceptions.py | 0 metrics/benchmarks/{ => core}/models.py | 52 +++++---- metrics/benchmarks/entity.py | 3 + .../{core => }/finds_appropriate_handler.py | 33 +++--- .../{core => }/most_similar_command.py | 33 +++--- .../benchmarks/{core => }/pre_cycle_setup.py | 38 +++--- metrics/benchmarks/utils.py | 110 ------------------ .../validate_routers_for_collisions.py | 39 ++++--- metrics/handlers.py | 19 +-- metrics/utils.py | 63 ++++++++++ 16 files changed, 188 insertions(+), 221 deletions(-) rename metrics/benchmarks/{ => core}/exceptions.py (100%) rename metrics/benchmarks/{ => core}/models.py (71%) create mode 100644 metrics/benchmarks/entity.py rename metrics/benchmarks/{core => }/finds_appropriate_handler.py (57%) rename metrics/benchmarks/{core => }/most_similar_command.py (52%) rename metrics/benchmarks/{core => }/pre_cycle_setup.py (72%) delete mode 100644 metrics/benchmarks/utils.py rename metrics/benchmarks/{core => }/validate_routers_for_collisions.py (58%) create mode 100644 metrics/utils.py diff --git a/docs/locales/en/LC_MESSAGES/root/api/app/index.po b/docs/locales/en/LC_MESSAGES/root/api/app/index.po index e27c887..32951af 100644 --- a/docs/locales/en/LC_MESSAGES/root/api/app/index.po +++ b/docs/locales/en/LC_MESSAGES/root/api/app/index.po @@ -29,7 +29,7 @@ msgid "" "взаимодействие с пользователем, координируя работу всех компонентов: " "роутеров, обработчиков и системных сообщений." msgstr "" -"The ``App`` object is the core of your console application. It handles " +"The ``App`` object is the implementations of your console application. It handles " "configuration, lifecycle management, command processing, and user " "interaction, coordinating the work of all components: routers, handlers, " "and system messages." diff --git a/docs/locales/en/LC_MESSAGES/root/dependency_injection.po b/docs/locales/en/LC_MESSAGES/root/dependency_injection.po index b4d124d..07a130a 100644 --- a/docs/locales/en/LC_MESSAGES/root/dependency_injection.po +++ b/docs/locales/en/LC_MESSAGES/root/dependency_injection.po @@ -109,7 +109,7 @@ msgstr "How Does It Work?" #: ../../root/dependency_injection.rst:51 msgid "В основе DI в Argenta лежат **провайдеры** и **контейнер**." -msgstr "At the core of DI in Argenta are **providers** and a **container**." +msgstr "At the implementations of DI in Argenta are **providers** and a **container**." #: ../../root/dependency_injection.rst:53 msgid "" diff --git a/docs/locales/en/LC_MESSAGES/root/flags.po b/docs/locales/en/LC_MESSAGES/root/flags.po index cc51eb2..cab78b5 100644 --- a/docs/locales/en/LC_MESSAGES/root/flags.po +++ b/docs/locales/en/LC_MESSAGES/root/flags.po @@ -49,7 +49,7 @@ msgstr "" "The main purpose of flags is to provide a way to change the command's " "logic without reworking it. A command can operate in several modes: " "standard, verbose, debug, or simplified. Flags switch these modes on user" -" demand, keeping the core functionality unchanged." +" demand, keeping the implementations functionality unchanged." #: ../../root/flags.rst:17 msgid "Опциональность и удобство" diff --git a/metrics/__main__.py b/metrics/__main__.py index dcaa5d6..a775226 100644 --- a/metrics/__main__.py +++ b/metrics/__main__.py @@ -1,9 +1,9 @@ from argenta import App, Orchestrator -from argenta.app import DynamicDividingLine +from argenta.app import StaticDividingLine from .handlers import router -app = App(initial_message="metrics", prompt=">>> ", dividing_line=DynamicDividingLine('~')) +app = App(initial_message="metrics", prompt=">>> ", dividing_line=StaticDividingLine('~', length=120)) orchestrator = Orchestrator() diff --git a/metrics/benchmarks/__init__.py b/metrics/benchmarks/__init__.py index a8ce586..cf0f7aa 100644 --- a/metrics/benchmarks/__init__.py +++ b/metrics/benchmarks/__init__.py @@ -1 +1,4 @@ -from .core import * \ No newline at end of file +from .pre_cycle_setup import * +from .most_similar_command import * +from .finds_appropriate_handler import * +from .validate_routers_for_collisions import * \ No newline at end of file diff --git a/metrics/benchmarks/core/__init__.py b/metrics/benchmarks/core/__init__.py index bddae4e..e69de29 100644 --- a/metrics/benchmarks/core/__init__.py +++ b/metrics/benchmarks/core/__init__.py @@ -1,4 +0,0 @@ -from .pre_cycle_setup import * -from .validate_routers_for_collisions import * -from .most_similar_command import * -from .finds_appropriate_handler import * \ No newline at end of file diff --git a/metrics/benchmarks/exceptions.py b/metrics/benchmarks/core/exceptions.py similarity index 100% rename from metrics/benchmarks/exceptions.py rename to metrics/benchmarks/core/exceptions.py diff --git a/metrics/benchmarks/models.py b/metrics/benchmarks/core/models.py similarity index 71% rename from metrics/benchmarks/models.py rename to metrics/benchmarks/core/models.py index 9726d9c..4be6969 100644 --- a/metrics/benchmarks/models.py +++ b/metrics/benchmarks/core/models.py @@ -1,9 +1,12 @@ __all__ = [ "Benchmark", "Benchmarks", - "BenchmarkResult" + "BenchmarkResult", + "BenchmarkGroupResult" ] +import io +from contextlib import redirect_stdout from dataclasses import dataclass import time import gc @@ -13,7 +16,8 @@ from typing import Callable, override from .exceptions import BenchmarkNotFound, BenchmarksNotFound -BenchmarkAsFunc = Callable[[], float] +FuncForBenchmark = Callable[[], None] +MILLISECONDS_IN_SECONDS = 1000 @dataclass(frozen=True, slots=True) @@ -37,7 +41,7 @@ class BenchmarkGroupResult: class Benchmark: def __init__( self, - func: BenchmarkAsFunc, + func: FuncForBenchmark, *, type_: str, name: str, @@ -53,26 +57,28 @@ class Benchmark: was_gc_enabled = gc.isenabled() gc.disable() - start = time.perf_counter() - self.func() - end = time.perf_counter() + with redirect_stdout(io.StringIO()): + start = time.perf_counter() + self.func() + end = time.perf_counter() if was_gc_enabled: gc.enable() gc.collect() - return end - start + return (end - start) * MILLISECONDS_IN_SECONDS else: - start = time.perf_counter() - self.func() - end = time.perf_counter() - return end - start + with redirect_stdout(io.StringIO()): + start = time.perf_counter() + self.func() + end = time.perf_counter() + return (end - start) * MILLISECONDS_IN_SECONDS - def multiple_runs(self, iterations: int, is_gc_disabled: bool = False) -> tuple[float]: + def multiple_runs(self, iterations: int, is_gc_disabled: bool = False) -> tuple[float, ...]: run_attempts: list[float] = [] for _ in range(iterations): run_attempts.append(self.single_run(is_gc_disabled)) - return tuple(*run_attempts) + return tuple(run_attempts) @override def __repr__(self) -> str: @@ -93,8 +99,8 @@ class Benchmarks: self, type_: str, description: str = "" - ) -> Callable[[BenchmarkAsFunc], BenchmarkAsFunc]: - def decorator(func: BenchmarkAsFunc) -> BenchmarkAsFunc: + ) -> Callable[[FuncForBenchmark], FuncForBenchmark]: + def decorator(func: FuncForBenchmark) -> FuncForBenchmark: benchmark = Benchmark( func, type_=type_, @@ -102,7 +108,7 @@ class Benchmarks: description=description or f'description for {func.__name__} with type {type_}', ) self._benchmarks.append(benchmark) - self._benchmarks_paired_by_name[type_] = benchmark + self._benchmarks_paired_by_name[func.__name__] = benchmark self._benchmarks_grouped_by_type.setdefault(type_, []).append(benchmark) return func return decorator @@ -111,11 +117,11 @@ class Benchmarks: benchmark = self.get_benchmark_by_name(name) if not benchmark: raise BenchmarkNotFound(name) - run_attempts: tuple[float] = benchmark.multiple_runs(iterations, is_gc_disables) + run_attempts: tuple[float, ...] = benchmark.multiple_runs(iterations, is_gc_disables) - avg = statistics.mean(run_attempts) - median = statistics.median(run_attempts) - std_dev = statistics.stdev(run_attempts) if len(run_attempts) > 1 else 0 + avg = round(statistics.mean(run_attempts), 4) + median = round(statistics.median(run_attempts), 4) + std_dev = round(statistics.stdev(run_attempts) if len(run_attempts) > 1 else 0, 4) return BenchmarkResult( type_=benchmark.type_, @@ -142,6 +148,12 @@ class Benchmarks: benchmark_results=benchmark_results ) + def run_benchmarks_grouped_by_type(self) -> list[BenchmarkGroupResult]: + results: list[BenchmarkGroupResult] = [] + for type_, benchmarks in self._benchmarks_grouped_by_type.items(): + results.append(self.run_benchmarks_by_type(type_)) + return results + def get_benchmarks_by_type(self, type_: str) -> list[Benchmark]: return self._benchmarks_grouped_by_type.get(type_, []) diff --git a/metrics/benchmarks/entity.py b/metrics/benchmarks/entity.py new file mode 100644 index 0000000..6722849 --- /dev/null +++ b/metrics/benchmarks/entity.py @@ -0,0 +1,3 @@ +from metrics.benchmarks.core.models import Benchmarks + +benchmarks = Benchmarks() \ No newline at end of file diff --git a/metrics/benchmarks/core/finds_appropriate_handler.py b/metrics/benchmarks/finds_appropriate_handler.py similarity index 57% rename from metrics/benchmarks/core/finds_appropriate_handler.py rename to metrics/benchmarks/finds_appropriate_handler.py index 9c22ae4..822b5e4 100644 --- a/metrics/benchmarks/core/finds_appropriate_handler.py +++ b/metrics/benchmarks/finds_appropriate_handler.py @@ -11,12 +11,11 @@ from argenta.command import Flag, Flags from argenta.response import Response from argenta.router import Router -from ..models import benchmark -from ..utils import get_time_of_finds_appropriate_handler +from .entity import benchmarks -@benchmark(type_="finds_appropriate_handler", description="Simple command (no flags)") -def benchmark_simple_command() -> float: +@benchmarks.register(type_="finds_appropriate_handler", description="Simple command (no flags)") +def benchmark_simple_command() -> None: router = Router() @router.command(Command('test')) @@ -24,11 +23,11 @@ def benchmark_simple_command() -> float: pass input_cmd = InputCommand.parse('test') - return get_time_of_finds_appropriate_handler(router, input_cmd) + router.finds_appropriate_handler(input_cmd) -@benchmark(type_="finds_appropriate_handler", description="Command with flags (3 flags)") -def benchmark_command_with_flags() -> float: +@benchmarks.register(type_="finds_appropriate_handler", description="Command with flags (3 flags)") +def benchmark_command_with_flags() -> None: router = Router() @router.command(Command('test', flags=Flags([Flag('a'), Flag('b'), Flag('c')]))) @@ -36,11 +35,11 @@ def benchmark_command_with_flags() -> float: pass input_cmd = InputCommand.parse('test -a -b -c') - return get_time_of_finds_appropriate_handler(router, input_cmd) + router.finds_appropriate_handler(input_cmd) -@benchmark(type_="finds_appropriate_handler", description="Many commands (50 commands)") -def benchmark_many_commands() -> float: +@benchmarks.register(type_="finds_appropriate_handler", description="Many commands (50 commands)") +def benchmark_many_commands() -> None: router = Router() for i in range(50): @@ -49,11 +48,11 @@ def benchmark_many_commands() -> float: pass input_cmd = InputCommand.parse('cmd25') - return get_time_of_finds_appropriate_handler(router, input_cmd) + router.finds_appropriate_handler(input_cmd) -@benchmark(type_="finds_appropriate_handler", description="Command with many flags (20 flags)") -def benchmark_command_with_many_flags() -> float: +@benchmarks.register(type_="finds_appropriate_handler", description="Command with many flags (20 flags)") +def benchmark_command_with_many_flags() -> None: router = Router() flags = Flags([Flag(f'flag{i}') for i in range(20)]) @@ -63,11 +62,11 @@ def benchmark_command_with_many_flags() -> float: pass input_cmd = InputCommand.parse('test ' + ' '.join(f'-flag{i}' for i in range(10))) - return get_time_of_finds_appropriate_handler(router, input_cmd) + router.finds_appropriate_handler(input_cmd) -@benchmark(type_="finds_appropriate_handler", description="Extreme (100 commands, 10 flags each)") -def benchmark_extreme_router() -> float: +@benchmarks.register(type_="finds_appropriate_handler", description="Extreme (100 commands, 10 flags each)") +def benchmark_extreme_router() -> None: router = Router() for i in range(100): @@ -78,4 +77,4 @@ def benchmark_extreme_router() -> float: pass input_cmd = InputCommand.parse('cmd50 -f50_0 -f50_1 -f50_2') - return get_time_of_finds_appropriate_handler(router, input_cmd) + router.finds_appropriate_handler(input_cmd) diff --git a/metrics/benchmarks/core/most_similar_command.py b/metrics/benchmarks/most_similar_command.py similarity index 52% rename from metrics/benchmarks/core/most_similar_command.py rename to metrics/benchmarks/most_similar_command.py index ffe513c..38a8950 100644 --- a/metrics/benchmarks/core/most_similar_command.py +++ b/metrics/benchmarks/most_similar_command.py @@ -14,8 +14,7 @@ from argenta.command.models import Command from argenta.response import Response from argenta.router import Router -from ..models import benchmark -from ..utils import get_time_of_most_similar_command +from .entity import benchmarks def setup_app_with_commands(command_count: int, aliases_per_command: int = 0) -> App: @@ -35,31 +34,31 @@ def setup_app_with_commands(command_count: int, aliases_per_command: int = 0) -> return app -@benchmark(type_="most_similar_command", description="Few commands (10 commands, no match)") -def benchmark_few_commands() -> float: +@benchmarks.register(type_="most_similar_command", description="Few commands (10 commands, no match)") +def benchmark_few_commands() -> None: app = setup_app_with_commands(10) - return get_time_of_most_similar_command(app, "unknown") + app._most_similar_command("unknown") -@benchmark(type_="most_similar_command", description="Many commands (50 commands, no match)") -def benchmark_many_commands() -> float: +@benchmarks.register(type_="most_similar_command", description="Many commands (50 commands, no match)") +def benchmark_many_commands() -> None: app = setup_app_with_commands(50) - return get_time_of_most_similar_command(app, "unknown") + app._most_similar_command("unknown") -@benchmark(type_="most_similar_command", description="Many aliases (20 commands, 10 aliases each)") -def benchmark_many_aliases() -> float: +@benchmarks.register(type_="most_similar_command", description="Many aliases (20 commands, 10 aliases each)") +def benchmark_many_aliases() -> None: app = setup_app_with_commands(20, aliases_per_command=10) - return get_time_of_most_similar_command(app, "unknown") + app._most_similar_command("unknown") -@benchmark(type_="most_similar_command", description="Partial match (50 commands, prefix match)") -def benchmark_partial_match() -> float: +@benchmarks.register(type_="most_similar_command", description="Partial match (50 commands, prefix match)") +def benchmark_partial_match() -> None: app = setup_app_with_commands(50) - return get_time_of_most_similar_command(app, "comm") + app._most_similar_command("comm") -@benchmark(type_="most_similar_command", description="Extreme (100 commands, 20 aliases each)") -def benchmark_extreme_commands() -> float: +@benchmarks.register(type_="most_similar_command", description="Extreme (100 commands, 20 aliases each)") +def benchmark_extreme_commands() -> None: app = setup_app_with_commands(100, aliases_per_command=20) - return get_time_of_most_similar_command(app, "comm") + app._most_similar_command("comm") diff --git a/metrics/benchmarks/core/pre_cycle_setup.py b/metrics/benchmarks/pre_cycle_setup.py similarity index 72% rename from metrics/benchmarks/core/pre_cycle_setup.py rename to metrics/benchmarks/pre_cycle_setup.py index d56cb95..9c3c621 100644 --- a/metrics/benchmarks/core/pre_cycle_setup.py +++ b/metrics/benchmarks/pre_cycle_setup.py @@ -11,12 +11,11 @@ from argenta.command.models import Command from argenta.response import Response from argenta.router import Router -from ..models import benchmark -from ..utils import get_time_of_pre_cycle_setup +from .entity import benchmarks -@benchmark(type_="pre_cycle_setup", description="With no aliases") -def benchmark_no_aliases() -> float: +@benchmarks.register(type_="pre_cycle_setup", description="With no aliases") +def benchmark_no_aliases() -> None: app = App(override_system_messages=True) router = Router() @@ -33,12 +32,11 @@ def benchmark_no_aliases() -> float: pass app.include_router(router) - execution_time = get_time_of_pre_cycle_setup(app) - return execution_time + app._pre_cycle_setup() -@benchmark(type_="pre_cycle_setup", description="With few aliases (6 total)") -def benchmark_few_aliases() -> float: +@benchmarks.register(type_="pre_cycle_setup", description="With few aliases (6 total)") +def benchmark_few_aliases() -> None: app = App(override_system_messages=True) router = Router() @@ -55,12 +53,11 @@ def benchmark_few_aliases() -> float: pass app.include_router(router) - execution_time = get_time_of_pre_cycle_setup(app) - return execution_time + app._pre_cycle_setup() -@benchmark(type_="pre_cycle_setup", description="With many aliases (15 total)") -def benchmark_many_aliases() -> float: +@benchmarks.register(type_="pre_cycle_setup", description="With many aliases (15 total)") +def benchmark_many_aliases() -> None: app = App(override_system_messages=True) router = Router() @@ -77,12 +74,11 @@ def benchmark_many_aliases() -> float: pass app.include_router(router) - execution_time = get_time_of_pre_cycle_setup(app) - return execution_time + app._pre_cycle_setup() -@benchmark(type_="pre_cycle_setup", description="With very many aliases (60 total)") -def benchmark_very_many_aliases() -> float: +@benchmarks.register(type_="pre_cycle_setup", description="With very many aliases (60 total)") +def benchmark_very_many_aliases() -> None: app = App(override_system_messages=True) router = Router() @@ -99,12 +95,11 @@ def benchmark_very_many_aliases() -> float: pass app.include_router(router) - execution_time = get_time_of_pre_cycle_setup(app) - return execution_time + app._pre_cycle_setup() -@benchmark(type_="pre_cycle_setup", description="With extreme aliases (300 total)") -def benchmark_extreme_aliases() -> float: +@benchmarks.register(type_="pre_cycle_setup", description="With extreme aliases (300 total)") +def benchmark_extreme_aliases() -> None: app = App(override_system_messages=True) router = Router() @@ -121,5 +116,4 @@ def benchmark_extreme_aliases() -> float: pass app.include_router(router) - execution_time = get_time_of_pre_cycle_setup(app) - return execution_time + app._pre_cycle_setup() diff --git a/metrics/benchmarks/utils.py b/metrics/benchmarks/utils.py deleted file mode 100644 index 95af193..0000000 --- a/metrics/benchmarks/utils.py +++ /dev/null @@ -1,110 +0,0 @@ -__all__ = [ - "get_time_of_pre_cycle_setup", - "get_time_of_validate_routers_for_collisions", - "get_time_of_most_similar_command", - "get_time_of_finds_appropriate_handler", - "get_kernel_version", - "get_gpu_info" -] - -import io -import os -import platform -import sys -import time -from concurrent.futures import ProcessPoolExecutor -from contextlib import redirect_stdout -from decimal import ROUND_HALF_UP, Decimal - -import pynvml - -from argenta import App -from argenta.router import Router -from argenta.command.models import InputCommand -from .models import Benchmark, BenchmarkResult, Benchmarks - - -def get_time_of_pre_cycle_setup(app: App) -> float: - start = time.perf_counter() - with redirect_stdout(io.StringIO()): - app._pre_cycle_setup() # pyright: ignore[reportPrivateUsage] - end = time.perf_counter() - return (end - start) * 1000 # as milliseconds - -def get_time_of_validate_routers_for_collisions(app: App) -> float: - app._setup_system_router() # pyright: ignore[reportPrivateUsage] - start = time.perf_counter() - with redirect_stdout(io.StringIO()): - app._validate_routers_for_collisions() # pyright: ignore[reportPrivateUsage] - end = time.perf_counter() - return (end - start) * 1000 - - -def get_time_of_most_similar_command(app: App, unknown_command: str) -> float: - start = time.perf_counter() - with redirect_stdout(io.StringIO()): - app._most_similar_command(unknown_command) # pyright: ignore[reportPrivateUsage] - end = time.perf_counter() - return (end - start) * 1000 - - -def get_time_of_finds_appropriate_handler(router: Router, input_command: InputCommand) -> float: - start = time.perf_counter() - with redirect_stdout(io.StringIO()): - router.finds_appropriate_handler(input_command) - end = time.perf_counter() - return (end - start) * 1000 - - -def get_kernel_version() -> dict[str, str]: - system = platform.system() - - if system == "Windows": - ver = sys.getwindowsversion() - kernel_version = f"{ver.major}.{ver.minor}.{ver.build}" - - if ver.build >= 22000: - product_name = "Windows 11" - else: - product_name = "Windows 10" - - return { - 'kernel_version': kernel_version, - 'product_name': product_name - } - - elif system == "Linux": - return { - 'kernel_version': platform.release(), - 'product_name': platform.system() - } - - elif system == "Darwin": - return { - 'kernel_version': platform.release(), - 'product_name': f"macOS {platform.mac_ver()[0]}" - } - else: - return { - 'kernel_version': platform.release(), - 'product_name': platform.system(), - } - -def get_gpu_info() -> str: - try: - pynvml.nvmlInit() - device_count = pynvml.nvmlDeviceGetCount() - if device_count == 0: - return "N/A" - - handle = pynvml.nvmlDeviceGetHandleByIndex(0) - name = pynvml.nvmlDeviceGetName(handle) - - if isinstance(name, bytes): - name = name.decode("utf-8") - - pynvml.nvmlShutdown() - return name - except pynvml.NVMLError: - return "N/A" - diff --git a/metrics/benchmarks/core/validate_routers_for_collisions.py b/metrics/benchmarks/validate_routers_for_collisions.py similarity index 58% rename from metrics/benchmarks/core/validate_routers_for_collisions.py rename to metrics/benchmarks/validate_routers_for_collisions.py index 65c8c1d..60e9aa7 100644 --- a/metrics/benchmarks/core/validate_routers_for_collisions.py +++ b/metrics/benchmarks/validate_routers_for_collisions.py @@ -11,13 +11,11 @@ from argenta.command.models import Command from argenta.response import Response from argenta.router import Router -from ..utils import get_time_of_validate_routers_for_collisions - -from ..models import benchmark +from .entity import benchmarks -@benchmark(type_="validate_routers_for_collisions", description="With few routers (3 routers, 1 command each)") -def benchmark_few_routers() -> float: +@benchmarks.register(type_="validate_routers_for_collisions", description="With few routers (3 routers, 1 command each)") +def benchmark_few_routers() -> None: app = App(override_system_messages=True) for i in range(3): @@ -29,11 +27,12 @@ def benchmark_few_routers() -> float: app.include_router(router) - return get_time_of_validate_routers_for_collisions(app) + app._setup_system_router() + app._validate_routers_for_collisions() -@benchmark(type_="validate_routers_for_collisions", description="With many routers (10 routers, 1 command each)") -def benchmark_many_routers() -> float: +@benchmarks.register(type_="validate_routers_for_collisions", description="With many routers (10 routers, 1 command each)") +def benchmark_many_routers() -> None: app = App(override_system_messages=True) for i in range(10): @@ -45,11 +44,12 @@ def benchmark_many_routers() -> float: app.include_router(router) - return get_time_of_validate_routers_for_collisions(app) + app._setup_system_router() + app._validate_routers_for_collisions() -@benchmark(type_="validate_routers_for_collisions", description="With many commands per router (3 routers, 10 commands each)") -def benchmark_many_commands_per_router() -> float: +@benchmarks.register(type_="validate_routers_for_collisions", description="With many commands per router (3 routers, 10 commands each)") +def benchmark_many_commands_per_router() -> None: app = App(override_system_messages=True) for i in range(3): @@ -62,11 +62,12 @@ def benchmark_many_commands_per_router() -> float: app.include_router(router) - return get_time_of_validate_routers_for_collisions(app) + app._setup_system_router() + app._validate_routers_for_collisions() -@benchmark(type_="validate_routers_for_collisions", description="With many aliases (3 routers, 5 commands, 10 aliases each)") -def benchmark_many_aliases_per_command() -> float: +@benchmarks.register(type_="validate_routers_for_collisions", description="With many aliases (3 routers, 5 commands, 10 aliases each)") +def benchmark_many_aliases_per_command() -> None: app = App(override_system_messages=True) for i in range(3): @@ -79,11 +80,12 @@ def benchmark_many_aliases_per_command() -> float: app.include_router(router) - return get_time_of_validate_routers_for_collisions(app) + app._setup_system_router() + app._validate_routers_for_collisions() -@benchmark(type_="validate_routers_for_collisions", description="Extreme (20 routers, 10 commands, 20 aliases each)") -def benchmark_extreme_routers() -> float: +@benchmarks.register(type_="validate_routers_for_collisions", description="Extreme (20 routers, 10 commands, 20 aliases each)") +def benchmark_extreme_routers() -> None: app = App(override_system_messages=True) for i in range(20): @@ -96,4 +98,5 @@ def benchmark_extreme_routers() -> float: app.include_router(router) - return get_time_of_validate_routers_for_collisions(app) + app._setup_system_router() + app._validate_routers_for_collisions() diff --git a/metrics/handlers.py b/metrics/handlers.py index 9516bc0..6ab2484 100644 --- a/metrics/handlers.py +++ b/metrics/handlers.py @@ -9,8 +9,9 @@ from rich.text import Text from argenta.command.models import Command from argenta.response import Response from argenta.router import Router -from .benchmarks.models import BenchmarkResult -from .benchmarks.utils import run_all_benchmarks, get_kernel_version, get_gpu_info +from .benchmarks.core.models import BenchmarkGroupResult +from .benchmarks.entity import benchmarks as registered_benchmarks +from .utils import get_kernel_version, get_gpu_info console = Console() router = Router(title="Metrics commands:") @@ -39,10 +40,10 @@ def all_print_handler(_: Response) -> None: console.print(Panel(header_text, expand=False, border_style="magenta")) console.print(table, end="\n\n") - type_paired_benchmarks: dict[str, list[BenchmarkResult]] = run_all_benchmarks() + type_grouped_benchmarks: list[BenchmarkGroupResult] = registered_benchmarks.run_benchmarks_grouped_by_type() - for type_, benchmarks in type_paired_benchmarks.items(): - header_text = Text(f"TYPE: {type_.upper()}", style="bold magenta") + for results in type_grouped_benchmarks: + header_text = Text(f"TYPE: {results.type_.upper()}", style="bold magenta") console.print(Panel(header_text, expand=False, border_style="magenta")) table = Table(show_header=True, header_style="bold cyan", border_style="blue", show_lines=True) @@ -50,13 +51,17 @@ def all_print_handler(_: Response) -> None: table.add_column("Description", style="dim") table.add_column("Iterations", justify="right") table.add_column("Avg Time (ms)", justify="right", style="bold yellow") + table.add_column("Median Time (ms)", justify="right", style="bold yellow") + table.add_column("Stdev (ms)", justify="right", style="bold yellow") - for benchmark in benchmarks: + for benchmark in results.benchmark_results: table.add_row( benchmark.name, benchmark.description, str(benchmark.iterations), - str(benchmark.avg_time) + str(benchmark.avg_time), + str(benchmark.median_time), + str(benchmark.std_dev), ) console.print(table) diff --git a/metrics/utils.py b/metrics/utils.py new file mode 100644 index 0000000..ba951e3 --- /dev/null +++ b/metrics/utils.py @@ -0,0 +1,63 @@ +__all__ = [ + "get_kernel_version", + "get_gpu_info" +] + +import platform +import sys + +import pynvml + + +def get_kernel_version() -> dict[str, str]: + system = platform.system() + + if system == "Windows": + ver = sys.getwindowsversion() + kernel_version = f"{ver.major}.{ver.minor}.{ver.build}" + + if ver.build >= 22000: + product_name = "Windows 11" + else: + product_name = "Windows 10" + + return { + 'kernel_version': kernel_version, + 'product_name': product_name + } + + elif system == "Linux": + return { + 'kernel_version': platform.release(), + 'product_name': platform.system() + } + + elif system == "Darwin": + return { + 'kernel_version': platform.release(), + 'product_name': f"macOS {platform.mac_ver()[0]}" + } + else: + return { + 'kernel_version': platform.release(), + 'product_name': platform.system(), + } + +def get_gpu_info() -> str: + try: + pynvml.nvmlInit() + device_count = pynvml.nvmlDeviceGetCount() + if device_count == 0: + return "N/A" + + handle = pynvml.nvmlDeviceGetHandleByIndex(0) + name = pynvml.nvmlDeviceGetName(handle) + + if isinstance(name, bytes): + name = name.decode("utf-8") + + pynvml.nvmlShutdown() + return name + except pynvml.NVMLError: + return "N/A" + From ba9a7b5539bbf65577176b4dcdfeb1d1764acfcc Mon Sep 17 00:00:00 2001 From: kolo Date: Sun, 18 Jan 2026 02:30:13 +0300 Subject: [PATCH 05/31] bbbbbbenchh --- metrics/benchmarks/most_similar_command.py | 2 -- metrics/benchmarks/pre_cycle_setup.py | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/metrics/benchmarks/most_similar_command.py b/metrics/benchmarks/most_similar_command.py index 38a8950..7455999 100644 --- a/metrics/benchmarks/most_similar_command.py +++ b/metrics/benchmarks/most_similar_command.py @@ -29,8 +29,6 @@ def setup_app_with_commands(command_count: int, aliases_per_command: int = 0) -> pass app.include_router(router) - with redirect_stdout(io.StringIO()): - app._pre_cycle_setup() # pyright: ignore[reportPrivateUsage] return app diff --git a/metrics/benchmarks/pre_cycle_setup.py b/metrics/benchmarks/pre_cycle_setup.py index 9c3c621..03c3676 100644 --- a/metrics/benchmarks/pre_cycle_setup.py +++ b/metrics/benchmarks/pre_cycle_setup.py @@ -1,6 +1,6 @@ __all__ = [ "benchmark_no_aliases", - "benchmark_many_aliases", + "benchmark_with_many_aliases", "benchmark_few_aliases", "benchmark_extreme_aliases", "benchmark_very_many_aliases" @@ -57,7 +57,7 @@ def benchmark_few_aliases() -> None: @benchmarks.register(type_="pre_cycle_setup", description="With many aliases (15 total)") -def benchmark_many_aliases() -> None: +def benchmark_with_many_aliases() -> None: app = App(override_system_messages=True) router = Router() From e7d064908fb60b241486fa36c727e7d69dfd0839 Mon Sep 17 00:00:00 2001 From: kolo Date: Sun, 18 Jan 2026 18:41:03 +0300 Subject: [PATCH 06/31] bench --- metrics/__main__.py | 2 +- metrics/benchmarks/core/exceptions.py | 6 ++ metrics/benchmarks/core/models.py | 12 ++- metrics/benchmarks/most_similar_command.py | 7 +- metrics/handlers.py | 13 +-- metrics/services/__init__.py | 0 metrics/services/report_generator.py | 0 metrics/services/system_info_reader.py | 114 +++++++++++++++++++++ metrics/utils.py | 63 ------------ pyproject.toml | 2 +- uv.lock | 41 ++++++-- 11 files changed, 167 insertions(+), 93 deletions(-) create mode 100644 metrics/services/__init__.py create mode 100644 metrics/services/report_generator.py create mode 100644 metrics/services/system_info_reader.py delete mode 100644 metrics/utils.py diff --git a/metrics/__main__.py b/metrics/__main__.py index a775226..78f64ea 100644 --- a/metrics/__main__.py +++ b/metrics/__main__.py @@ -3,7 +3,7 @@ from argenta.app import StaticDividingLine from .handlers import router -app = App(initial_message="metrics", prompt=">>> ", dividing_line=StaticDividingLine('~', length=120)) +app = App(initial_message="metrics", prompt=">>> ", dividing_line=StaticDividingLine('~', length=70)) orchestrator = Orchestrator() diff --git a/metrics/benchmarks/core/exceptions.py b/metrics/benchmarks/core/exceptions.py index 497ee02..b849974 100644 --- a/metrics/benchmarks/core/exceptions.py +++ b/metrics/benchmarks/core/exceptions.py @@ -12,3 +12,9 @@ class BenchmarksNotFound(Exception): def __str__(self): return f"Benchmarks with type '{self.type_}' not found" + +class BenchmarksWithSameNameAlreadyExists(Exception): + def __init__(self, benchmark_name: str): + self.benchmark_name = benchmark_name + def __str__(self): + return f"Benchmarks with name '{self.benchmark_name}' already exists" diff --git a/metrics/benchmarks/core/models.py b/metrics/benchmarks/core/models.py index 4be6969..386dea1 100644 --- a/metrics/benchmarks/core/models.py +++ b/metrics/benchmarks/core/models.py @@ -13,8 +13,7 @@ import gc import statistics from typing import Callable, override -from .exceptions import BenchmarkNotFound, BenchmarksNotFound - +from .exceptions import BenchmarkNotFound, BenchmarksNotFound, BenchmarksWithSameNameAlreadyExists FuncForBenchmark = Callable[[], None] MILLISECONDS_IN_SECONDS = 1000 @@ -35,6 +34,8 @@ class BenchmarkResult: @dataclass(frozen=True, slots=True) class BenchmarkGroupResult: type_: str + iterations: int + is_gc_disabled: bool benchmark_results: list[BenchmarkResult] @@ -107,8 +108,11 @@ class Benchmarks: name=func.__name__, description=description or f'description for {func.__name__} with type {type_}', ) - self._benchmarks.append(benchmark) + if self._benchmarks_paired_by_name.get(func.__name__): + raise BenchmarksWithSameNameAlreadyExists(func.__name__) + self._benchmarks_paired_by_name[func.__name__] = benchmark + self._benchmarks.append(benchmark) self._benchmarks_grouped_by_type.setdefault(type_, []).append(benchmark) return func return decorator @@ -145,6 +149,8 @@ class Benchmarks: return BenchmarkGroupResult( type_=type_, + iterations=iterations, + is_gc_disabled=is_gc_disabled, benchmark_results=benchmark_results ) diff --git a/metrics/benchmarks/most_similar_command.py b/metrics/benchmarks/most_similar_command.py index 7455999..bcc1d3b 100644 --- a/metrics/benchmarks/most_similar_command.py +++ b/metrics/benchmarks/most_similar_command.py @@ -1,14 +1,11 @@ __all__ = [ "benchmark_few_commands", - "benchmark_many_commands", + "benchmark_many_commands_most_similar", "benchmark_many_aliases", "benchmark_partial_match", "benchmark_extreme_commands" ] -import io -from contextlib import redirect_stdout - from argenta import App from argenta.command.models import Command from argenta.response import Response @@ -39,7 +36,7 @@ def benchmark_few_commands() -> None: @benchmarks.register(type_="most_similar_command", description="Many commands (50 commands, no match)") -def benchmark_many_commands() -> None: +def benchmark_many_commands_most_similar() -> None: app = setup_app_with_commands(50) app._most_similar_command("unknown") diff --git a/metrics/handlers.py b/metrics/handlers.py index 6ab2484..108bb65 100644 --- a/metrics/handlers.py +++ b/metrics/handlers.py @@ -11,7 +11,6 @@ from argenta.response import Response from argenta.router import Router from .benchmarks.core.models import BenchmarkGroupResult from .benchmarks.entity import benchmarks as registered_benchmarks -from .utils import get_kernel_version, get_gpu_info console = Console() router = Router(title="Metrics commands:") @@ -43,22 +42,18 @@ def all_print_handler(_: Response) -> None: type_grouped_benchmarks: list[BenchmarkGroupResult] = registered_benchmarks.run_benchmarks_grouped_by_type() for results in type_grouped_benchmarks: - header_text = Text(f"TYPE: {results.type_.upper()}", style="bold magenta") + header_text = Text(f"TYPE: {results.type_.upper()} ; ITERATIONS: {results.iterations} ; ALL TIME IN MS", style="bold magenta") console.print(Panel(header_text, expand=False, border_style="magenta")) table = Table(show_header=True, header_style="bold cyan", border_style="blue", show_lines=True) - table.add_column("Name", style="green") table.add_column("Description", style="dim") - table.add_column("Iterations", justify="right") - table.add_column("Avg Time (ms)", justify="right", style="bold yellow") - table.add_column("Median Time (ms)", justify="right", style="bold yellow") - table.add_column("Stdev (ms)", 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("Stdev", justify="right", style="bold yellow") for benchmark in results.benchmark_results: table.add_row( - benchmark.name, benchmark.description, - str(benchmark.iterations), str(benchmark.avg_time), str(benchmark.median_time), str(benchmark.std_dev), diff --git a/metrics/services/__init__.py b/metrics/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/metrics/services/report_generator.py b/metrics/services/report_generator.py new file mode 100644 index 0000000..e69de29 diff --git a/metrics/services/system_info_reader.py b/metrics/services/system_info_reader.py new file mode 100644 index 0000000..47d17e6 --- /dev/null +++ b/metrics/services/system_info_reader.py @@ -0,0 +1,114 @@ +__all__ = [ + "SystemInfo", + "SystemInfoGetter", + "get_system_info" +] + +from dataclasses import dataclass +import platform +import sys +from typing import Protocol + +import cpuinfo +import psutil + + +@dataclass(frozen=True, slots=True) +class SystemInfo: + os_info: OSInfo + cpu_info: CPUInfo + memory_info: MemoryInfo + python_version: str + python_implementation: str + + +@dataclass(frozen=True, slots=True) +class OSInfo: + os_name: str + kernel_version: str + + +@dataclass(frozen=True, slots=True) +class CPUInfo: + cpu_name: str + cpu_architecture: str + cpu_physical_cores: int + cpu_logical_cores: int + cpu_max_frequency: float + cpu_base_frequency: float + + +@dataclass(frozen=True, slots=True) +class MemoryInfo: + total_ram: float # in GB + available_ram: float # in GB + l1_cache: float + l2_cache: float + l3_cache: float + + +@dataclass(frozen=True, slots=True) +class PythonInfo: + python_version: str + python_implementation: str + python_compiler: str + + +class SystemInfoGetter(Protocol): + def __call__(self) -> SystemInfo: + raise NotImplementedError + + +def get_system_info() -> SystemInfo: + os_info = get_os_info() + os_name = os_info.os_name + os_kernel_version = os_info.kernel_version + + cpu_info = cpuinfo.get_cpu_info() + cpu_architecture = cpu_info["arch"] + cpu_name = cpu_info["brand_raw"] + + gpu_name = get_gpu_name() + + total_ram = psutil.virtual_memory().total / (1024 ** 3) + + python_version = platform.python_version() + python_implementation = platform.python_implementation() + + return SystemInfo( + os_name=os_name, + kernel_version=os_kernel_version, + cpu_architecture=cpu_architecture, + cpu_name=cpu_name, + gpu_name=gpu_name, + total_ram=total_ram, + python_version=python_version, + python_implementation=python_implementation, + ) + +def get_os_info() -> OSInfo: + system = platform.system() + + if system == "Windows": + ver = sys.getwindowsversion() + kernel_version = f"{ver.major}.{ver.minor}.{ver.build}" + + if ver.build >= 22000: + product_name = "Windows 11" + else: + product_name = "Windows 10" + + return OSInfo( + os_name=product_name, + kernel_version=kernel_version, + ) + elif system == "Darwin": + return OSInfo( + kernel_version=platform.release(), + os_name=f"macOS {platform.mac_ver()[0]}" + ) + else: + return OSInfo( + kernel_version=platform.release(), + os_name=platform.system() + ) diff --git a/metrics/utils.py b/metrics/utils.py deleted file mode 100644 index ba951e3..0000000 --- a/metrics/utils.py +++ /dev/null @@ -1,63 +0,0 @@ -__all__ = [ - "get_kernel_version", - "get_gpu_info" -] - -import platform -import sys - -import pynvml - - -def get_kernel_version() -> dict[str, str]: - system = platform.system() - - if system == "Windows": - ver = sys.getwindowsversion() - kernel_version = f"{ver.major}.{ver.minor}.{ver.build}" - - if ver.build >= 22000: - product_name = "Windows 11" - else: - product_name = "Windows 10" - - return { - 'kernel_version': kernel_version, - 'product_name': product_name - } - - elif system == "Linux": - return { - 'kernel_version': platform.release(), - 'product_name': platform.system() - } - - elif system == "Darwin": - return { - 'kernel_version': platform.release(), - 'product_name': f"macOS {platform.mac_ver()[0]}" - } - else: - return { - 'kernel_version': platform.release(), - 'product_name': platform.system(), - } - -def get_gpu_info() -> str: - try: - pynvml.nvmlInit() - device_count = pynvml.nvmlDeviceGetCount() - if device_count == 0: - return "N/A" - - handle = pynvml.nvmlDeviceGetHandleByIndex(0) - name = pynvml.nvmlDeviceGetName(handle) - - if isinstance(name, bytes): - name = name.decode("utf-8") - - pynvml.nvmlShutdown() - return name - except pynvml.NVMLError: - return "N/A" - diff --git a/pyproject.toml b/pyproject.toml index d92d3b9..ed0413d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ tests = [ "pytest-mock>=3.15.1", ] metrics = [ - "nvidia-ml-py>=13.590.44", + "psutil>=7.2.1", "py-cpuinfo>=9.0.0", ] diff --git a/uv.lock b/uv.lock index 5fceb6e..63462f5 100644 --- a/uv.lock +++ b/uv.lock @@ -62,7 +62,7 @@ linters = [ { name = "wemake-python-styleguide" }, ] metrics = [ - { name = "nvidia-ml-py" }, + { name = "psutil" }, { name = "py-cpuinfo" }, ] tests = [ @@ -97,7 +97,7 @@ linters = [ { name = "wemake-python-styleguide", specifier = ">=0.17.0" }, ] metrics = [ - { name = "nvidia-ml-py", specifier = ">=13.590.44" }, + { name = "psutil", specifier = ">=7.2.1" }, { name = "py-cpuinfo", specifier = ">=9.0.0" }, ] tests = [ @@ -561,15 +561,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] -[[package]] -name = "nvidia-ml-py" -version = "13.590.44" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1b/23/3871537f204aee823c574ba25cbeb08cae779979d4d43c01adddda00bab9/nvidia_ml_py-13.590.44.tar.gz", hash = "sha256:b358c7614b0fdeea4b95f046f1c90123bfe25d148ab93bb1c00248b834703373", size = 49737, upload-time = "2025-12-08T14:41:10.872Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/47/4c822bd37a008e72fd5a0eae33524ae3ac97b13f7030f63bae1728b8957e/nvidia_ml_py-13.590.44-py3-none-any.whl", hash = "sha256:18feb54eca7d0e3cdc8d1a040a771eda72d9ec3148e5443087970dbfd7377ecc", size = 50683, upload-time = "2025-12-08T14:41:09.597Z" }, -] - [[package]] name = "packaging" version = "25.0" @@ -606,6 +597,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "psutil" +version = "7.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/73/cb/09e5184fb5fc0358d110fc3ca7f6b1d033800734d34cac10f4136cfac10e/psutil-7.2.1.tar.gz", hash = "sha256:f7583aec590485b43ca601dd9cea0dcd65bd7bb21d30ef4ddbf4ea6b5ed1bdd3", size = 490253, upload-time = "2025-12-29T08:26:00.169Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/8e/f0c242053a368c2aa89584ecd1b054a18683f13d6e5a318fc9ec36582c94/psutil-7.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ba9f33bb525b14c3ea563b2fd521a84d2fa214ec59e3e6a2858f78d0844dd60d", size = 129624, upload-time = "2025-12-29T08:26:04.255Z" }, + { url = "https://files.pythonhosted.org/packages/26/97/a58a4968f8990617decee234258a2b4fc7cd9e35668387646c1963e69f26/psutil-7.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:81442dac7abfc2f4f4385ea9e12ddf5a796721c0f6133260687fec5c3780fa49", size = 130132, upload-time = "2025-12-29T08:26:06.228Z" }, + { url = "https://files.pythonhosted.org/packages/db/6d/ed44901e830739af5f72a85fa7ec5ff1edea7f81bfbf4875e409007149bd/psutil-7.2.1-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea46c0d060491051d39f0d2cff4f98d5c72b288289f57a21556cc7d504db37fc", size = 180612, upload-time = "2025-12-29T08:26:08.276Z" }, + { url = "https://files.pythonhosted.org/packages/c7/65/b628f8459bca4efbfae50d4bf3feaab803de9a160b9d5f3bd9295a33f0c2/psutil-7.2.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35630d5af80d5d0d49cfc4d64c1c13838baf6717a13effb35869a5919b854cdf", size = 183201, upload-time = "2025-12-29T08:26:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/fb/23/851cadc9764edcc18f0effe7d0bf69f727d4cf2442deb4a9f78d4e4f30f2/psutil-7.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:923f8653416604e356073e6e0bccbe7c09990acef442def2f5640dd0faa9689f", size = 139081, upload-time = "2025-12-29T08:26:12.483Z" }, + { url = "https://files.pythonhosted.org/packages/59/82/d63e8494ec5758029f31c6cb06d7d161175d8281e91d011a4a441c8a43b5/psutil-7.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cfbe6b40ca48019a51827f20d830887b3107a74a79b01ceb8cc8de4ccb17b672", size = 134767, upload-time = "2025-12-29T08:26:14.528Z" }, + { url = "https://files.pythonhosted.org/packages/05/c2/5fb764bd61e40e1fe756a44bd4c21827228394c17414ade348e28f83cd79/psutil-7.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:494c513ccc53225ae23eec7fe6e1482f1b8a44674241b54561f755a898650679", size = 129716, upload-time = "2025-12-29T08:26:16.017Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d2/935039c20e06f615d9ca6ca0ab756cf8408a19d298ffaa08666bc18dc805/psutil-7.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fce5f92c22b00cdefd1645aa58ab4877a01679e901555067b1bd77039aa589f", size = 130133, upload-time = "2025-12-29T08:26:18.009Z" }, + { url = "https://files.pythonhosted.org/packages/77/69/19f1eb0e01d24c2b3eacbc2f78d3b5add8a89bf0bb69465bc8d563cc33de/psutil-7.2.1-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93f3f7b0bb07711b49626e7940d6fe52aa9940ad86e8f7e74842e73189712129", size = 181518, upload-time = "2025-12-29T08:26:20.241Z" }, + { url = "https://files.pythonhosted.org/packages/e1/6d/7e18b1b4fa13ad370787626c95887b027656ad4829c156bb6569d02f3262/psutil-7.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d34d2ca888208eea2b5c68186841336a7f5e0b990edec929be909353a202768a", size = 184348, upload-time = "2025-12-29T08:26:22.215Z" }, + { url = "https://files.pythonhosted.org/packages/98/60/1672114392dd879586d60dd97896325df47d9a130ac7401318005aab28ec/psutil-7.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2ceae842a78d1603753561132d5ad1b2f8a7979cb0c283f5b52fb4e6e14b1a79", size = 140400, upload-time = "2025-12-29T08:26:23.993Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7b/d0e9d4513c46e46897b46bcfc410d51fc65735837ea57a25170f298326e6/psutil-7.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:08a2f175e48a898c8eb8eace45ce01777f4785bc744c90aa2cc7f2fa5462a266", size = 135430, upload-time = "2025-12-29T08:26:25.999Z" }, + { url = "https://files.pythonhosted.org/packages/c5/cf/5180eb8c8bdf6a503c6919f1da28328bd1e6b3b1b5b9d5b01ae64f019616/psutil-7.2.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b2e953fcfaedcfbc952b44744f22d16575d3aa78eb4f51ae74165b4e96e55f42", size = 128137, upload-time = "2025-12-29T08:26:27.759Z" }, + { url = "https://files.pythonhosted.org/packages/c5/2c/78e4a789306a92ade5000da4f5de3255202c534acdadc3aac7b5458fadef/psutil-7.2.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:05cc68dbb8c174828624062e73078e7e35406f4ca2d0866c272c2410d8ef06d1", size = 128947, upload-time = "2025-12-29T08:26:29.548Z" }, + { url = "https://files.pythonhosted.org/packages/29/f8/40e01c350ad9a2b3cb4e6adbcc8a83b17ee50dd5792102b6142385937db5/psutil-7.2.1-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e38404ca2bb30ed7267a46c02f06ff842e92da3bb8c5bfdadbd35a5722314d8", size = 154694, upload-time = "2025-12-29T08:26:32.147Z" }, + { url = "https://files.pythonhosted.org/packages/06/e4/b751cdf839c011a9714a783f120e6a86b7494eb70044d7d81a25a5cd295f/psutil-7.2.1-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab2b98c9fc19f13f59628d94df5cc4cc4844bc572467d113a8b517d634e362c6", size = 156136, upload-time = "2025-12-29T08:26:34.079Z" }, + { url = "https://files.pythonhosted.org/packages/44/ad/bbf6595a8134ee1e94a4487af3f132cef7fce43aef4a93b49912a48c3af7/psutil-7.2.1-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f78baafb38436d5a128f837fab2d92c276dfb48af01a240b861ae02b2413ada8", size = 148108, upload-time = "2025-12-29T08:26:36.225Z" }, + { url = "https://files.pythonhosted.org/packages/1c/15/dd6fd869753ce82ff64dcbc18356093471a5a5adf4f77ed1f805d473d859/psutil-7.2.1-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:99a4cd17a5fdd1f3d014396502daa70b5ec21bf4ffe38393e152f8e449757d67", size = 147402, upload-time = "2025-12-29T08:26:39.21Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/d9317542e3f2b180c4306e3f45d3c922d7e86d8ce39f941bb9e2e9d8599e/psutil-7.2.1-cp37-abi3-win_amd64.whl", hash = "sha256:b1b0671619343aa71c20ff9767eced0483e4fc9e1f489d50923738caf6a03c17", size = 136938, upload-time = "2025-12-29T08:26:41.036Z" }, + { url = "https://files.pythonhosted.org/packages/3e/73/2ce007f4198c80fcf2cb24c169884f833fe93fbc03d55d302627b094ee91/psutil-7.2.1-cp37-abi3-win_arm64.whl", hash = "sha256:0d67c1822c355aa6f7314d92018fb4268a76668a536f133599b91edd48759442", size = 133836, upload-time = "2025-12-29T08:26:43.086Z" }, +] + [[package]] name = "py-cpuinfo" version = "9.0.0" From 9d250fde9c566273868c9b928a2e9f7c7a8f607c Mon Sep 17 00:00:00 2001 From: kolo Date: Sun, 18 Jan 2026 23:53:52 +0300 Subject: [PATCH 07/31] bench --- metrics/handlers.py | 56 ++---------- metrics/services/report_generator.py | 73 +++++++++++++++ metrics/services/system_info_reader.py | 120 ++++++++++++++----------- mock/local_test.py | 65 ++------------ pyproject.toml | 12 +-- 5 files changed, 158 insertions(+), 168 deletions(-) diff --git a/metrics/handlers.py b/metrics/handlers.py index 108bb65..e4f50b8 100644 --- a/metrics/handlers.py +++ b/metrics/handlers.py @@ -1,16 +1,12 @@ -import platform -import cpuinfo - from rich.console import Console -from rich.panel import Panel -from rich.table import Table -from rich.text import Text from argenta.command.models import Command from argenta.response import Response from argenta.router import Router from .benchmarks.core.models import BenchmarkGroupResult from .benchmarks.entity import benchmarks as registered_benchmarks +from .services.report_generator import ReportGenerator +from .services.system_info_reader import get_system_info console = Console() router = Router(title="Metrics commands:") @@ -18,48 +14,14 @@ router = Router(title="Metrics commands:") @router.command(Command("all-print", description="Print all benchmarks results")) def all_print_handler(_: Response) -> None: - cpu_info = cpuinfo.get_cpu_info() - gpu_info = get_gpu_info() - os_info = get_kernel_version() - - table = Table(show_header=True, header_style="bold cyan", border_style="blue", show_lines=True) - table.add_column("Parameter", style="green") - table.add_column("Value", style="yellow") - - table.add_row("OS", platform.system()) - table.add_row("OS Name", os_info['product_name']) - table.add_row("OS Kernel Version", os_info['kernel_version']) - table.add_row("Architecture", cpu_info['arch']) - table.add_row("CPU", cpu_info['brand_raw']) - table.add_row("GPU", gpu_info) - table.add_row("Python Version", cpu_info['python_version']) - table.add_row("Python Implementation", platform.python_implementation()) - - header_text = Text("SYSTEM INFO", style="bold magenta") - console.print(Panel(header_text, expand=False, border_style="magenta")) - console.print(table, end="\n\n") - + report_generator = ReportGenerator(get_system_info()) + console.print(report_generator.generate_system_info_header()) + console.print(report_generator.generate_system_info_table()) + return type_grouped_benchmarks: list[BenchmarkGroupResult] = registered_benchmarks.run_benchmarks_grouped_by_type() - - for results in type_grouped_benchmarks: - header_text = Text(f"TYPE: {results.type_.upper()} ; ITERATIONS: {results.iterations} ; ALL TIME IN MS", style="bold magenta") - console.print(Panel(header_text, expand=False, border_style="magenta")) - - table = Table(show_header=True, header_style="bold cyan", border_style="blue", show_lines=True) - table.add_column("Description", style="dim") - table.add_column("Avg Time", justify="right", style="bold yellow") - table.add_column("Median Time", justify="right", style="bold yellow") - table.add_column("Stdev", justify="right", style="bold yellow") - - for benchmark in results.benchmark_results: - table.add_row( - benchmark.description, - str(benchmark.avg_time), - str(benchmark.median_time), - str(benchmark.std_dev), - ) - - console.print(table) + 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_report_table(benchmark_group_result)) @router.command(Command("release-generate", description="Generate release report")) diff --git a/metrics/services/report_generator.py b/metrics/services/report_generator.py index e69de29..4765221 100644 --- a/metrics/services/report_generator.py +++ b/metrics/services/report_generator.py @@ -0,0 +1,73 @@ +from rich.panel import Panel +from rich.table import Table +from rich.text import Text + +from ..benchmarks.core.models import BenchmarkGroupResult +from metrics.services.system_info_reader import SystemInfo + + +class ReportGenerator: + def __init__(self, system_info: SystemInfo): + self.system_info = system_info + self._cached_benchmark_tables: dict[int, Table] = {} + self._cached_system_info_table: Table | None = None + + def generate_benchmark_report_table(self, benchmark_group_result: BenchmarkGroupResult) -> Table: + if cached_result := self._cached_benchmark_tables.get(id(benchmark_group_result)): + return cached_result + + table = Table(show_header=True, header_style="bold cyan", border_style="blue", show_lines=True) + table.add_column("Description", style="dim") + table.add_column("Avg Time", justify="right", style="bold yellow") + table.add_column("Median Time", justify="right", style="bold yellow") + table.add_column("Stdev", justify="right", style="bold yellow") + + for benchmark in benchmark_group_result.benchmark_results: + table.add_row( + benchmark.description, + str(benchmark.avg_time), + str(benchmark.median_time), + str(benchmark.std_dev), + ) + self._cached_benchmark_tables[id(benchmark_group_result)] = table + return table + + @staticmethod + def generate_benchmark_table_header(benchmark_group_result: BenchmarkGroupResult) -> Panel: + header_text = Text(f"TYPE: {benchmark_group_result.type_.upper()} ; " + f"ITERATIONS: {benchmark_group_result.iterations} ; " + f"ALL TIME IN MS", + style="bold magenta") + return Panel(header_text, expand=False, border_style="magenta") + + def generate_system_info_table(self) -> Table: + if self._cached_system_info_table is not None: + return self._cached_system_info_table + + table = Table(show_header=True, header_style="bold cyan", border_style="blue", show_lines=True) + table.add_column("Parameter", style="green") + table.add_column("Value", style="yellow") + + table.add_row("OS Name", self.system_info.os_info.name) + table.add_row("OS Kernel Version", self.system_info.os_info.kernel_version) + table.add_row("Architecture", self.system_info.cpu_info.architecture) + table.add_row("CPU", self.system_info.cpu_info.name) + table.add_row("CPU Physical Cores", str(self.system_info.cpu_info.physical_cores)) + table.add_row("CPU Logical Cores", str(self.system_info.cpu_info.logical_cores)) + table.add_row("CPU Max Frequency", str(self.system_info.cpu_info.max_frequency) + ' GHz') + table.add_row("CPU Min Frequency", str(self.system_info.cpu_info.min_frequency) + ' GHz') + table.add_row("CPU Current Frequency", str(self.system_info.cpu_info.current_frequency) + ' GHz') + table.add_row("Total RAM", str(self.system_info.memory_info.total_ram) + ' GB') + table.add_row("Used RAM", str(self.system_info.memory_info.used_ram) + ' GB') + table.add_row("Available RAM", str(self.system_info.memory_info.available_ram) + ' GB') + table.add_row("Python Version", self.system_info.python_info.version) + table.add_row("Python Implementation", self.system_info.python_info.implementation) + table.add_row("Python Compiler", self.system_info.python_info.compiler) + + self._cached_system_info_table = table + return table + + @staticmethod + def generate_system_info_header() -> Panel: + header_text = Text("SYSTEM INFO", style="bold magenta") + return Panel(header_text, expand=False, border_style="magenta") \ No newline at end of file diff --git a/metrics/services/system_info_reader.py b/metrics/services/system_info_reader.py index 47d17e6..2578158 100644 --- a/metrics/services/system_info_reader.py +++ b/metrics/services/system_info_reader.py @@ -1,13 +1,11 @@ __all__ = [ "SystemInfo", - "SystemInfoGetter", "get_system_info" ] from dataclasses import dataclass import platform import sys -from typing import Protocol import cpuinfo import psutil @@ -18,72 +16,46 @@ class SystemInfo: os_info: OSInfo cpu_info: CPUInfo memory_info: MemoryInfo - python_version: str - python_implementation: str - + python_info: PythonInfo @dataclass(frozen=True, slots=True) class OSInfo: - os_name: str + name: str kernel_version: str - @dataclass(frozen=True, slots=True) class CPUInfo: - cpu_name: str - cpu_architecture: str - cpu_physical_cores: int - cpu_logical_cores: int - cpu_max_frequency: float - cpu_base_frequency: float - + name: str + architecture: str + physical_cores: int + logical_cores: int + max_frequency: float + min_frequency: float + current_frequency: float @dataclass(frozen=True, slots=True) class MemoryInfo: total_ram: float # in GB + used_ram: float # in GB available_ram: float # in GB - l1_cache: float - l2_cache: float - l3_cache: float - @dataclass(frozen=True, slots=True) class PythonInfo: - python_version: str - python_implementation: str - python_compiler: str - - -class SystemInfoGetter(Protocol): - def __call__(self) -> SystemInfo: - raise NotImplementedError + version: str + implementation: str + compiler: str def get_system_info() -> SystemInfo: os_info = get_os_info() - os_name = os_info.os_name - os_kernel_version = os_info.kernel_version - - cpu_info = cpuinfo.get_cpu_info() - cpu_architecture = cpu_info["arch"] - cpu_name = cpu_info["brand_raw"] - - gpu_name = get_gpu_name() - - total_ram = psutil.virtual_memory().total / (1024 ** 3) - - python_version = platform.python_version() - python_implementation = platform.python_implementation() - + cpu_info = get_cpu_info() + memory_info = get_memory_info() + python_info = get_python_info() return SystemInfo( - os_name=os_name, - kernel_version=os_kernel_version, - cpu_architecture=cpu_architecture, - cpu_name=cpu_name, - gpu_name=gpu_name, - total_ram=total_ram, - python_version=python_version, - python_implementation=python_implementation, + os_info=os_info, + cpu_info=cpu_info, + memory_info=memory_info, + python_info=python_info, ) def get_os_info() -> OSInfo: @@ -99,16 +71,62 @@ def get_os_info() -> OSInfo: product_name = "Windows 10" return OSInfo( - os_name=product_name, + name=product_name, kernel_version=kernel_version, ) elif system == "Darwin": return OSInfo( kernel_version=platform.release(), - os_name=f"macOS {platform.mac_ver()[0]}" + name=f"macOS {platform.mac_ver()[0]}" ) else: return OSInfo( kernel_version=platform.release(), - os_name=platform.system() + name=platform.system() ) + +def get_cpu_info() -> CPUInfo: + cpu_info = cpuinfo.get_cpu_info() + cpu_name = cpu_info["brand_raw"] + cpu_architecture = cpu_info["arch"] + cpu_physical_cores = psutil.cpu_count(logical=False) + cpu_logical_cores = psutil.cpu_count(logical=True) + + cpu_freq = psutil.cpu_freq() or "N/A" + cpu_current_frequency = cpu_freq.current + cpu_min_frequency = cpu_freq.min + cpu_max_frequency = cpu_freq.max + + return CPUInfo( + name=cpu_name, + architecture=cpu_architecture, + physical_cores=cpu_physical_cores, + logical_cores=cpu_logical_cores, + current_frequency=cpu_current_frequency, + min_frequency=cpu_min_frequency, + max_frequency=cpu_max_frequency + ) + +def get_memory_info() -> MemoryInfo: + mem = psutil.virtual_memory() + total_ram = round(mem.total / (1024**3), 2) + used_ram = round(mem.used / (1024**3), 2) + available_ram = round(mem.available / (1024**3), 2) + + return MemoryInfo( + total_ram=total_ram, + used_ram=used_ram, + available_ram=available_ram, + ) + +def get_python_info() -> PythonInfo: + python_version = platform.python_version() + python_implementation = platform.python_implementation() + python_compiler = platform.python_compiler() + return PythonInfo( + version=python_version, + implementation=python_implementation, + compiler=python_compiler + ) + + diff --git a/mock/local_test.py b/mock/local_test.py index 9e20232..5af84a4 100644 --- a/mock/local_test.py +++ b/mock/local_test.py @@ -1,61 +1,6 @@ -import math +a = BrokenPipeError() +def q(f): + print(id(f)) - -def estimate_nth_prime_upper_bound(n: int): - if n < 6: - return 15 - - log_n = math.log(n) - log_log_n = math.log(log_n) - - if n < 100: - return int(n * (log_n + log_log_n) * 1.5) - elif n < 1000: - return int(n * (log_n + log_log_n) * 1.3) - elif n >= 8009824: - return int(n * (log_n + log_log_n - 1 + 1.8 * log_log_n / log_n)) - else: - return int(n * (log_n + log_log_n - 1 + 2.0 * log_log_n / log_n)) - - -def odd_dig_primes(n: int) -> list[int]: - nums = {k: True for k in range(2, n+1)} - - for num, is_checkable in nums.items(): - if not is_checkable: - continue - - if nums[2]: - nums[2] = False - - for x in range(num * num, n, num): - nums[x] = False - - primes = len([x for x in nums.items() if x[1]]) - max_prime = max([x[0] for x in nums.items() if x[1]]) - - upper_bound = estimate_nth_prime_upper_bound(primes+1) - print(upper_bound) - nums2 = {k: True for k in range(2, upper_bound)} - - for num, is_checkable in nums2.items(): - if not is_checkable: - continue - - if nums2[2]: - nums2[2] = False - - for x in range(num * num, upper_bound, num): - nums2[x] = False - - print([x for x in nums2.items() if x[1]]) - - next_prime_after_max = [x[0] for x in nums2.items() if x[1]][-1] - - return [ - primes, - max_prime, - next_prime_after_max - ] - -print(odd_dig_primes(13)) \ No newline at end of file +print(id(a)) +q(a) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index ed0413d..46c5294 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,14 +41,6 @@ metrics = [ ] [tool.ruff] -exclude = [ - ".idea", - "venv", - ".git", - "poetry.lock", - ".__pycache__", - "tests" -] line-length=90 [tool.pyright] @@ -79,5 +71,5 @@ disable_error_code = "import-untyped" line_length=90 [build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" +requires = ["uv_build"] +build-backend = "uv_build" From a174e0d5abfaaf509dc10a02013bfb274a13f720 Mon Sep 17 00:00:00 2001 From: kolo Date: Mon, 19 Jan 2026 00:29:24 +0300 Subject: [PATCH 08/31] bench --- metrics/__main__.py | 2 +- metrics/benchmarks/core/models.py | 4 ++-- metrics/handlers.py | 13 ++++++++++--- metrics/services/report_generator.py | 3 +-- metrics/services/system_info_reader.py | 6 ------ mock/min_app/main.py | 2 +- src/argenta/app/models.py | 7 ++++--- tests/unit_tests/test_app.py | 2 +- 8 files changed, 20 insertions(+), 19 deletions(-) diff --git a/metrics/__main__.py b/metrics/__main__.py index 78f64ea..9c15a8d 100644 --- a/metrics/__main__.py +++ b/metrics/__main__.py @@ -3,7 +3,7 @@ from argenta.app import StaticDividingLine from .handlers import router -app = App(initial_message="metrics", prompt=">>> ", dividing_line=StaticDividingLine('~', length=70)) +app = App(initial_message="metrics", prompt=">>> ", dividing_line=None) orchestrator = Orchestrator() diff --git a/metrics/benchmarks/core/models.py b/metrics/benchmarks/core/models.py index 386dea1..a495bf2 100644 --- a/metrics/benchmarks/core/models.py +++ b/metrics/benchmarks/core/models.py @@ -154,10 +154,10 @@ class Benchmarks: benchmark_results=benchmark_results ) - def run_benchmarks_grouped_by_type(self) -> list[BenchmarkGroupResult]: + def run_benchmarks_grouped_by_type(self, iterations: int = 100, is_gc_disabled: bool = False) -> list[BenchmarkGroupResult]: results: list[BenchmarkGroupResult] = [] for type_, benchmarks in self._benchmarks_grouped_by_type.items(): - results.append(self.run_benchmarks_by_type(type_)) + results.append(self.run_benchmarks_by_type(type_, iterations, is_gc_disabled)) return results def get_benchmarks_by_type(self, type_: str) -> list[Benchmark]: diff --git a/metrics/handlers.py b/metrics/handlers.py index e4f50b8..80ae9bf 100644 --- a/metrics/handlers.py +++ b/metrics/handlers.py @@ -1,5 +1,6 @@ from rich.console import Console +from argenta.command import Flag, PossibleValues from argenta.command.models import Command from argenta.response import Response from argenta.router import Router @@ -12,13 +13,19 @@ console = Console() router = Router(title="Metrics commands:") -@router.command(Command("all-print", description="Print all benchmarks results")) +@router.command( + Command( + "all-print", + description="Print all benchmarks results", + flags=Flag('without-gc', possible_values=PossibleValues.NEITHER) + ) +) def all_print_handler(_: Response) -> None: report_generator = ReportGenerator(get_system_info()) console.print(report_generator.generate_system_info_header()) console.print(report_generator.generate_system_info_table()) - return - type_grouped_benchmarks: list[BenchmarkGroupResult] = registered_benchmarks.run_benchmarks_grouped_by_type() + is_gc_disabled = _.input_flags.get_flag_by_name("without-gc") + type_grouped_benchmarks: list[BenchmarkGroupResult] = registered_benchmarks.run_benchmarks_grouped_by_type(is_gc_disabled=is_gc_disabled) 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_report_table(benchmark_group_result)) diff --git a/metrics/services/report_generator.py b/metrics/services/report_generator.py index 4765221..5c383fb 100644 --- a/metrics/services/report_generator.py +++ b/metrics/services/report_generator.py @@ -36,6 +36,7 @@ class ReportGenerator: def generate_benchmark_table_header(benchmark_group_result: BenchmarkGroupResult) -> Panel: header_text = Text(f"TYPE: {benchmark_group_result.type_.upper()} ; " f"ITERATIONS: {benchmark_group_result.iterations} ; " + f"GC {"DISABLED" if benchmark_group_result.is_gc_disabled else "ENABLED"} ; " f"ALL TIME IN MS", style="bold magenta") return Panel(header_text, expand=False, border_style="magenta") @@ -55,8 +56,6 @@ class ReportGenerator: table.add_row("CPU Physical Cores", str(self.system_info.cpu_info.physical_cores)) table.add_row("CPU Logical Cores", str(self.system_info.cpu_info.logical_cores)) table.add_row("CPU Max Frequency", str(self.system_info.cpu_info.max_frequency) + ' GHz') - table.add_row("CPU Min Frequency", str(self.system_info.cpu_info.min_frequency) + ' GHz') - table.add_row("CPU Current Frequency", str(self.system_info.cpu_info.current_frequency) + ' GHz') table.add_row("Total RAM", str(self.system_info.memory_info.total_ram) + ' GB') table.add_row("Used RAM", str(self.system_info.memory_info.used_ram) + ' GB') table.add_row("Available RAM", str(self.system_info.memory_info.available_ram) + ' GB') diff --git a/metrics/services/system_info_reader.py b/metrics/services/system_info_reader.py index 2578158..4d369fe 100644 --- a/metrics/services/system_info_reader.py +++ b/metrics/services/system_info_reader.py @@ -30,8 +30,6 @@ class CPUInfo: physical_cores: int logical_cores: int max_frequency: float - min_frequency: float - current_frequency: float @dataclass(frozen=True, slots=True) class MemoryInfo: @@ -93,8 +91,6 @@ def get_cpu_info() -> CPUInfo: cpu_logical_cores = psutil.cpu_count(logical=True) cpu_freq = psutil.cpu_freq() or "N/A" - cpu_current_frequency = cpu_freq.current - cpu_min_frequency = cpu_freq.min cpu_max_frequency = cpu_freq.max return CPUInfo( @@ -102,8 +98,6 @@ def get_cpu_info() -> CPUInfo: architecture=cpu_architecture, physical_cores=cpu_physical_cores, logical_cores=cpu_logical_cores, - current_frequency=cpu_current_frequency, - min_frequency=cpu_min_frequency, max_frequency=cpu_max_frequency ) diff --git a/mock/min_app/main.py b/mock/min_app/main.py index a2b4326..87e534c 100644 --- a/mock/min_app/main.py +++ b/mock/min_app/main.py @@ -4,7 +4,7 @@ from argenta.app import DynamicDividingLine from .routers import router -app: App = App(prompt='>>> ', dividing_line=DynamicDividingLine('~')) +app: App = App(prompt='>>> ', dividing_line=None) orchestrator: Orchestrator = Orchestrator() def main() -> None: diff --git a/src/argenta/app/models.py b/src/argenta/app/models.py index 5f03f88..fd49d6a 100644 --- a/src/argenta/app/models.py +++ b/src/argenta/app/models.py @@ -42,7 +42,7 @@ class BaseApp: farewell_message: str, exit_command: Command, system_router_title: str, - dividing_line: StaticDividingLine | DynamicDividingLine, + dividing_line: StaticDividingLine | DynamicDividingLine | None, repeat_command_groups_printing: bool, override_system_messages: bool, autocompleter: AutoCompleter, @@ -198,7 +198,8 @@ class BaseApp: is_override=self._override_system_messages ) ) - + elif self._dividing_line is None: + print('\n' + text.strip("\n") + '\n') else: raise NotImplementedError @@ -408,7 +409,7 @@ class App(BaseApp): farewell_message: str = "\nSee you\n", exit_command: Command = DEFAULT_EXIT_COMMAND, system_router_title: str = "System points:", - dividing_line: AVAILABLE_DIVIDING_LINES = DEFAULT_DIVIDING_LINE, + dividing_line: AVAILABLE_DIVIDING_LINES | None = DEFAULT_DIVIDING_LINE, repeat_command_groups_printing: bool = False, override_system_messages: bool = False, autocompleter: AutoCompleter = DEFAULT_AUTOCOMPLETER, diff --git a/tests/unit_tests/test_app.py b/tests/unit_tests/test_app.py index 13b7924..df4b9cf 100644 --- a/tests/unit_tests/test_app.py +++ b/tests/unit_tests/test_app.py @@ -361,7 +361,7 @@ def test_set_exit_command_handler_stores_handler() -> None: def test_setup_default_view_formats_prompt() -> None: app = App(prompt='>>') - assert app._prompt == '[italic dim bold]>>' + assert app._prompt == '[italic dim bold]>>[/italic dim bold]' def test_setup_default_view_sets_default_unknown_command_handler() -> None: From 3fa7b17de9c36deda0b85ed1b4e248cab1fd2326 Mon Sep 17 00:00:00 2001 From: kolo Date: Thu, 22 Jan 2026 01:59:03 +0300 Subject: [PATCH 09/31] benchs --- metrics/benchmarks/core/models.py | 43 ++++++++++++++----------------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/metrics/benchmarks/core/models.py b/metrics/benchmarks/core/models.py index a495bf2..e24fcda 100644 --- a/metrics/benchmarks/core/models.py +++ b/metrics/benchmarks/core/models.py @@ -53,33 +53,28 @@ class Benchmark: self.name = name self.description = description - def single_run(self, is_gc_disabled: bool = False) -> float: - if is_gc_disabled: - was_gc_enabled = gc.isenabled() - gc.disable() - - with redirect_stdout(io.StringIO()): - start = time.perf_counter() - self.func() - end = time.perf_counter() - - if was_gc_enabled: - gc.enable() - gc.collect() - - return (end - start) * MILLISECONDS_IN_SECONDS - else: - with redirect_stdout(io.StringIO()): - start = time.perf_counter() - self.func() - end = time.perf_counter() - return (end - start) * MILLISECONDS_IN_SECONDS + def single_run(self) -> float: + with redirect_stdout(io.StringIO()): + start = time.perf_counter() + self.func() + end = time.perf_counter() + return (end - start) * MILLISECONDS_IN_SECONDS def multiple_runs(self, iterations: int, is_gc_disabled: bool = False) -> tuple[float, ...]: run_attempts: list[float] = [] - for _ in range(iterations): - run_attempts.append(self.single_run(is_gc_disabled)) - return tuple(run_attempts) + if is_gc_disabled: + was_gc_enabled = gc.isenabled() + gc.disable() + for _ in range(iterations): + run_attempts.append(self.single_run()) + if was_gc_enabled: + gc.enable() + gc.collect() + return tuple(run_attempts) + else: + for _ in range(iterations): + run_attempts.append(self.single_run()) + return tuple(run_attempts) @override def __repr__(self) -> str: From f27f7b135b529dc17595d98e318fbeb825255be3 Mon Sep 17 00:00:00 2001 From: kolo Date: Thu, 22 Jan 2026 22:02:19 +0300 Subject: [PATCH 10/31] Update documentation and code snippets --- docs/code_snippets/input_flags/snippet3.py | 3 +- docs/code_snippets/input_flags/snippet4.py | 3 +- docs/code_snippets/input_flags/snippet8.py | 2 +- .../quickstart/task_manager/handlers.py | 3 +- metrics/__main__.py | 5 +- metrics/benchmarks/__init__.py | 3 +- metrics/benchmarks/core/models.py | 3 + metrics/benchmarks/input_command_parse.py | 51 ++++++++ metrics/handlers.py | 64 +++++++++- mock/mock_app/routers.py | 2 +- src/argenta/app/models.py | 22 +--- src/argenta/command/__init__.py | 3 +- src/argenta/command/flag/__init__.py | 3 +- src/argenta/command/flag/flags/__init__.py | 2 - src/argenta/command/flag/flags/models.py | 107 ---------------- src/argenta/command/flag/models.py | 118 +++++++++++++++++- src/argenta/command/models.py | 2 +- src/argenta/response/entity.py | 2 +- src/argenta/router/entity.py | 3 +- ...t_system_handling_non_standard_behavior.py | 3 +- .../test_system_handling_normal_behavior.py | 3 +- tests/unit_tests/test_command.py | 2 +- tests/unit_tests/test_flag.py | 2 +- tests/unit_tests/test_response.py | 2 +- tests/unit_tests/test_router.py | 3 +- uv.lock | 45 +++++++ 26 files changed, 301 insertions(+), 160 deletions(-) create mode 100644 metrics/benchmarks/input_command_parse.py delete mode 100644 src/argenta/command/flag/flags/__init__.py delete mode 100644 src/argenta/command/flag/flags/models.py diff --git a/docs/code_snippets/input_flags/snippet3.py b/docs/code_snippets/input_flags/snippet3.py index e4c33e8..a122879 100644 --- a/docs/code_snippets/input_flags/snippet3.py +++ b/docs/code_snippets/input_flags/snippet3.py @@ -1,5 +1,6 @@ from argenta import Command, Response, Router -from argenta.command.flag import InputFlag, InputFlags, ValidationStatus +from argenta.command.flag import InputFlag, ValidationStatus +from argenta.command import InputFlags router = Router(title="Add Flag Example") diff --git a/docs/code_snippets/input_flags/snippet4.py b/docs/code_snippets/input_flags/snippet4.py index 2afbc64..5d143ad 100644 --- a/docs/code_snippets/input_flags/snippet4.py +++ b/docs/code_snippets/input_flags/snippet4.py @@ -1,4 +1,5 @@ -from argenta.command.flag import InputFlag, InputFlags, ValidationStatus +from argenta.command.flag import InputFlag, ValidationStatus +from argenta.command import InputFlags # Create InputFlags collection flags = InputFlags() diff --git a/docs/code_snippets/input_flags/snippet8.py b/docs/code_snippets/input_flags/snippet8.py index 96dafdf..fffd022 100644 --- a/docs/code_snippets/input_flags/snippet8.py +++ b/docs/code_snippets/input_flags/snippet8.py @@ -1,5 +1,5 @@ from argenta.command.flag import InputFlag, ValidationStatus -from argenta.command.flag.flags.models import InputFlags +from argenta.command import InputFlags # Create first collection flags1 = InputFlags( diff --git a/docs/code_snippets/quickstart/task_manager/handlers.py b/docs/code_snippets/quickstart/task_manager/handlers.py index 826b3ea..90e17f4 100644 --- a/docs/code_snippets/quickstart/task_manager/handlers.py +++ b/docs/code_snippets/quickstart/task_manager/handlers.py @@ -1,7 +1,8 @@ from typing import cast from argenta import Command, Response, Router -from argenta.command.flag import Flag, Flags, ValidationStatus +from argenta.command.flag import Flag, ValidationStatus +from argenta.command import Flags from argenta.di import FromDishka from .repository import Priority, Task, TaskRepository diff --git a/metrics/__main__.py b/metrics/__main__.py index 9c15a8d..c1b604b 100644 --- a/metrics/__main__.py +++ b/metrics/__main__.py @@ -1,9 +1,8 @@ from argenta import App, Orchestrator -from argenta.app import StaticDividingLine - from .handlers import router -app = App(initial_message="metrics", prompt=">>> ", dividing_line=None) + +app = App(initial_message="metrics") orchestrator = Orchestrator() diff --git a/metrics/benchmarks/__init__.py b/metrics/benchmarks/__init__.py index cf0f7aa..631b500 100644 --- a/metrics/benchmarks/__init__.py +++ b/metrics/benchmarks/__init__.py @@ -1,4 +1,5 @@ from .pre_cycle_setup import * from .most_similar_command import * from .finds_appropriate_handler import * -from .validate_routers_for_collisions import * \ No newline at end of file +from .validate_routers_for_collisions import * +from .input_command_parse import * \ No newline at end of file diff --git a/metrics/benchmarks/core/models.py b/metrics/benchmarks/core/models.py index e24fcda..3a79880 100644 --- a/metrics/benchmarks/core/models.py +++ b/metrics/benchmarks/core/models.py @@ -160,3 +160,6 @@ class Benchmarks: def get_benchmark_by_name(self, name: str) -> Benchmark | None: return self._benchmarks_paired_by_name.get(name) + + def get_types(self) -> set[str]: + return set(self._benchmarks_grouped_by_type.keys()) diff --git a/metrics/benchmarks/input_command_parse.py b/metrics/benchmarks/input_command_parse.py new file mode 100644 index 0000000..9e22cf3 --- /dev/null +++ b/metrics/benchmarks/input_command_parse.py @@ -0,0 +1,51 @@ +__all__ = [ + "benchmark_parse_simple_command", + "benchmark_command_with_few_flags", + "benchmark_command_with_flags_and_values", + "benchmark_command_with_mixed_prefixes", + "benchmark_command_with_long_values", + "benchmark_command_with_quoted_values", + "benchmark_extreme_many_flags" +] + +from argenta.command.models import InputCommand + +from .entity import benchmarks + + +@benchmarks.register(type_="input_command_parse", description="Simple command (no flags)") +def benchmark_parse_simple_command() -> None: + InputCommand.parse("start") + + +@benchmarks.register(type_="input_command_parse", description="Command with few flags (3 flags)") +def benchmark_command_with_few_flags() -> None: + InputCommand.parse("start -a -b -c") + + +@benchmarks.register(type_="input_command_parse", description="Command with flags and values (5 flags)") +def benchmark_command_with_flags_and_values() -> None: + InputCommand.parse("start --host localhost --port 8080 --debug --verbose -c config.json") + + +@benchmarks.register(type_="input_command_parse", description="Command with mixed prefixes (-, --, ---)") +def benchmark_command_with_mixed_prefixes() -> None: + InputCommand.parse("cmd -a --bb ---ccc -d value --ee value2 ---fff value3") + + +@benchmarks.register(type_="input_command_parse", description="Command with long values (10 flags)") +def benchmark_command_with_long_values() -> None: + long_value = "a" * 100 + cmd = f"process --data {long_value} --config {long_value} --output {long_value}" + InputCommand.parse(cmd) + + +@benchmarks.register(type_="input_command_parse", description="Command with quoted values (5 flags)") +def benchmark_command_with_quoted_values() -> None: + InputCommand.parse("cmd --text 'hello world' --path '/usr/local/bin' --msg \"test message\"") + + +@benchmarks.register(type_="input_command_parse", description="Extreme (50 flags with values)") +def benchmark_extreme_many_flags() -> None: + flags = " ".join(f"--flag{i} value{i}" for i in range(50)) + InputCommand.parse(f"command {flags}") diff --git a/metrics/handlers.py b/metrics/handlers.py index 80ae9bf..029cb42 100644 --- a/metrics/handlers.py +++ b/metrics/handlers.py @@ -1,6 +1,7 @@ from rich.console import Console -from argenta.command import Flag, PossibleValues +from argenta.command import Flag, PossibleValues, Flags +from argenta.command.flag import ValidationStatus from argenta.command.models import Command from argenta.response import Response from argenta.router import Router @@ -15,22 +16,77 @@ router = Router(title="Metrics commands:") @router.command( Command( - "all-print", + "run-all", description="Print all benchmarks results", flags=Flag('without-gc', possible_values=PossibleValues.NEITHER) ) ) -def all_print_handler(_: Response) -> None: +def all_print_handler(response: Response) -> None: report_generator = ReportGenerator(get_system_info()) console.print(report_generator.generate_system_info_header()) console.print(report_generator.generate_system_info_table()) - is_gc_disabled = _.input_flags.get_flag_by_name("without-gc") + + is_gc_disabled = response.input_flags.get_flag_by_name("without-gc", with_status=ValidationStatus.VALID) type_grouped_benchmarks: list[BenchmarkGroupResult] = registered_benchmarks.run_benchmarks_grouped_by_type(is_gc_disabled=is_gc_disabled) + 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_report_table(benchmark_group_result)) +@router.command(Command("list-types", description="List all benchmark types")) +def list_types_handler(_: Response) -> None: + types = registered_benchmarks.get_types() + + if not types: + console.print("[yellow]No benchmark types found[/yellow]") + return + + console.print("[bold cyan]Available benchmark types:[/bold cyan]\n") + for type_ in types: + benchmarks_count = len(registered_benchmarks.get_benchmarks_by_type(type_)) + console.print(f" [green]•[/green] [bold]{type_}[/bold] ({benchmarks_count} benchmarks)") + + +@router.command( + Command( + "run-type", + description="Run benchmarks by specific type", + flags=Flags([ + Flag('type', possible_values=registered_benchmarks.get_types()), + Flag('without-gc', possible_values=PossibleValues.NEITHER) + ]) + ) +) +def run_type_handler(response: Response) -> None: + type_flag = response.input_flags.get_flag_by_name("type") + + if not type_flag: + console.print("[red]Error: --type flag is required[/red]") + console.print("[yellow]Usage: run-type --type [/yellow]") + return + + benchmark_type = type_flag.input_value + + if not type_flag.status == ValidationStatus.VALID: + console.print(f"[red]Error: No benchmarks found for type '{benchmark_type}'[/red]") + console.print("\n[yellow]Available types:[/yellow]") + types = registered_benchmarks.get_types() + for t in types: + console.print(f" • {t}") + return + + report_generator = ReportGenerator(get_system_info()) + console.print(report_generator.generate_system_info_header()) + console.print(report_generator.generate_system_info_table()) + + is_gc_disabled = response.input_flags.get_flag_by_name("without-gc", with_status=ValidationStatus.VALID) + benchmark_group_result = registered_benchmarks.run_benchmarks_by_type(benchmark_type, is_gc_disabled=is_gc_disabled) + + console.print(report_generator.generate_benchmark_table_header(benchmark_group_result)) + console.print(report_generator.generate_benchmark_report_table(benchmark_group_result)) + + @router.command(Command("release-generate", description="Generate release report")) def release_generate_handler(_: Response) -> None: console.print("[yellow]Release report generation not implemented yet[/yellow]") diff --git a/mock/mock_app/routers.py b/mock/mock_app/routers.py index a433c50..a6d4123 100644 --- a/mock/mock_app/routers.py +++ b/mock/mock_app/routers.py @@ -6,7 +6,7 @@ work_router: Router = Router(title="Base points:", disable_redirect_stdout=True) @work_router.command( Command( - "hello", + "hello", flags=Flags([ Flag("test") ]), diff --git a/src/argenta/app/models.py b/src/argenta/app/models.py index d68d05a..93c35c3 100644 --- a/src/argenta/app/models.py +++ b/src/argenta/app/models.py @@ -187,7 +187,7 @@ class BaseApp: ) ) - elif isinstance(self._dividing_line, StaticDividingLine): # pyright: ignore[reportUnnecessaryIsInstance] + elif isinstance(self._dividing_line, StaticDividingLine): self._print_func( self._dividing_line.get_full_static_line( is_override=self._override_system_messages @@ -374,18 +374,7 @@ class BaseApp: raise RuntimeError(f"Router for '{input_command.trigger}' not found. Panic!") if processing_router.disable_redirect_stdout: - dividing_line_unit_part: str = self._dividing_line.get_unit_part() - self._print_func( - StaticDividingLine(dividing_line_unit_part).get_full_static_line( - is_override=self._override_system_messages - ) - ) processing_router.finds_appropriate_handler(input_command) - self._print_func( - StaticDividingLine(dividing_line_unit_part).get_full_static_line( - is_override=self._override_system_messages - ) - ) else: stdout_result = self._capture_stdout( lambda: processing_router.finds_appropriate_handler(input_command) @@ -394,10 +383,7 @@ class BaseApp: AVAILABLE_DIVIDING_LINES: TypeAlias = StaticDividingLine | DynamicDividingLine -DEFAULT_DIVIDING_LINE: StaticDividingLine = StaticDividingLine() - DEFAULT_PRINT_FUNC: Printer = Console().print -DEFAULT_AUTOCOMPLETER: AutoCompleter = AutoCompleter() DEFAULT_EXIT_COMMAND: Command = Command("q", description="Exit command") @@ -410,10 +396,10 @@ class App(BaseApp): farewell_message: str = "\nSee you\n", exit_command: Command = DEFAULT_EXIT_COMMAND, system_router_title: str = "System points:", - dividing_line: AVAILABLE_DIVIDING_LINES | None = DEFAULT_DIVIDING_LINE, + dividing_line: AVAILABLE_DIVIDING_LINES | None = None, repeat_command_groups_printing: bool = False, override_system_messages: bool = False, - autocompleter: AutoCompleter = DEFAULT_AUTOCOMPLETER, + autocompleter: AutoCompleter | None = None, print_func: Printer = DEFAULT_PRINT_FUNC, ) -> None: """ @@ -440,7 +426,7 @@ class App(BaseApp): dividing_line=dividing_line, repeat_command_groups_printing=repeat_command_groups_printing, override_system_messages=override_system_messages, - autocompleter=autocompleter, + autocompleter=autocompleter or AutoCompleter(), print_func=print_func, ) if not self._override_system_messages: diff --git a/src/argenta/command/__init__.py b/src/argenta/command/__init__.py index 6bc02cc..3b57a28 100644 --- a/src/argenta/command/__init__.py +++ b/src/argenta/command/__init__.py @@ -1,7 +1,6 @@ from argenta.command.flag import Flag as Flag -from argenta.command.flag import Flags as Flags +from argenta.command.flag.models import Flags as Flags, InputFlags as InputFlags from argenta.command.flag import InputFlag as InputFlag -from argenta.command.flag import InputFlags as InputFlags from argenta.command.flag import PossibleValues as PossibleValues from argenta.command.flag.defaults import PredefinedFlags as PredefinedFlags from argenta.command.models import Command as Command diff --git a/src/argenta/command/flag/__init__.py b/src/argenta/command/flag/__init__.py index dc8e2be..58d4991 100644 --- a/src/argenta/command/flag/__init__.py +++ b/src/argenta/command/flag/__init__.py @@ -1,5 +1,4 @@ -from argenta.command.flag.flags.models import Flags as Flags -from argenta.command.flag.flags.models import InputFlags as InputFlags +from argenta.command.flag.models import Flags as Flags, InputFlags as InputFlags from argenta.command.flag.models import Flag as Flag from argenta.command.flag.models import InputFlag as InputFlag from argenta.command.flag.models import PossibleValues as PossibleValues diff --git a/src/argenta/command/flag/flags/__init__.py b/src/argenta/command/flag/flags/__init__.py deleted file mode 100644 index a7d3a22..0000000 --- a/src/argenta/command/flag/flags/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from argenta.command.flag.flags.models import Flags as Flags -from argenta.command.flag.flags.models import InputFlags as InputFlags diff --git a/src/argenta/command/flag/flags/models.py b/src/argenta/command/flag/flags/models.py deleted file mode 100644 index 18ab340..0000000 --- a/src/argenta/command/flag/flags/models.py +++ /dev/null @@ -1,107 +0,0 @@ -__all__ = ["Flags", "InputFlags"] - -from collections.abc import Iterator -from typing import Generic, TypeVar, override - -from argenta.command.flag.models import Flag, InputFlag - -FlagType = TypeVar("FlagType") - - -class BaseFlags(Generic[FlagType]): - def __init__(self, flags: list[FlagType] | None = None) -> None: - """ - Public. A model that combines the registered flags - :param flags: the flags that will be registered - :return: None - """ - self.flags: list[FlagType] = flags if flags else [] - - def add_flag(self, flag: FlagType) -> None: - """ - Public. Adds a flag to the list of flags - :param flag: flag to add - :return: None - """ - self.flags.append(flag) - - def add_flags(self, flags: list[FlagType]) -> None: - """ - Public. Adds a list of flags to the list of flags - :param flags: list of flags to add - :return: None - """ - self.flags.extend(flags) - - def __len__(self) -> int: - return len(self.flags) - - def __iter__(self) -> Iterator[FlagType]: - return iter(self.flags) - - def __getitem__(self, flag_index: int) -> FlagType: - return self.flags[flag_index] - - def __bool__(self) -> bool: - return bool(self.flags) - - -class Flags(BaseFlags[Flag]): - def get_flag_by_name(self, name: str) -> Flag | None: - """ - Public. Returns the flag entity by its name or None if not found - :param name: the name of the flag to get - :return: entity of the flag or None - """ - return next((flag for flag in self.flags if flag.name == name), None) - - @override - def __eq__(self, other: object) -> bool: - if not isinstance(other, Flags): - return False - - if len(self.flags) != len(other.flags): - return False - - flag_pairs: zip[tuple[Flag, Flag]] = zip(self.flags, other.flags) - return all(s_flag == o_flag for s_flag, o_flag in flag_pairs) - - def __contains__(self, flag_to_check: object) -> bool: - if isinstance(flag_to_check, Flag): - for flag in self.flags: - if flag == flag_to_check: - return True - return False - else: - raise TypeError - - -class InputFlags(BaseFlags[InputFlag]): - def get_flag_by_name(self, name: str) -> InputFlag | None: - """ - Public. Returns the flag entity by its name or None if not found - :param name: the name of the flag to get - :return: entity of the flag or None - """ - return next((flag for flag in self.flags if flag.name == name), None) - - @override - def __eq__(self, other: object) -> bool: - if not isinstance(other, InputFlags): - return False - - if len(self.flags) != len(other.flags): - return False - - paired_flags: zip[tuple[InputFlag, InputFlag]] = zip(self.flags, other.flags) - - return all(my_flag == other_flag for my_flag, other_flag in paired_flags) - - def __contains__(self, ingressable_item: object) -> bool: - if isinstance(ingressable_item, InputFlag): - for flag in self.flags: - if flag == ingressable_item: - return True - return False - else: - raise TypeError diff --git a/src/argenta/command/flag/models.py b/src/argenta/command/flag/models.py index 32b5891..3eaae40 100644 --- a/src/argenta/command/flag/models.py +++ b/src/argenta/command/flag/models.py @@ -1,8 +1,8 @@ -__all__ = ["PossibleValues", "ValidationStatus", "Flag", "InputFlag"] +__all__ = ["PossibleValues", "ValidationStatus", "Flag", "InputFlag", "InputFlags", "Flags"] from enum import Enum from re import Pattern -from typing import Literal, override +from typing import Literal, override, TypeVar, Generic, Iterator, Any PREFIX_TYPE = Literal["-", "--", "---"] @@ -91,7 +91,7 @@ class InputFlag: Public. The entity of the flag of the entered command :param name: the name of the input flag :param prefix: the prefix of the input flag - :param value: the value of the input flag + :param input_value: the value of the input flag :return: None """ self.name: str = name @@ -122,3 +122,115 @@ class InputFlag: return self.name == other.name else: raise NotImplementedError + + +FlagType = TypeVar("FlagType") + + +class BaseFlags(Generic[FlagType]): + def __init__(self, flags: list[FlagType] | None = None) -> None: + """ + Public. A model that combines the registered flags + :param flags: the flags that will be registered + :return: None + """ + self.flags: list[FlagType] = flags if flags else [] + + def add_flag(self, flag: FlagType) -> None: + """ + Public. Adds a flag to the list of flags + :param flag: flag to add + :return: None + """ + self.flags.append(flag) + + def add_flags(self, flags: list[FlagType]) -> None: + """ + Public. Adds a list of flags to the list of flags + :param flags: list of flags to add + :return: None + """ + self.flags.extend(flags) + + def __len__(self) -> int: + return len(self.flags) + + def __iter__(self) -> Iterator[FlagType]: + return iter(self.flags) + + def __getitem__(self, flag_index: int) -> FlagType: + return self.flags[flag_index] + + def __bool__(self) -> bool: + return bool(self.flags) + + +class Flags(BaseFlags[Flag]): + def get_flag_by_name(self, name: str) -> Flag | None: + """ + Public. Returns the flag entity by its name or None if not found + :param name: the name of the flag to get + :return: entity of the flag or None + """ + return next((flag for flag in self.flags if flag.name == name), None) + + @override + def __eq__(self, other: object) -> bool: + if not isinstance(other, Flags): + return False + + if len(self.flags) != len(other.flags): + return False + + flag_pairs: Iterator[tuple[Flag, Flag]] = zip(self.flags, other.flags) + return all(s_flag == o_flag for s_flag, o_flag in flag_pairs) + + def __contains__(self, flag_to_check: object) -> bool: + if isinstance(flag_to_check, Flag): + for flag in self.flags: + if flag == flag_to_check: + return True + return False + else: + raise TypeError + + +class InputFlags(BaseFlags[InputFlag]): + def get_flag_by_name( + self, + name: str, + with_status: ValidationStatus | None = None, + default: Any = None + ) -> InputFlag | None: + """ + Public. Returns the flag entity by its name or None if not found + :param default: + :param with_status: + :param name: the name of the flag to get + :return: entity of the flag or None + """ + if with_status is None: + return next((flag for flag in self.flags if flag.name == name), default) + else: + return next((flag for flag in self.flags if flag.name == name and flag.status == with_status), default) + + @override + def __eq__(self, other: object) -> bool: + if not isinstance(other, InputFlags): + return False + + if len(self.flags) != len(other.flags): + return False + + paired_flags: Iterator[tuple[InputFlag, InputFlag]] = zip(self.flags, other.flags) + + return all(my_flag == other_flag for my_flag, other_flag in paired_flags) + + def __contains__(self, ingressable_item: object) -> bool: + if isinstance(ingressable_item, InputFlag): + for flag in self.flags: + if flag == ingressable_item: + return True + return False + else: + raise TypeError diff --git a/src/argenta/command/models.py b/src/argenta/command/models.py index fc8507f..7996c22 100644 --- a/src/argenta/command/models.py +++ b/src/argenta/command/models.py @@ -8,7 +8,7 @@ from argenta.command.exceptions import ( RepeatedInputFlagsException, UnprocessedInputFlagException, ) -from argenta.command.flag.flags.models import Flags, InputFlags +from argenta.command import Flags, InputFlags from argenta.command.flag.models import Flag, InputFlag, ValidationStatus ParseFlagsResult = tuple[InputFlags, str | None, str | None] diff --git a/src/argenta/response/entity.py b/src/argenta/response/entity.py index 985e864..09a0296 100644 --- a/src/argenta/response/entity.py +++ b/src/argenta/response/entity.py @@ -2,7 +2,7 @@ __all__ = ["Response"] from dishka import Container -from argenta.command.flag.flags.models import InputFlags +from argenta.command import InputFlags from argenta.response.status import ResponseStatus EMPTY_INPUT_FLAGS: InputFlags = InputFlags() diff --git a/src/argenta/router/entity.py b/src/argenta/router/entity.py index c32695a..d339639 100644 --- a/src/argenta/router/entity.py +++ b/src/argenta/router/entity.py @@ -6,9 +6,8 @@ from typing import Callable from rich.console import Console from argenta.app.protocols import HandlerFunc -from argenta.command import Command, InputCommand +from argenta.command import Command, InputCommand, InputFlags from argenta.command.flag import ValidationStatus -from argenta.command.flag.flags import InputFlags from argenta.response import Response, ResponseStatus from argenta.router.command_handler.entity import CommandHandler, CommandHandlers from argenta.router.exceptions import (RepeatedAliasNameException, diff --git a/tests/system_tests/test_system_handling_non_standard_behavior.py b/tests/system_tests/test_system_handling_non_standard_behavior.py index 696d97f..5a489de 100644 --- a/tests/system_tests/test_system_handling_non_standard_behavior.py +++ b/tests/system_tests/test_system_handling_non_standard_behavior.py @@ -5,8 +5,7 @@ from collections.abc import Iterator import pytest from argenta import App, Orchestrator, Router -from argenta.command import Command, PredefinedFlags -from argenta.command.flag.flags.models import Flags +from argenta.command import Command, PredefinedFlags, Flags from argenta.command.flag.models import ValidationStatus from argenta.response import Response diff --git a/tests/system_tests/test_system_handling_normal_behavior.py b/tests/system_tests/test_system_handling_normal_behavior.py index dc231aa..dbb5cac 100644 --- a/tests/system_tests/test_system_handling_normal_behavior.py +++ b/tests/system_tests/test_system_handling_normal_behavior.py @@ -5,9 +5,8 @@ from collections.abc import Iterator import pytest from argenta import App, Orchestrator, Router -from argenta.command import Command, PredefinedFlags +from argenta.command import Command, PredefinedFlags, Flags from argenta.command.flag import Flag -from argenta.command.flag.flags import Flags from argenta.command.flag.models import PossibleValues, ValidationStatus from argenta.response import Response diff --git a/tests/unit_tests/test_command.py b/tests/unit_tests/test_command.py index db8dada..ba661d7 100644 --- a/tests/unit_tests/test_command.py +++ b/tests/unit_tests/test_command.py @@ -8,7 +8,7 @@ from argenta.command.exceptions import ( UnprocessedInputFlagException, ) from argenta.command.flag import Flag, InputFlag -from argenta.command.flag.flags import Flags +from argenta.command import Flags from argenta.command.flag.models import PossibleValues, ValidationStatus from argenta.command.models import Command, InputCommand diff --git a/tests/unit_tests/test_flag.py b/tests/unit_tests/test_flag.py index 3d4e211..a398281 100644 --- a/tests/unit_tests/test_flag.py +++ b/tests/unit_tests/test_flag.py @@ -3,7 +3,7 @@ import re import pytest from argenta.command.flag import Flag, InputFlag, PossibleValues -from argenta.command.flag.flags import Flags, InputFlags +from argenta.command import Flags, InputFlags # ============================================================================ diff --git a/tests/unit_tests/test_response.py b/tests/unit_tests/test_response.py index fdbdf3d..b86eda5 100644 --- a/tests/unit_tests/test_response.py +++ b/tests/unit_tests/test_response.py @@ -2,7 +2,7 @@ from datetime import date, datetime import pytest -from argenta.command.flag.flags.models import InputFlags +from argenta.command import InputFlags from argenta.command.flag.models import InputFlag from argenta.data_bridge import DataBridge from argenta.response.entity import EMPTY_INPUT_FLAGS, Response diff --git a/tests/unit_tests/test_router.py b/tests/unit_tests/test_router.py index 21bc6d3..a3c2ba5 100644 --- a/tests/unit_tests/test_router.py +++ b/tests/unit_tests/test_router.py @@ -3,9 +3,8 @@ import re import pytest from pytest import CaptureFixture -from argenta.command import Command, InputCommand +from argenta.command import Command, InputCommand, Flags, InputFlags from argenta.command.flag import Flag, InputFlag -from argenta.command.flag.flags import Flags, InputFlags from argenta.command.flag.models import PossibleValues, ValidationStatus from argenta.response.entity import Response from argenta.router import Router diff --git a/uv.lock b/uv.lock index bdb3c02..a5d1416 100644 --- a/uv.lock +++ b/uv.lock @@ -61,6 +61,10 @@ linters = [ { name = "ruff" }, { name = "wemake-python-styleguide" }, ] +metrics = [ + { name = "psutil" }, + { name = "py-cpuinfo" }, +] tests = [ { name = "pyfakefs" }, { name = "pytest" }, @@ -92,6 +96,10 @@ linters = [ { name = "ruff", specifier = ">=0.12.12" }, { name = "wemake-python-styleguide", specifier = ">=0.17.0" }, ] +metrics = [ + { name = "psutil", specifier = ">=7.2.1" }, + { name = "py-cpuinfo", specifier = ">=9.0.0" }, +] tests = [ { name = "pyfakefs", specifier = ">=5.5.0" }, { name = "pytest", specifier = ">=8.3.2" }, @@ -601,6 +609,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, ] +[[package]] +name = "psutil" +version = "7.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/73/cb/09e5184fb5fc0358d110fc3ca7f6b1d033800734d34cac10f4136cfac10e/psutil-7.2.1.tar.gz", hash = "sha256:f7583aec590485b43ca601dd9cea0dcd65bd7bb21d30ef4ddbf4ea6b5ed1bdd3", size = 490253, upload-time = "2025-12-29T08:26:00.169Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/8e/f0c242053a368c2aa89584ecd1b054a18683f13d6e5a318fc9ec36582c94/psutil-7.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ba9f33bb525b14c3ea563b2fd521a84d2fa214ec59e3e6a2858f78d0844dd60d", size = 129624, upload-time = "2025-12-29T08:26:04.255Z" }, + { url = "https://files.pythonhosted.org/packages/26/97/a58a4968f8990617decee234258a2b4fc7cd9e35668387646c1963e69f26/psutil-7.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:81442dac7abfc2f4f4385ea9e12ddf5a796721c0f6133260687fec5c3780fa49", size = 130132, upload-time = "2025-12-29T08:26:06.228Z" }, + { url = "https://files.pythonhosted.org/packages/db/6d/ed44901e830739af5f72a85fa7ec5ff1edea7f81bfbf4875e409007149bd/psutil-7.2.1-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea46c0d060491051d39f0d2cff4f98d5c72b288289f57a21556cc7d504db37fc", size = 180612, upload-time = "2025-12-29T08:26:08.276Z" }, + { url = "https://files.pythonhosted.org/packages/c7/65/b628f8459bca4efbfae50d4bf3feaab803de9a160b9d5f3bd9295a33f0c2/psutil-7.2.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35630d5af80d5d0d49cfc4d64c1c13838baf6717a13effb35869a5919b854cdf", size = 183201, upload-time = "2025-12-29T08:26:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/fb/23/851cadc9764edcc18f0effe7d0bf69f727d4cf2442deb4a9f78d4e4f30f2/psutil-7.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:923f8653416604e356073e6e0bccbe7c09990acef442def2f5640dd0faa9689f", size = 139081, upload-time = "2025-12-29T08:26:12.483Z" }, + { url = "https://files.pythonhosted.org/packages/59/82/d63e8494ec5758029f31c6cb06d7d161175d8281e91d011a4a441c8a43b5/psutil-7.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cfbe6b40ca48019a51827f20d830887b3107a74a79b01ceb8cc8de4ccb17b672", size = 134767, upload-time = "2025-12-29T08:26:14.528Z" }, + { url = "https://files.pythonhosted.org/packages/05/c2/5fb764bd61e40e1fe756a44bd4c21827228394c17414ade348e28f83cd79/psutil-7.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:494c513ccc53225ae23eec7fe6e1482f1b8a44674241b54561f755a898650679", size = 129716, upload-time = "2025-12-29T08:26:16.017Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d2/935039c20e06f615d9ca6ca0ab756cf8408a19d298ffaa08666bc18dc805/psutil-7.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fce5f92c22b00cdefd1645aa58ab4877a01679e901555067b1bd77039aa589f", size = 130133, upload-time = "2025-12-29T08:26:18.009Z" }, + { url = "https://files.pythonhosted.org/packages/77/69/19f1eb0e01d24c2b3eacbc2f78d3b5add8a89bf0bb69465bc8d563cc33de/psutil-7.2.1-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93f3f7b0bb07711b49626e7940d6fe52aa9940ad86e8f7e74842e73189712129", size = 181518, upload-time = "2025-12-29T08:26:20.241Z" }, + { url = "https://files.pythonhosted.org/packages/e1/6d/7e18b1b4fa13ad370787626c95887b027656ad4829c156bb6569d02f3262/psutil-7.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d34d2ca888208eea2b5c68186841336a7f5e0b990edec929be909353a202768a", size = 184348, upload-time = "2025-12-29T08:26:22.215Z" }, + { url = "https://files.pythonhosted.org/packages/98/60/1672114392dd879586d60dd97896325df47d9a130ac7401318005aab28ec/psutil-7.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2ceae842a78d1603753561132d5ad1b2f8a7979cb0c283f5b52fb4e6e14b1a79", size = 140400, upload-time = "2025-12-29T08:26:23.993Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7b/d0e9d4513c46e46897b46bcfc410d51fc65735837ea57a25170f298326e6/psutil-7.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:08a2f175e48a898c8eb8eace45ce01777f4785bc744c90aa2cc7f2fa5462a266", size = 135430, upload-time = "2025-12-29T08:26:25.999Z" }, + { url = "https://files.pythonhosted.org/packages/c5/cf/5180eb8c8bdf6a503c6919f1da28328bd1e6b3b1b5b9d5b01ae64f019616/psutil-7.2.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b2e953fcfaedcfbc952b44744f22d16575d3aa78eb4f51ae74165b4e96e55f42", size = 128137, upload-time = "2025-12-29T08:26:27.759Z" }, + { url = "https://files.pythonhosted.org/packages/c5/2c/78e4a789306a92ade5000da4f5de3255202c534acdadc3aac7b5458fadef/psutil-7.2.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:05cc68dbb8c174828624062e73078e7e35406f4ca2d0866c272c2410d8ef06d1", size = 128947, upload-time = "2025-12-29T08:26:29.548Z" }, + { url = "https://files.pythonhosted.org/packages/29/f8/40e01c350ad9a2b3cb4e6adbcc8a83b17ee50dd5792102b6142385937db5/psutil-7.2.1-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e38404ca2bb30ed7267a46c02f06ff842e92da3bb8c5bfdadbd35a5722314d8", size = 154694, upload-time = "2025-12-29T08:26:32.147Z" }, + { url = "https://files.pythonhosted.org/packages/06/e4/b751cdf839c011a9714a783f120e6a86b7494eb70044d7d81a25a5cd295f/psutil-7.2.1-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab2b98c9fc19f13f59628d94df5cc4cc4844bc572467d113a8b517d634e362c6", size = 156136, upload-time = "2025-12-29T08:26:34.079Z" }, + { url = "https://files.pythonhosted.org/packages/44/ad/bbf6595a8134ee1e94a4487af3f132cef7fce43aef4a93b49912a48c3af7/psutil-7.2.1-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f78baafb38436d5a128f837fab2d92c276dfb48af01a240b861ae02b2413ada8", size = 148108, upload-time = "2025-12-29T08:26:36.225Z" }, + { url = "https://files.pythonhosted.org/packages/1c/15/dd6fd869753ce82ff64dcbc18356093471a5a5adf4f77ed1f805d473d859/psutil-7.2.1-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:99a4cd17a5fdd1f3d014396502daa70b5ec21bf4ffe38393e152f8e449757d67", size = 147402, upload-time = "2025-12-29T08:26:39.21Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/d9317542e3f2b180c4306e3f45d3c922d7e86d8ce39f941bb9e2e9d8599e/psutil-7.2.1-cp37-abi3-win_amd64.whl", hash = "sha256:b1b0671619343aa71c20ff9767eced0483e4fc9e1f489d50923738caf6a03c17", size = 136938, upload-time = "2025-12-29T08:26:41.036Z" }, + { url = "https://files.pythonhosted.org/packages/3e/73/2ce007f4198c80fcf2cb24c169884f833fe93fbc03d55d302627b094ee91/psutil-7.2.1-cp37-abi3-win_arm64.whl", hash = "sha256:0d67c1822c355aa6f7314d92018fb4268a76668a536f133599b91edd48759442", size = 133836, upload-time = "2025-12-29T08:26:43.086Z" }, +] + +[[package]] +name = "py-cpuinfo" +version = "9.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/a8/d832f7293ebb21690860d2e01d8115e5ff6f2ae8bbdc953f0eb0fa4bd2c7/py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690", size = 104716, upload-time = "2022-10-25T20:38:06.303Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5", size = 22335, upload-time = "2022-10-25T20:38:27.636Z" }, +] + [[package]] name = "pycodestyle" version = "2.14.0" From 2ad86dbedd8913b5f2b32f2380d32e2c814ac73b Mon Sep 17 00:00:00 2001 From: kolo Date: Thu, 22 Jan 2026 22:39:35 +0300 Subject: [PATCH 11/31] bench --- metrics/__main__.py | 1 + metrics/handlers.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/metrics/__main__.py b/metrics/__main__.py index c1b604b..7042abf 100644 --- a/metrics/__main__.py +++ b/metrics/__main__.py @@ -8,6 +8,7 @@ orchestrator = Orchestrator() def main() -> None: app.include_router(router) + app.set_description_message_pattern(lambda command, description: f'[blue]{command}[/blue] [red]--->[/red] [i]{description}[/i]') orchestrator.start_polling(app) diff --git a/metrics/handlers.py b/metrics/handlers.py index 029cb42..06e47ee 100644 --- a/metrics/handlers.py +++ b/metrics/handlers.py @@ -80,7 +80,7 @@ def run_type_handler(response: Response) -> None: console.print(report_generator.generate_system_info_header()) console.print(report_generator.generate_system_info_table()) - is_gc_disabled = response.input_flags.get_flag_by_name("without-gc", with_status=ValidationStatus.VALID) + is_gc_disabled = response.input_flags.get_flag_by_name("without-gc", with_status=ValidationStatus.VALID, default=False) benchmark_group_result = registered_benchmarks.run_benchmarks_by_type(benchmark_type, is_gc_disabled=is_gc_disabled) console.print(report_generator.generate_benchmark_table_header(benchmark_group_result)) From 1c54f11f3135755c20dee75f153b6be20ca0cdf5 Mon Sep 17 00:00:00 2001 From: kolo Date: Sat, 24 Jan 2026 00:04:28 +0300 Subject: [PATCH 12/31] benchs --- metrics/benchmarks/__init__.py | 3 +- metrics/benchmarks/core/exceptions.py | 6 +- metrics/benchmarks/entity.py | 2 +- metrics/benchmarks/flag_validation.py | 102 +++++++++++++++++++++++++ metrics/handlers.py | 26 +++++-- metrics/services/system_info_reader.py | 2 +- mock/local_test.py | 18 ++--- src/argenta/app/models.py | 2 +- src/argenta/command/flag/models.py | 4 +- 9 files changed, 136 insertions(+), 29 deletions(-) create mode 100644 metrics/benchmarks/flag_validation.py diff --git a/metrics/benchmarks/__init__.py b/metrics/benchmarks/__init__.py index 631b500..37d80b8 100644 --- a/metrics/benchmarks/__init__.py +++ b/metrics/benchmarks/__init__.py @@ -2,4 +2,5 @@ from .pre_cycle_setup import * from .most_similar_command import * from .finds_appropriate_handler import * from .validate_routers_for_collisions import * -from .input_command_parse import * \ No newline at end of file +from .input_command_parse import * +from .flag_validation import * \ No newline at end of file diff --git a/metrics/benchmarks/core/exceptions.py b/metrics/benchmarks/core/exceptions.py index b849974..bf199c1 100644 --- a/metrics/benchmarks/core/exceptions.py +++ b/metrics/benchmarks/core/exceptions.py @@ -2,7 +2,7 @@ class BenchmarkNotFound(Exception): def __init__(self, benchmark_name: str): self.benchmark_name = benchmark_name - def __str__(self): + def __str__(self) -> str: return f"Benchmark with name '{self.benchmark_name}' not found" @@ -10,11 +10,11 @@ class BenchmarksNotFound(Exception): def __init__(self, type_: str): self.type_ = type_ - def __str__(self): + def __str__(self) -> str: return f"Benchmarks with type '{self.type_}' not found" class BenchmarksWithSameNameAlreadyExists(Exception): def __init__(self, benchmark_name: str): self.benchmark_name = benchmark_name - def __str__(self): + def __str__(self) -> str: return f"Benchmarks with name '{self.benchmark_name}' already exists" diff --git a/metrics/benchmarks/entity.py b/metrics/benchmarks/entity.py index 6722849..2365dee 100644 --- a/metrics/benchmarks/entity.py +++ b/metrics/benchmarks/entity.py @@ -1,3 +1,3 @@ -from metrics.benchmarks.core.models import Benchmarks +from .core.models import Benchmarks benchmarks = Benchmarks() \ No newline at end of file diff --git a/metrics/benchmarks/flag_validation.py b/metrics/benchmarks/flag_validation.py new file mode 100644 index 0000000..c3144f1 --- /dev/null +++ b/metrics/benchmarks/flag_validation.py @@ -0,0 +1,102 @@ +__all__ = [ + "benchmark_validate_all_single_flag", + "benchmark_validate_neither_single_flag", + "benchmark_validate_list_small", + "benchmark_validate_list_large", + "benchmark_validate_regex_simple", + "benchmark_validate_regex_complex", + "benchmark_validate_multiple_flags_10", + "benchmark_validate_multiple_flags_50", + "benchmark_validate_extreme_100_flags" +] + +import re + +from argenta.command.flag import Flag, InputFlag, PossibleValues + +from .entity import benchmarks + + +@benchmarks.register(type_="flag_validation", description="Single flag with PossibleValues.ALL") +def benchmark_validate_all_single_flag() -> None: + flag = Flag("test", possible_values=PossibleValues.ALL) + flag.validate_input_flag_value("some_value") + + +@benchmarks.register(type_="flag_validation", description="Single flag with PossibleValues.NEITHER") +def benchmark_validate_neither_single_flag() -> None: + flag = Flag("test", possible_values=PossibleValues.NEITHER) + flag.validate_input_flag_value("") + + +@benchmarks.register(type_="flag_validation", description="List validation (5 possible values)") +def benchmark_validate_list_small() -> None: + flag = Flag("env", possible_values=["dev", "staging", "prod", "test", "local"]) + flag.validate_input_flag_value("prod") + + +@benchmarks.register(type_="flag_validation", description="List validation (50 possible values)") +def benchmark_validate_list_large() -> None: + possible_values = [f"value{i}" for i in range(50)] + flag = Flag("option", possible_values=possible_values) + flag.validate_input_flag_value("value25") + + +@benchmarks.register(type_="flag_validation", description="Regex validation (simple pattern)") +def benchmark_validate_regex_simple() -> None: + pattern = re.compile(r"^\d+$") + flag = Flag("port", possible_values=pattern) + flag.validate_input_flag_value("8080") + + +@benchmarks.register(type_="flag_validation", description="Regex validation (complex pattern)") +def benchmark_validate_regex_complex() -> None: + pattern = re.compile(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$") + flag = Flag("email", possible_values=pattern) + flag.validate_input_flag_value("user@example.com") + + +@benchmarks.register(type_="flag_validation", description="Multiple flags validation (10 flags)") +def benchmark_validate_multiple_flags_10() -> None: + flags = [ + Flag(f"flag{i}", possible_values=PossibleValues.ALL) + for i in range(10) + ] + input_flags = [ + InputFlag(f"flag{i}", input_value=f"value{i}") + for i in range(10) + ] + + for flag, input_flag in zip(flags, input_flags): + flag.validate_input_flag_value(input_flag.input_value) + + +@benchmarks.register(type_="flag_validation", description="Multiple flags validation (50 flags)") +def benchmark_validate_multiple_flags_50() -> None: + flags = [ + Flag(f"flag{i}", possible_values=PossibleValues.ALL) + for i in range(50) + ] + input_flags = [ + InputFlag(f"flag{i}", input_value=f"value{i}") + for i in range(50) + ] + + for flag, input_flag in zip(flags, input_flags): + flag.validate_input_flag_value(input_flag.input_value) + + +@benchmarks.register(type_="flag_validation", description="Extreme (100 flags with regex validation)") +def benchmark_validate_extreme_100_flags() -> None: + pattern = re.compile(r"^[a-zA-Z0-9_-]+$") + flags = [ + Flag(f"flag{i}", possible_values=pattern) + for i in range(100) + ] + input_flags = [ + InputFlag(f"flag{i}", input_value=f"valid_value_{i}") + for i in range(100) + ] + + for flag, input_flag in zip(flags, input_flags): + flag.validate_input_flag_value(input_flag.input_value) diff --git a/metrics/handlers.py b/metrics/handlers.py index 06e47ee..ee65b94 100644 --- a/metrics/handlers.py +++ b/metrics/handlers.py @@ -18,16 +18,22 @@ router = Router(title="Metrics commands:") Command( "run-all", description="Print all benchmarks results", - flags=Flag('without-gc', possible_values=PossibleValues.NEITHER) + flags=Flags([ + Flag('without-gc', possible_values=PossibleValues.NEITHER), + Flag('without-system-info', possible_values=PossibleValues.NEITHER) + ]) ) ) def all_print_handler(response: Response) -> None: report_generator = ReportGenerator(get_system_info()) - console.print(report_generator.generate_system_info_header()) - console.print(report_generator.generate_system_info_table()) + + without_system_info = response.input_flags.get_flag_by_name("without-system-info", with_status=ValidationStatus.VALID) + if not without_system_info: + console.print(report_generator.generate_system_info_header()) + console.print(report_generator.generate_system_info_table()) is_gc_disabled = response.input_flags.get_flag_by_name("without-gc", with_status=ValidationStatus.VALID) - type_grouped_benchmarks: list[BenchmarkGroupResult] = registered_benchmarks.run_benchmarks_grouped_by_type(is_gc_disabled=is_gc_disabled) + type_grouped_benchmarks: list[BenchmarkGroupResult] = registered_benchmarks.run_benchmarks_grouped_by_type(is_gc_disabled=bool(is_gc_disabled)) for benchmark_group_result in type_grouped_benchmarks: console.print(report_generator.generate_benchmark_table_header(benchmark_group_result)) @@ -54,7 +60,8 @@ def list_types_handler(_: Response) -> None: description="Run benchmarks by specific type", flags=Flags([ Flag('type', possible_values=registered_benchmarks.get_types()), - Flag('without-gc', possible_values=PossibleValues.NEITHER) + Flag('without-gc', possible_values=PossibleValues.NEITHER), + Flag('without-system-info', possible_values=PossibleValues.NEITHER) ]) ) ) @@ -77,11 +84,14 @@ def run_type_handler(response: Response) -> None: return report_generator = ReportGenerator(get_system_info()) - console.print(report_generator.generate_system_info_header()) - console.print(report_generator.generate_system_info_table()) + + without_system_info = response.input_flags.get_flag_by_name("without-system-info", with_status=ValidationStatus.VALID) + if not without_system_info: + console.print(report_generator.generate_system_info_header()) + console.print(report_generator.generate_system_info_table()) is_gc_disabled = response.input_flags.get_flag_by_name("without-gc", with_status=ValidationStatus.VALID, default=False) - benchmark_group_result = registered_benchmarks.run_benchmarks_by_type(benchmark_type, is_gc_disabled=is_gc_disabled) + benchmark_group_result = registered_benchmarks.run_benchmarks_by_type(benchmark_type, is_gc_disabled=bool(is_gc_disabled)) console.print(report_generator.generate_benchmark_table_header(benchmark_group_result)) console.print(report_generator.generate_benchmark_report_table(benchmark_group_result)) diff --git a/metrics/services/system_info_reader.py b/metrics/services/system_info_reader.py index 4d369fe..6f832b2 100644 --- a/metrics/services/system_info_reader.py +++ b/metrics/services/system_info_reader.py @@ -90,7 +90,7 @@ def get_cpu_info() -> CPUInfo: cpu_physical_cores = psutil.cpu_count(logical=False) cpu_logical_cores = psutil.cpu_count(logical=True) - cpu_freq = psutil.cpu_freq() or "N/A" + cpu_freq = psutil.cpu_freq() cpu_max_frequency = cpu_freq.max return CPUInfo( diff --git a/mock/local_test.py b/mock/local_test.py index 3e47833..1982580 100644 --- a/mock/local_test.py +++ b/mock/local_test.py @@ -1,14 +1,8 @@ -from argenta.app import AutoCompleter +from importlib.metadata import version, PackageNotFoundError +try: + __version__ = version("argenta") +except PackageNotFoundError: + __version__ = "unknown" -if __name__ == "__main__": - test_commands: set[str] = {"start", "qwertyu", "stop", "exit"} - hist_file: str = "history.txt" - - ac: AutoCompleter = AutoCompleter(autocomplete_button='tab') - ac.initial_setup(test_commands) - - while True: - inp: str = ac.prompt(">>> ").strip() - if inp == "exit": - break +print("__version__ = {}".format(__version__)) \ No newline at end of file diff --git a/src/argenta/app/models.py b/src/argenta/app/models.py index 93c35c3..22c256f 100644 --- a/src/argenta/app/models.py +++ b/src/argenta/app/models.py @@ -52,7 +52,7 @@ class BaseApp: self._prompt: str | HTML = prompt self._print_func: Printer = print_func self._exit_command: Command = exit_command - self._dividing_line: StaticDividingLine | DynamicDividingLine = dividing_line + self._dividing_line: StaticDividingLine | DynamicDividingLine | None = dividing_line self._repeat_command_groups_printing: bool = repeat_command_groups_printing self._override_system_messages: bool = override_system_messages self._autocompleter: AutoCompleter = autocompleter diff --git a/src/argenta/command/flag/models.py b/src/argenta/command/flag/models.py index 3eaae40..9355362 100644 --- a/src/argenta/command/flag/models.py +++ b/src/argenta/command/flag/models.py @@ -2,7 +2,7 @@ __all__ = ["PossibleValues", "ValidationStatus", "Flag", "InputFlag", "InputFlag from enum import Enum from re import Pattern -from typing import Literal, override, TypeVar, Generic, Iterator, Any +from typing import Literal, override, TypeVar, Generic, Iterator, Any, Container PREFIX_TYPE = Literal["-", "--", "---"] @@ -24,7 +24,7 @@ class Flag: name: str, *, prefix: PREFIX_TYPE = "--", - possible_values: list[str] | Pattern[str] | PossibleValues = PossibleValues.ALL, + possible_values: Container[str] | Pattern[str] | PossibleValues = PossibleValues.ALL, ) -> None: """ Public. The entity of the flag being registered for subsequent processing From 19bbaab1eedabfcfa6c4774c4b1160da9afacfbb Mon Sep 17 00:00:00 2001 From: kolo Date: Sat, 24 Jan 2026 02:06:46 +0300 Subject: [PATCH 13/31] diagrams --- .gitignore | 2 + metrics/handlers.py | 57 +++- metrics/services/__init__.py | 5 + metrics/services/diagram_generator.py | 90 ++++++ mock/local_test.py | 10 +- pyproject.toml | 1 + src/argenta/app/autocompleter/entity.py | 26 +- src/argenta/app/models.py | 8 +- uv.lock | 404 ++++++++++++++++++++++++ 9 files changed, 576 insertions(+), 27 deletions(-) create mode 100644 metrics/services/diagram_generator.py diff --git a/.gitignore b/.gitignore index a71cd19..46af4f2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ #### joe made this: http://goel.io/joe +metrics/reports/diagrams + #### python #### # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/metrics/handlers.py b/metrics/handlers.py index ee65b94..b135652 100644 --- a/metrics/handlers.py +++ b/metrics/handlers.py @@ -1,3 +1,7 @@ +import re +from datetime import datetime +from pathlib import Path + from rich.console import Console from argenta.command import Flag, PossibleValues, Flags @@ -9,9 +13,12 @@ from .benchmarks.core.models import BenchmarkGroupResult from .benchmarks.entity import benchmarks as registered_benchmarks from .services.report_generator import ReportGenerator from .services.system_info_reader import get_system_info +from .services.diagram_generator import DiagramGenerator console = Console() -router = Router(title="Metrics commands:") +router = Router(title="Metrics commands:", disable_redirect_stdout=True) + +POSITIVE_INTEGER_PATTERN = re.compile(r"^[1-9]\d*$") @router.command( @@ -102,6 +109,48 @@ def release_generate_handler(_: Response) -> None: console.print("[yellow]Release report generation not implemented yet[/yellow]") -@router.command(Command("diagrams-generate", description="Generate diagrams")) -def diagrams_generate_handler(_: Response) -> None: - console.print("[yellow]Diagrams generation not implemented yet[/yellow]") +@router.command( + Command( + "diagrams-generate", + description="Generate diagrams for all benchmarks", + flags=Flags([ + Flag('without-gc', possible_values=PossibleValues.NEITHER), + Flag('iterations', possible_values=POSITIVE_INTEGER_PATTERN) + ]) + ) +) +def diagrams_generate_handler(response: Response) -> None: + iterations = 100 + iterations_flag = response.input_flags.get_flag_by_name("iterations", with_status=ValidationStatus.VALID) + if iterations_flag: + iterations = int(iterations_flag.input_value) + + is_gc_disabled = bool(response.input_flags.get_flag_by_name("without-gc", with_status=ValidationStatus.VALID)) + + console.print("[cyan]Running all benchmarks...[/cyan]") + console.print(f"[dim]Iterations: {iterations}, GC Disabled: {is_gc_disabled}[/dim]\n") + + type_grouped_benchmarks: list[BenchmarkGroupResult] = registered_benchmarks.run_benchmarks_grouped_by_type( + iterations=iterations, + is_gc_disabled=is_gc_disabled + ) + + timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + output_dir = Path("metrics/reports/diagrams") / timestamp + output_dir.mkdir(parents=True, exist_ok=True) + + diagram_generator = DiagramGenerator(output_dir) + + console.print(f"[green]✓[/green] Benchmarks completed. Generating diagrams...\n") + + generated_count = 0 + + for benchmark_group in type_grouped_benchmarks: + console.print(f"[cyan]Generating diagram for:[/cyan] [bold]{benchmark_group.type_}[/bold]") + + comparison_path = diagram_generator.generate_comparison_diagram(benchmark_group) + generated_count += 1 + console.print(f" [green]✓[/green] {comparison_path.name}\n") + + console.print(f"[bold green]✓ Successfully generated {generated_count} diagrams[/bold green]") + console.print(f"[cyan]Output directory:[/cyan] [bold]{output_dir}[/bold]") diff --git a/metrics/services/__init__.py b/metrics/services/__init__.py index e69de29..9095bae 100644 --- a/metrics/services/__init__.py +++ b/metrics/services/__init__.py @@ -0,0 +1,5 @@ +from .diagram_generator import DiagramGenerator +from .report_generator import ReportGenerator +from .system_info_reader import get_system_info + +__all__ = ["DiagramGenerator", "ReportGenerator", "get_system_info"] diff --git a/metrics/services/diagram_generator.py b/metrics/services/diagram_generator.py new file mode 100644 index 0000000..f3dc82b --- /dev/null +++ b/metrics/services/diagram_generator.py @@ -0,0 +1,90 @@ +__all__ = ["DiagramGenerator"] + +from pathlib import Path + +import matplotlib +import matplotlib.pyplot as plt +import numpy as np + +from ..benchmarks.core.models import BenchmarkGroupResult + + +class DiagramGenerator: + def __init__(self, output_dir: Path | str) -> None: + self.output_dir = output_dir + matplotlib.use('Agg') + plt.style.use('seaborn-v0_8-whitegrid') + + def generate_comparison_diagram(self, benchmark_group: BenchmarkGroupResult) -> Path: + results = benchmark_group.benchmark_results + sorted_results = sorted(results, key=lambda br: br.avg_time) + + descriptions = [br.description for br in sorted_results] + avg_times = [br.avg_time for br in sorted_results] + median_times = [br.median_time for br in sorted_results] + std_devs = [br.std_dev for br in sorted_results] + + max_value = max(max(avg_times), max(median_times), max(std_devs)) + y_limit = max_value / 0.85 + + x = np.arange(len(descriptions)) + width = 0.25 + + fig, ax = plt.subplots(figsize=(16, 8)) + fig.patch.set_facecolor('white') + + bars1 = ax.bar(x - width, std_devs, width, label='Std Deviation', + color='#2ecc71', alpha=0.9, edgecolor='#27ae60', linewidth=1.5) + bars2 = ax.bar(x, avg_times, width, label='Average Time', + color='#3498db', alpha=0.9, edgecolor='#2980b9', linewidth=1.5) + bars3 = ax.bar(x + width, median_times, width, label='Median Time', + color='#e74c3c', alpha=0.9, edgecolor='#c0392b', linewidth=1.5) + + for bars in [bars1, bars2, bars3]: + for bar in bars: + 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) + ax.set_xticklabels([]) + + for i, (pos, desc) in enumerate(zip(x, descriptions)): + bar_x = pos - width - width / 2 + ax.text(bar_x, y_limit * 0.02, desc, + rotation=90, va='bottom', ha='right', fontsize=10, + color='#2c3e50') + + ax.set_ylim(0, y_limit) + + legend = ax.legend(loc='upper left', fontsize=12, framealpha=0.95, + edgecolor='#34495e', fancybox=True, shadow=True) + legend.get_frame().set_facecolor('#ecf0f1') + + ax.grid(axis='y', alpha=0.4, linestyle='--', linewidth=0.8) + ax.set_axisbelow(True) + + ax.spines['top'].set_visible(False) + ax.spines['right'].set_visible(False) + ax.spines['left'].set_color('#7f8c8d') + ax.spines['bottom'].set_color('#7f8c8d') + + plt.tight_layout() + + filename = f"{benchmark_group.type_}_comparison.png" + output_path = self.output_dir / filename + plt.savefig(output_path, dpi=200, bbox_inches='tight', facecolor='white') + plt.close(fig) + + return output_path diff --git a/mock/local_test.py b/mock/local_test.py index 1982580..ddfa8c5 100644 --- a/mock/local_test.py +++ b/mock/local_test.py @@ -1,8 +1,4 @@ -from importlib.metadata import version, PackageNotFoundError +from argenta import App -try: - __version__ = version("argenta") -except PackageNotFoundError: - __version__ = "unknown" - -print("__version__ = {}".format(__version__)) \ No newline at end of file +app = App() +app._autocompleter.initial_setup(set()) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 23ea3f1..f3e09d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ tests = [ "pytest-mock>=3.15.1", ] metrics = [ + "matplotlib>=3.10.8", "psutil>=7.2.1", "py-cpuinfo>=9.0.0", ] diff --git a/src/argenta/app/autocompleter/entity.py b/src/argenta/app/autocompleter/entity.py index dedc320..fa298c4 100644 --- a/src/argenta/app/autocompleter/entity.py +++ b/src/argenta/app/autocompleter/entity.py @@ -5,7 +5,7 @@ from typing import Callable, Iterable from prompt_toolkit import PromptSession, HTML from prompt_toolkit.auto_suggest import AutoSuggestFromHistory -from prompt_toolkit.completion import Completer, Completion, CompleteEvent +from prompt_toolkit.completion import Completer, Completion, CompleteEvent, ThreadedCompleter from prompt_toolkit.document import Document from prompt_toolkit.formatted_text import StyleAndTextTuples from prompt_toolkit.history import History, ThreadedHistory, FileHistory, InMemoryHistory @@ -97,34 +97,36 @@ class AutoCompleter: def _(event: KeyPressEvent) -> None: buff = event.app.current_buffer - if buff.complete_state: buff.complete_next() - else: - completions = list(buff.completer.get_completions(buff.document, CompleteEvent())) - if len(completions) == 1: - buff.apply_completion(completions[0]) - else: - buff.start_completion(select_first=False) + return + comps_gen = 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=HistoryCompleter(history, all_commands), + 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 style, + style=style if self.command_highlighting else None, lexer=CommandLexer(all_commands) if self.command_highlighting else None, ) diff --git a/src/argenta/app/models.py b/src/argenta/app/models.py index 22c256f..ec1b4c4 100644 --- a/src/argenta/app/models.py +++ b/src/argenta/app/models.py @@ -147,7 +147,7 @@ class BaseApp: :return: None """ for registered_router in self.registered_routers: - self._print_func(registered_router.title) + self._print_func('\n'+registered_router.title) for command_handler in registered_router.command_handlers: handled_command = command_handler.handled_command self._print_func( @@ -156,7 +156,6 @@ class BaseApp: handled_command.description, ) ) - self._print_func("") def _print_framed_text(self, text: str) -> None: """ @@ -308,9 +307,9 @@ class BaseApp: Private. Sets up default app view :return: None """ - self._prompt = f"{self._prompt}" + self._prompt = f"\n{self._prompt}" self._initial_message = ( - "\n" + f"[bold red]{text2art(self._initial_message, font='tarty1')}" + "\n" + "\n" + f"[bold red]{text2art(self._initial_message, font='tarty1')}" ) self._farewell_message = ( "[bold red]\n\n" @@ -443,6 +442,7 @@ class App(BaseApp): self._print_command_group_description() raw_command: str = self._autocompleter.prompt(self._prompt) + print() # post-prompt gap try: input_command: InputCommand = InputCommand.parse(raw_command=raw_command) diff --git a/uv.lock b/uv.lock index a5d1416..7ed2bdf 100644 --- a/uv.lock +++ b/uv.lock @@ -62,6 +62,7 @@ linters = [ { name = "wemake-python-styleguide" }, ] metrics = [ + { name = "matplotlib" }, { name = "psutil" }, { name = "py-cpuinfo" }, ] @@ -97,6 +98,7 @@ linters = [ { name = "wemake-python-styleguide", specifier = ">=0.17.0" }, ] metrics = [ + { name = "matplotlib", specifier = ">=3.10.8" }, { name = "psutil", specifier = ">=7.2.1" }, { name = "py-cpuinfo", specifier = ">=9.0.0" }, ] @@ -235,6 +237,72 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, + { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, + { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, +] + [[package]] name = "coverage" version = "7.11.0" @@ -309,6 +377,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5f/04/642c1d8a448ae5ea1369eac8495740a79eb4e581a9fb0cbdce56bbf56da1/coverage-7.11.0-py3-none-any.whl", hash = "sha256:4b7589765348d78fb4e5fb6ea35d07564e387da2fc5efff62e0222971f155f68", size = 207761, upload-time = "2025-10-15T15:15:06.439Z" }, ] +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + [[package]] name = "dishka" version = "1.7.2" @@ -357,6 +434,47 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e", size = 57922, upload-time = "2025-06-20T19:31:34.425Z" }, ] +[[package]] +name = "fonttools" +version = "4.61.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/ca/cf17b88a8df95691275a3d77dc0a5ad9907f328ae53acbe6795da1b2f5ed/fonttools-4.61.1.tar.gz", hash = "sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69", size = 3565756, upload-time = "2025-12-12T17:31:24.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/16/7decaa24a1bd3a70c607b2e29f0adc6159f36a7e40eaba59846414765fd4/fonttools-4.61.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f3cb4a569029b9f291f88aafc927dd53683757e640081ca8c412781ea144565e", size = 2851593, upload-time = "2025-12-12T17:30:04.225Z" }, + { url = "https://files.pythonhosted.org/packages/94/98/3c4cb97c64713a8cf499b3245c3bf9a2b8fd16a3e375feff2aed78f96259/fonttools-4.61.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41a7170d042e8c0024703ed13b71893519a1a6d6e18e933e3ec7507a2c26a4b2", size = 2400231, upload-time = "2025-12-12T17:30:06.47Z" }, + { url = "https://files.pythonhosted.org/packages/b7/37/82dbef0f6342eb01f54bca073ac1498433d6ce71e50c3c3282b655733b31/fonttools-4.61.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10d88e55330e092940584774ee5e8a6971b01fc2f4d3466a1d6c158230880796", size = 4954103, upload-time = "2025-12-12T17:30:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/6c/44/f3aeac0fa98e7ad527f479e161aca6c3a1e47bb6996b053d45226fe37bf2/fonttools-4.61.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15acc09befd16a0fb8a8f62bc147e1a82817542d72184acca9ce6e0aeda9fa6d", size = 5004295, upload-time = "2025-12-12T17:30:10.56Z" }, + { url = "https://files.pythonhosted.org/packages/14/e8/7424ced75473983b964d09f6747fa09f054a6d656f60e9ac9324cf40c743/fonttools-4.61.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e6bcdf33aec38d16508ce61fd81838f24c83c90a1d1b8c68982857038673d6b8", size = 4944109, upload-time = "2025-12-12T17:30:12.874Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8b/6391b257fa3d0b553d73e778f953a2f0154292a7a7a085e2374b111e5410/fonttools-4.61.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5fade934607a523614726119164ff621e8c30e8fa1ffffbbd358662056ba69f0", size = 5093598, upload-time = "2025-12-12T17:30:15.79Z" }, + { url = "https://files.pythonhosted.org/packages/d9/71/fd2ea96cdc512d92da5678a1c98c267ddd4d8c5130b76d0f7a80f9a9fde8/fonttools-4.61.1-cp312-cp312-win32.whl", hash = "sha256:75da8f28eff26defba42c52986de97b22106cb8f26515b7c22443ebc9c2d3261", size = 2269060, upload-time = "2025-12-12T17:30:18.058Z" }, + { url = "https://files.pythonhosted.org/packages/80/3b/a3e81b71aed5a688e89dfe0e2694b26b78c7d7f39a5ffd8a7d75f54a12a8/fonttools-4.61.1-cp312-cp312-win_amd64.whl", hash = "sha256:497c31ce314219888c0e2fce5ad9178ca83fe5230b01a5006726cdf3ac9f24d9", size = 2319078, upload-time = "2025-12-12T17:30:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/4b/cf/00ba28b0990982530addb8dc3e9e6f2fa9cb5c20df2abdda7baa755e8fe1/fonttools-4.61.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c56c488ab471628ff3bfa80964372fc13504ece601e0d97a78ee74126b2045c", size = 2846454, upload-time = "2025-12-12T17:30:24.938Z" }, + { url = "https://files.pythonhosted.org/packages/5a/ca/468c9a8446a2103ae645d14fee3f610567b7042aba85031c1c65e3ef7471/fonttools-4.61.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc492779501fa723b04d0ab1f5be046797fee17d27700476edc7ee9ae535a61e", size = 2398191, upload-time = "2025-12-12T17:30:27.343Z" }, + { url = "https://files.pythonhosted.org/packages/a3/4b/d67eedaed19def5967fade3297fed8161b25ba94699efc124b14fb68cdbc/fonttools-4.61.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:64102ca87e84261419c3747a0d20f396eb024bdbeb04c2bfb37e2891f5fadcb5", size = 4928410, upload-time = "2025-12-12T17:30:29.771Z" }, + { url = "https://files.pythonhosted.org/packages/b0/8d/6fb3494dfe61a46258cd93d979cf4725ded4eb46c2a4ca35e4490d84daea/fonttools-4.61.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c1b526c8d3f615a7b1867f38a9410849c8f4aef078535742198e942fba0e9bd", size = 4984460, upload-time = "2025-12-12T17:30:32.073Z" }, + { url = "https://files.pythonhosted.org/packages/f7/f1/a47f1d30b3dc00d75e7af762652d4cbc3dff5c2697a0dbd5203c81afd9c3/fonttools-4.61.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:41ed4b5ec103bd306bb68f81dc166e77409e5209443e5773cb4ed837bcc9b0d3", size = 4925800, upload-time = "2025-12-12T17:30:34.339Z" }, + { url = "https://files.pythonhosted.org/packages/a7/01/e6ae64a0981076e8a66906fab01539799546181e32a37a0257b77e4aa88b/fonttools-4.61.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b501c862d4901792adaec7c25b1ecc749e2662543f68bb194c42ba18d6eec98d", size = 5067859, upload-time = "2025-12-12T17:30:36.593Z" }, + { url = "https://files.pythonhosted.org/packages/73/aa/28e40b8d6809a9b5075350a86779163f074d2b617c15d22343fce81918db/fonttools-4.61.1-cp313-cp313-win32.whl", hash = "sha256:4d7092bb38c53bbc78e9255a59158b150bcdc115a1e3b3ce0b5f267dc35dd63c", size = 2267821, upload-time = "2025-12-12T17:30:38.478Z" }, + { url = "https://files.pythonhosted.org/packages/1a/59/453c06d1d83dc0951b69ef692d6b9f1846680342927df54e9a1ca91c6f90/fonttools-4.61.1-cp313-cp313-win_amd64.whl", hash = "sha256:21e7c8d76f62ab13c9472ccf74515ca5b9a761d1bde3265152a6dc58700d895b", size = 2318169, upload-time = "2025-12-12T17:30:40.951Z" }, + { url = "https://files.pythonhosted.org/packages/32/8f/4e7bf82c0cbb738d3c2206c920ca34ca74ef9dabde779030145d28665104/fonttools-4.61.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fff4f534200a04b4a36e7ae3cb74493afe807b517a09e99cb4faa89a34ed6ecd", size = 2846094, upload-time = "2025-12-12T17:30:43.511Z" }, + { url = "https://files.pythonhosted.org/packages/71/09/d44e45d0a4f3a651f23a1e9d42de43bc643cce2971b19e784cc67d823676/fonttools-4.61.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d9203500f7c63545b4ce3799319fe4d9feb1a1b89b28d3cb5abd11b9dd64147e", size = 2396589, upload-time = "2025-12-12T17:30:45.681Z" }, + { url = "https://files.pythonhosted.org/packages/89/18/58c64cafcf8eb677a99ef593121f719e6dcbdb7d1c594ae5a10d4997ca8a/fonttools-4.61.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa646ecec9528bef693415c79a86e733c70a4965dd938e9a226b0fc64c9d2e6c", size = 4877892, upload-time = "2025-12-12T17:30:47.709Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ec/9e6b38c7ba1e09eb51db849d5450f4c05b7e78481f662c3b79dbde6f3d04/fonttools-4.61.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11f35ad7805edba3aac1a3710d104592df59f4b957e30108ae0ba6c10b11dd75", size = 4972884, upload-time = "2025-12-12T17:30:49.656Z" }, + { url = "https://files.pythonhosted.org/packages/5e/87/b5339da8e0256734ba0dbbf5b6cdebb1dd79b01dc8c270989b7bcd465541/fonttools-4.61.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b931ae8f62db78861b0ff1ac017851764602288575d65b8e8ff1963fed419063", size = 4924405, upload-time = "2025-12-12T17:30:51.735Z" }, + { url = "https://files.pythonhosted.org/packages/0b/47/e3409f1e1e69c073a3a6fd8cb886eb18c0bae0ee13db2c8d5e7f8495e8b7/fonttools-4.61.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b148b56f5de675ee16d45e769e69f87623a4944f7443850bf9a9376e628a89d2", size = 5035553, upload-time = "2025-12-12T17:30:54.823Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b6/1f6600161b1073a984294c6c031e1a56ebf95b6164249eecf30012bb2e38/fonttools-4.61.1-cp314-cp314-win32.whl", hash = "sha256:9b666a475a65f4e839d3d10473fad6d47e0a9db14a2f4a224029c5bfde58ad2c", size = 2271915, upload-time = "2025-12-12T17:30:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/52/7b/91e7b01e37cc8eb0e1f770d08305b3655e4f002fc160fb82b3390eabacf5/fonttools-4.61.1-cp314-cp314-win_amd64.whl", hash = "sha256:4f5686e1fe5fce75d82d93c47a438a25bf0d1319d2843a926f741140b2b16e0c", size = 2323487, upload-time = "2025-12-12T17:30:59.804Z" }, + { url = "https://files.pythonhosted.org/packages/39/5c/908ad78e46c61c3e3ed70c3b58ff82ab48437faf84ec84f109592cabbd9f/fonttools-4.61.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e76ce097e3c57c4bcb67c5aa24a0ecdbd9f74ea9219997a707a4061fbe2707aa", size = 2929571, upload-time = "2025-12-12T17:31:02.574Z" }, + { url = "https://files.pythonhosted.org/packages/bd/41/975804132c6dea64cdbfbaa59f3518a21c137a10cccf962805b301ac6ab2/fonttools-4.61.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9cfef3ab326780c04d6646f68d4b4742aae222e8b8ea1d627c74e38afcbc9d91", size = 2435317, upload-time = "2025-12-12T17:31:04.974Z" }, + { url = "https://files.pythonhosted.org/packages/b0/5a/aef2a0a8daf1ebaae4cfd83f84186d4a72ee08fd6a8451289fcd03ffa8a4/fonttools-4.61.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a75c301f96db737e1c5ed5fd7d77d9c34466de16095a266509e13da09751bd19", size = 4882124, upload-time = "2025-12-12T17:31:07.456Z" }, + { url = "https://files.pythonhosted.org/packages/80/33/d6db3485b645b81cea538c9d1c9219d5805f0877fda18777add4671c5240/fonttools-4.61.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:91669ccac46bbc1d09e9273546181919064e8df73488ea087dcac3e2968df9ba", size = 5100391, upload-time = "2025-12-12T17:31:09.732Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d6/675ba631454043c75fcf76f0ca5463eac8eb0666ea1d7badae5fea001155/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c33ab3ca9d3ccd581d58e989d67554e42d8d4ded94ab3ade3508455fe70e65f7", size = 4978800, upload-time = "2025-12-12T17:31:11.681Z" }, + { url = "https://files.pythonhosted.org/packages/7f/33/d3ec753d547a8d2bdaedd390d4a814e8d5b45a093d558f025c6b990b554c/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:664c5a68ec406f6b1547946683008576ef8b38275608e1cee6c061828171c118", size = 5006426, upload-time = "2025-12-12T17:31:13.764Z" }, + { url = "https://files.pythonhosted.org/packages/b4/40/cc11f378b561a67bea850ab50063366a0d1dd3f6d0a30ce0f874b0ad5664/fonttools-4.61.1-cp314-cp314t-win32.whl", hash = "sha256:aed04cabe26f30c1647ef0e8fbb207516fd40fe9472e9439695f5c6998e60ac5", size = 2335377, upload-time = "2025-12-12T17:31:16.49Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ff/c9a2b66b39f8628531ea58b320d66d951267c98c6a38684daa8f50fb02f8/fonttools-4.61.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2180f14c141d2f0f3da43f3a81bc8aa4684860f6b0e6f9e165a4831f24e6a23b", size = 2400613, upload-time = "2025-12-12T17:31:18.769Z" }, + { url = "https://files.pythonhosted.org/packages/c7/4e/ce75a57ff3aebf6fc1f4e9d508b8e5810618a33d900ad6c19eb30b290b97/fonttools-4.61.1-py3-none-any.whl", hash = "sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371", size = 1148996, upload-time = "2025-12-12T17:31:21.03Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -414,6 +532,78 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] +[[package]] +name = "kiwisolver" +version = "1.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" }, + { url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756, upload-time = "2025-08-10T21:26:13.096Z" }, + { url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404, upload-time = "2025-08-10T21:26:14.457Z" }, + { url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410, upload-time = "2025-08-10T21:26:15.73Z" }, + { url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631, upload-time = "2025-08-10T21:26:17.045Z" }, + { url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963, upload-time = "2025-08-10T21:26:18.737Z" }, + { url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295, upload-time = "2025-08-10T21:26:20.11Z" }, + { url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987, upload-time = "2025-08-10T21:26:21.49Z" }, + { url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817, upload-time = "2025-08-10T21:26:22.812Z" }, + { url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895, upload-time = "2025-08-10T21:26:24.37Z" }, + { url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" }, + { url = "https://files.pythonhosted.org/packages/31/c1/c2686cda909742ab66c7388e9a1a8521a59eb89f8bcfbee28fc980d07e24/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8", size = 123681, upload-time = "2025-08-10T21:26:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f0/f44f50c9f5b1a1860261092e3bc91ecdc9acda848a8b8c6abfda4a24dd5c/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2", size = 66464, upload-time = "2025-08-10T21:26:27.733Z" }, + { url = "https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f", size = 64961, upload-time = "2025-08-10T21:26:28.729Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098", size = 1474607, upload-time = "2025-08-10T21:26:29.798Z" }, + { url = "https://files.pythonhosted.org/packages/d9/28/aac26d4c882f14de59041636292bc838db8961373825df23b8eeb807e198/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed", size = 1276546, upload-time = "2025-08-10T21:26:31.401Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ad/8bfc1c93d4cc565e5069162f610ba2f48ff39b7de4b5b8d93f69f30c4bed/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525", size = 1294482, upload-time = "2025-08-10T21:26:32.721Z" }, + { url = "https://files.pythonhosted.org/packages/da/f1/6aca55ff798901d8ce403206d00e033191f63d82dd708a186e0ed2067e9c/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78", size = 1343720, upload-time = "2025-08-10T21:26:34.032Z" }, + { url = "https://files.pythonhosted.org/packages/d1/91/eed031876c595c81d90d0f6fc681ece250e14bf6998c3d7c419466b523b7/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b", size = 2224907, upload-time = "2025-08-10T21:26:35.824Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ec/4d1925f2e49617b9cca9c34bfa11adefad49d00db038e692a559454dfb2e/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799", size = 2321334, upload-time = "2025-08-10T21:26:37.534Z" }, + { url = "https://files.pythonhosted.org/packages/43/cb/450cd4499356f68802750c6ddc18647b8ea01ffa28f50d20598e0befe6e9/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3", size = 2488313, upload-time = "2025-08-10T21:26:39.191Z" }, + { url = "https://files.pythonhosted.org/packages/71/67/fc76242bd99f885651128a5d4fa6083e5524694b7c88b489b1b55fdc491d/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c", size = 2291970, upload-time = "2025-08-10T21:26:40.828Z" }, + { url = "https://files.pythonhosted.org/packages/75/bd/f1a5d894000941739f2ae1b65a32892349423ad49c2e6d0771d0bad3fae4/kiwisolver-1.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d", size = 73894, upload-time = "2025-08-10T21:26:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/95/38/dce480814d25b99a391abbddadc78f7c117c6da34be68ca8b02d5848b424/kiwisolver-1.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2", size = 64995, upload-time = "2025-08-10T21:26:43.889Z" }, + { url = "https://files.pythonhosted.org/packages/e2/37/7d218ce5d92dadc5ebdd9070d903e0c7cf7edfe03f179433ac4d13ce659c/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1", size = 126510, upload-time = "2025-08-10T21:26:44.915Z" }, + { url = "https://files.pythonhosted.org/packages/23/b0/e85a2b48233daef4b648fb657ebbb6f8367696a2d9548a00b4ee0eb67803/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1", size = 67903, upload-time = "2025-08-10T21:26:45.934Z" }, + { url = "https://files.pythonhosted.org/packages/44/98/f2425bc0113ad7de24da6bb4dae1343476e95e1d738be7c04d31a5d037fd/kiwisolver-1.4.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11", size = 66402, upload-time = "2025-08-10T21:26:47.101Z" }, + { url = "https://files.pythonhosted.org/packages/98/d8/594657886df9f34c4177cc353cc28ca7e6e5eb562d37ccc233bff43bbe2a/kiwisolver-1.4.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c", size = 1582135, upload-time = "2025-08-10T21:26:48.665Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c6/38a115b7170f8b306fc929e166340c24958347308ea3012c2b44e7e295db/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197", size = 1389409, upload-time = "2025-08-10T21:26:50.335Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3b/e04883dace81f24a568bcee6eb3001da4ba05114afa622ec9b6fafdc1f5e/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c", size = 1401763, upload-time = "2025-08-10T21:26:51.867Z" }, + { url = "https://files.pythonhosted.org/packages/9f/80/20ace48e33408947af49d7d15c341eaee69e4e0304aab4b7660e234d6288/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185", size = 1453643, upload-time = "2025-08-10T21:26:53.592Z" }, + { url = "https://files.pythonhosted.org/packages/64/31/6ce4380a4cd1f515bdda976a1e90e547ccd47b67a1546d63884463c92ca9/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748", size = 2330818, upload-time = "2025-08-10T21:26:55.051Z" }, + { url = "https://files.pythonhosted.org/packages/fa/e9/3f3fcba3bcc7432c795b82646306e822f3fd74df0ee81f0fa067a1f95668/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64", size = 2419963, upload-time = "2025-08-10T21:26:56.421Z" }, + { url = "https://files.pythonhosted.org/packages/99/43/7320c50e4133575c66e9f7dadead35ab22d7c012a3b09bb35647792b2a6d/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff", size = 2594639, upload-time = "2025-08-10T21:26:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/65/d6/17ae4a270d4a987ef8a385b906d2bdfc9fce502d6dc0d3aea865b47f548c/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07", size = 2391741, upload-time = "2025-08-10T21:26:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8f/8f6f491d595a9e5912971f3f863d81baddccc8a4d0c3749d6a0dd9ffc9df/kiwisolver-1.4.9-cp313-cp313t-win_arm64.whl", hash = "sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c", size = 68646, upload-time = "2025-08-10T21:27:00.52Z" }, + { url = "https://files.pythonhosted.org/packages/6b/32/6cc0fbc9c54d06c2969faa9c1d29f5751a2e51809dd55c69055e62d9b426/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386", size = 123806, upload-time = "2025-08-10T21:27:01.537Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/2bfb1d4a4823d92e8cbb420fe024b8d2167f72079b3bb941207c42570bdf/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552", size = 66605, upload-time = "2025-08-10T21:27:03.335Z" }, + { url = "https://files.pythonhosted.org/packages/f7/69/00aafdb4e4509c2ca6064646cba9cd4b37933898f426756adb2cb92ebbed/kiwisolver-1.4.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3", size = 64925, upload-time = "2025-08-10T21:27:04.339Z" }, + { url = "https://files.pythonhosted.org/packages/43/dc/51acc6791aa14e5cb6d8a2e28cefb0dc2886d8862795449d021334c0df20/kiwisolver-1.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58", size = 1472414, upload-time = "2025-08-10T21:27:05.437Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bb/93fa64a81db304ac8a246f834d5094fae4b13baf53c839d6bb6e81177129/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4", size = 1281272, upload-time = "2025-08-10T21:27:07.063Z" }, + { url = "https://files.pythonhosted.org/packages/70/e6/6df102916960fb8d05069d4bd92d6d9a8202d5a3e2444494e7cd50f65b7a/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df", size = 1298578, upload-time = "2025-08-10T21:27:08.452Z" }, + { url = "https://files.pythonhosted.org/packages/7c/47/e142aaa612f5343736b087864dbaebc53ea8831453fb47e7521fa8658f30/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6", size = 1345607, upload-time = "2025-08-10T21:27:10.125Z" }, + { url = "https://files.pythonhosted.org/packages/54/89/d641a746194a0f4d1a3670fb900d0dbaa786fb98341056814bc3f058fa52/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5", size = 2230150, upload-time = "2025-08-10T21:27:11.484Z" }, + { url = "https://files.pythonhosted.org/packages/aa/6b/5ee1207198febdf16ac11f78c5ae40861b809cbe0e6d2a8d5b0b3044b199/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf", size = 2325979, upload-time = "2025-08-10T21:27:12.917Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ff/b269eefd90f4ae14dcc74973d5a0f6d28d3b9bb1afd8c0340513afe6b39a/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5", size = 2491456, upload-time = "2025-08-10T21:27:14.353Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d4/10303190bd4d30de547534601e259a4fbf014eed94aae3e5521129215086/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce", size = 2294621, upload-time = "2025-08-10T21:27:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/28/e0/a9a90416fce5c0be25742729c2ea52105d62eda6c4be4d803c2a7be1fa50/kiwisolver-1.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7", size = 75417, upload-time = "2025-08-10T21:27:17.436Z" }, + { url = "https://files.pythonhosted.org/packages/1f/10/6949958215b7a9a264299a7db195564e87900f709db9245e4ebdd3c70779/kiwisolver-1.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c", size = 66582, upload-time = "2025-08-10T21:27:18.436Z" }, + { url = "https://files.pythonhosted.org/packages/ec/79/60e53067903d3bc5469b369fe0dfc6b3482e2133e85dae9daa9527535991/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548", size = 126514, upload-time = "2025-08-10T21:27:19.465Z" }, + { url = "https://files.pythonhosted.org/packages/25/d1/4843d3e8d46b072c12a38c97c57fab4608d36e13fe47d47ee96b4d61ba6f/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d", size = 67905, upload-time = "2025-08-10T21:27:20.51Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ae/29ffcbd239aea8b93108de1278271ae764dfc0d803a5693914975f200596/kiwisolver-1.4.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c", size = 66399, upload-time = "2025-08-10T21:27:21.496Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ae/d7ba902aa604152c2ceba5d352d7b62106bedbccc8e95c3934d94472bfa3/kiwisolver-1.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122", size = 1582197, upload-time = "2025-08-10T21:27:22.604Z" }, + { url = "https://files.pythonhosted.org/packages/f2/41/27c70d427eddb8bc7e4f16420a20fefc6f480312122a59a959fdfe0445ad/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64", size = 1390125, upload-time = "2025-08-10T21:27:24.036Z" }, + { url = "https://files.pythonhosted.org/packages/41/42/b3799a12bafc76d962ad69083f8b43b12bf4fe78b097b12e105d75c9b8f1/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134", size = 1402612, upload-time = "2025-08-10T21:27:25.773Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b5/a210ea073ea1cfaca1bb5c55a62307d8252f531beb364e18aa1e0888b5a0/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370", size = 1453990, upload-time = "2025-08-10T21:27:27.089Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ce/a829eb8c033e977d7ea03ed32fb3c1781b4fa0433fbadfff29e39c676f32/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21", size = 2331601, upload-time = "2025-08-10T21:27:29.343Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4b/b5e97eb142eb9cd0072dacfcdcd31b1c66dc7352b0f7c7255d339c0edf00/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a", size = 2422041, upload-time = "2025-08-10T21:27:30.754Z" }, + { url = "https://files.pythonhosted.org/packages/40/be/8eb4cd53e1b85ba4edc3a9321666f12b83113a178845593307a3e7891f44/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f", size = 2594897, upload-time = "2025-08-10T21:27:32.803Z" }, + { url = "https://files.pythonhosted.org/packages/99/dd/841e9a66c4715477ea0abc78da039832fbb09dac5c35c58dc4c41a407b8a/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369", size = 2391835, upload-time = "2025-08-10T21:27:34.23Z" }, + { url = "https://files.pythonhosted.org/packages/0c/28/4b2e5c47a0da96896fdfdb006340ade064afa1e63675d01ea5ac222b6d52/kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891", size = 79988, upload-time = "2025-08-10T21:27:35.587Z" }, + { url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260, upload-time = "2025-08-10T21:27:36.606Z" }, +] + [[package]] name = "lsprotocol" version = "2025.0.0" @@ -502,6 +692,60 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] +[[package]] +name = "matplotlib" +version = "3.10.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/67/f997cdcbb514012eb0d10cd2b4b332667997fb5ebe26b8d41d04962fa0e6/matplotlib-3.10.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", size = 8260453, upload-time = "2025-12-10T22:55:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", size = 8148321, upload-time = "2025-12-10T22:55:33.265Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", size = 8716944, upload-time = "2025-12-10T22:55:34.922Z" }, + { url = "https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", size = 9550099, upload-time = "2025-12-10T22:55:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/57/61/78cd5920d35b29fd2a0fe894de8adf672ff52939d2e9b43cb83cd5ce1bc7/matplotlib-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", size = 9613040, upload-time = "2025-12-10T22:55:38.715Z" }, + { url = "https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", size = 8142717, upload-time = "2025-12-10T22:55:41.103Z" }, + { url = "https://files.pythonhosted.org/packages/f1/76/934db220026b5fef85f45d51a738b91dea7d70207581063cd9bd8fafcf74/matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", size = 8012751, upload-time = "2025-12-10T22:55:42.684Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b9/15fd5541ef4f5b9a17eefd379356cf12175fe577424e7b1d80676516031a/matplotlib-3.10.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6", size = 8261076, upload-time = "2025-12-10T22:55:44.648Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a0/2ba3473c1b66b9c74dc7107c67e9008cb1782edbe896d4c899d39ae9cf78/matplotlib-3.10.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1", size = 8148794, upload-time = "2025-12-10T22:55:46.252Z" }, + { url = "https://files.pythonhosted.org/packages/75/97/a471f1c3eb1fd6f6c24a31a5858f443891d5127e63a7788678d14e249aea/matplotlib-3.10.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486", size = 8718474, upload-time = "2025-12-10T22:55:47.864Z" }, + { url = "https://files.pythonhosted.org/packages/01/be/cd478f4b66f48256f42927d0acbcd63a26a893136456cd079c0cc24fbabf/matplotlib-3.10.8-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce", size = 9549637, upload-time = "2025-12-10T22:55:50.048Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8dc289776eae5109e268c4fb92baf870678dc048a25d4ac903683b86d5bf/matplotlib-3.10.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6", size = 9613678, upload-time = "2025-12-10T22:55:52.21Z" }, + { url = "https://files.pythonhosted.org/packages/64/40/37612487cc8a437d4dd261b32ca21fe2d79510fe74af74e1f42becb1bdb8/matplotlib-3.10.8-cp313-cp313-win_amd64.whl", hash = "sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149", size = 8142686, upload-time = "2025-12-10T22:55:54.253Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/8d8a8730e968185514680c2a6625943f70269509c3dcfc0dcf7d75928cb8/matplotlib-3.10.8-cp313-cp313-win_arm64.whl", hash = "sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645", size = 8012917, upload-time = "2025-12-10T22:55:56.268Z" }, + { url = "https://files.pythonhosted.org/packages/b5/27/51fe26e1062f298af5ef66343d8ef460e090a27fea73036c76c35821df04/matplotlib-3.10.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077", size = 8305679, upload-time = "2025-12-10T22:55:57.856Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1e/4de865bc591ac8e3062e835f42dd7fe7a93168d519557837f0e37513f629/matplotlib-3.10.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22", size = 8198336, upload-time = "2025-12-10T22:55:59.371Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cb/2f7b6e75fb4dce87ef91f60cac4f6e34f4c145ab036a22318ec837971300/matplotlib-3.10.8-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39", size = 8731653, upload-time = "2025-12-10T22:56:01.032Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/bd9c57d6ba670a37ab31fb87ec3e8691b947134b201f881665b28cc039ff/matplotlib-3.10.8-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565", size = 9561356, upload-time = "2025-12-10T22:56:02.95Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3d/8b94a481456dfc9dfe6e39e93b5ab376e50998cddfd23f4ae3b431708f16/matplotlib-3.10.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a", size = 9614000, upload-time = "2025-12-10T22:56:05.411Z" }, + { url = "https://files.pythonhosted.org/packages/bd/cd/bc06149fe5585ba800b189a6a654a75f1f127e8aab02fd2be10df7fa500c/matplotlib-3.10.8-cp313-cp313t-win_amd64.whl", hash = "sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958", size = 8220043, upload-time = "2025-12-10T22:56:07.551Z" }, + { url = "https://files.pythonhosted.org/packages/e3/de/b22cf255abec916562cc04eef457c13e58a1990048de0c0c3604d082355e/matplotlib-3.10.8-cp313-cp313t-win_arm64.whl", hash = "sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5", size = 8062075, upload-time = "2025-12-10T22:56:09.178Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/9c0ff7a2f11615e516c3b058e1e6e8f9614ddeca53faca06da267c48345d/matplotlib-3.10.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f", size = 8262481, upload-time = "2025-12-10T22:56:10.885Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ca/e8ae28649fcdf039fda5ef554b40a95f50592a3c47e6f7270c9561c12b07/matplotlib-3.10.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b", size = 8151473, upload-time = "2025-12-10T22:56:12.377Z" }, + { url = "https://files.pythonhosted.org/packages/f1/6f/009d129ae70b75e88cbe7e503a12a4c0670e08ed748a902c2568909e9eb5/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d", size = 9553896, upload-time = "2025-12-10T22:56:14.432Z" }, + { url = "https://files.pythonhosted.org/packages/f5/26/4221a741eb97967bc1fd5e4c52b9aa5a91b2f4ec05b59f6def4d820f9df9/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008", size = 9824193, upload-time = "2025-12-10T22:56:16.29Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/3abf75f38605772cf48a9daf5821cd4f563472f38b4b828c6fba6fa6d06e/matplotlib-3.10.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c", size = 9615444, upload-time = "2025-12-10T22:56:18.155Z" }, + { url = "https://files.pythonhosted.org/packages/93/a5/de89ac80f10b8dc615807ee1133cd99ac74082581196d4d9590bea10690d/matplotlib-3.10.8-cp314-cp314-win_amd64.whl", hash = "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11", size = 8272719, upload-time = "2025-12-10T22:56:20.366Z" }, + { url = "https://files.pythonhosted.org/packages/69/ce/b006495c19ccc0a137b48083168a37bd056392dee02f87dba0472f2797fe/matplotlib-3.10.8-cp314-cp314-win_arm64.whl", hash = "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8", size = 8144205, upload-time = "2025-12-10T22:56:22.239Z" }, + { url = "https://files.pythonhosted.org/packages/68/d9/b31116a3a855bd313c6fcdb7226926d59b041f26061c6c5b1be66a08c826/matplotlib-3.10.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50", size = 8305785, upload-time = "2025-12-10T22:56:24.218Z" }, + { url = "https://files.pythonhosted.org/packages/1e/90/6effe8103f0272685767ba5f094f453784057072f49b393e3ea178fe70a5/matplotlib-3.10.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908", size = 8198361, upload-time = "2025-12-10T22:56:26.787Z" }, + { url = "https://files.pythonhosted.org/packages/d7/65/a73188711bea603615fc0baecca1061429ac16940e2385433cc778a9d8e7/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a", size = 9561357, upload-time = "2025-12-10T22:56:28.953Z" }, + { url = "https://files.pythonhosted.org/packages/f4/3d/b5c5d5d5be8ce63292567f0e2c43dde9953d3ed86ac2de0a72e93c8f07a1/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1", size = 9823610, upload-time = "2025-12-10T22:56:31.455Z" }, + { url = "https://files.pythonhosted.org/packages/4d/4b/e7beb6bbd49f6bae727a12b270a2654d13c397576d25bd6786e47033300f/matplotlib-3.10.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", size = 9614011, upload-time = "2025-12-10T22:56:33.85Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e6/76f2813d31f032e65f6f797e3f2f6e4aab95b65015924b1c51370395c28a/matplotlib-3.10.8-cp314-cp314t-win_amd64.whl", hash = "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", size = 8362801, upload-time = "2025-12-10T22:56:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/5d/49/d651878698a0b67f23aa28e17f45a6d6dd3d3f933fa29087fa4ce5947b5a/matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", size = 8192560, upload-time = "2025-12-10T22:56:38.008Z" }, +] + [[package]] name = "mccabe" version = "0.7.0" @@ -561,6 +805,67 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] +[[package]] +name = "numpy" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/62/ae72ff66c0f1fd959925b4c11f8c2dea61f47f6acaea75a08512cdfe3fed/numpy-2.4.1.tar.gz", hash = "sha256:a1ceafc5042451a858231588a104093474c6a5c57dcc724841f5c888d237d690", size = 20721320, upload-time = "2026-01-10T06:44:59.619Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/7f/ec53e32bf10c813604edf07a3682616bd931d026fcde7b6d13195dfb684a/numpy-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d3703409aac693fa82c0aee023a1ae06a6e9d065dba10f5e8e80f642f1e9d0a2", size = 16656888, upload-time = "2026-01-10T06:42:40.913Z" }, + { url = "https://files.pythonhosted.org/packages/b8/e0/1f9585d7dae8f14864e948fd7fa86c6cb72dee2676ca2748e63b1c5acfe0/numpy-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7211b95ca365519d3596a1d8688a95874cc94219d417504d9ecb2df99fa7bfa8", size = 12373956, upload-time = "2026-01-10T06:42:43.091Z" }, + { url = "https://files.pythonhosted.org/packages/8e/43/9762e88909ff2326f5e7536fa8cb3c49fb03a7d92705f23e6e7f553d9cb3/numpy-2.4.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5adf01965456a664fc727ed69cc71848f28d063217c63e1a0e200a118d5eec9a", size = 5202567, upload-time = "2026-01-10T06:42:45.107Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ee/34b7930eb61e79feb4478800a4b95b46566969d837546aa7c034c742ef98/numpy-2.4.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:26f0bcd9c79a00e339565b303badc74d3ea2bd6d52191eeca5f95936cad107d0", size = 6549459, upload-time = "2026-01-10T06:42:48.152Z" }, + { url = "https://files.pythonhosted.org/packages/79/e3/5f115fae982565771be994867c89bcd8d7208dbfe9469185497d70de5ddf/numpy-2.4.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0093e85df2960d7e4049664b26afc58b03236e967fb942354deef3208857a04c", size = 14404859, upload-time = "2026-01-10T06:42:49.947Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7d/9c8a781c88933725445a859cac5d01b5871588a15969ee6aeb618ba99eee/numpy-2.4.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad270f438cbdd402c364980317fb6b117d9ec5e226fff5b4148dd9aa9fc6e02", size = 16371419, upload-time = "2026-01-10T06:42:52.409Z" }, + { url = "https://files.pythonhosted.org/packages/a6/d2/8aa084818554543f17cf4162c42f162acbd3bb42688aefdba6628a859f77/numpy-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:297c72b1b98100c2e8f873d5d35fb551fce7040ade83d67dd51d38c8d42a2162", size = 16182131, upload-time = "2026-01-10T06:42:54.694Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/0425216684297c58a8df35f3284ef56ec4a043e6d283f8a59c53562caf1b/numpy-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf6470d91d34bf669f61d515499859fa7a4c2f7c36434afb70e82df7217933f9", size = 18295342, upload-time = "2026-01-10T06:42:56.991Z" }, + { url = "https://files.pythonhosted.org/packages/31/4c/14cb9d86240bd8c386c881bafbe43f001284b7cce3bc01623ac9475da163/numpy-2.4.1-cp312-cp312-win32.whl", hash = "sha256:b6bcf39112e956594b3331316d90c90c90fb961e39696bda97b89462f5f3943f", size = 5959015, upload-time = "2026-01-10T06:42:59.631Z" }, + { url = "https://files.pythonhosted.org/packages/51/cf/52a703dbeb0c65807540d29699fef5fda073434ff61846a564d5c296420f/numpy-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:e1a27bb1b2dee45a2a53f5ca6ff2d1a7f135287883a1689e930d44d1ff296c87", size = 12310730, upload-time = "2026-01-10T06:43:01.627Z" }, + { url = "https://files.pythonhosted.org/packages/69/80/a828b2d0ade5e74a9fe0f4e0a17c30fdc26232ad2bc8c9f8b3197cf7cf18/numpy-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:0e6e8f9d9ecf95399982019c01223dc130542960a12edfa8edd1122dfa66a8a8", size = 10312166, upload-time = "2026-01-10T06:43:03.673Z" }, + { url = "https://files.pythonhosted.org/packages/04/68/732d4b7811c00775f3bd522a21e8dd5a23f77eb11acdeb663e4a4ebf0ef4/numpy-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d797454e37570cfd61143b73b8debd623c3c0952959adb817dd310a483d58a1b", size = 16652495, upload-time = "2026-01-10T06:43:06.283Z" }, + { url = "https://files.pythonhosted.org/packages/20/ca/857722353421a27f1465652b2c66813eeeccea9d76d5f7b74b99f298e60e/numpy-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82c55962006156aeef1629b953fd359064aa47e4d82cfc8e67f0918f7da3344f", size = 12368657, upload-time = "2026-01-10T06:43:09.094Z" }, + { url = "https://files.pythonhosted.org/packages/81/0d/2377c917513449cc6240031a79d30eb9a163d32a91e79e0da47c43f2c0c8/numpy-2.4.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:71abbea030f2cfc3092a0ff9f8c8fdefdc5e0bf7d9d9c99663538bb0ecdac0b9", size = 5197256, upload-time = "2026-01-10T06:43:13.634Z" }, + { url = "https://files.pythonhosted.org/packages/17/39/569452228de3f5de9064ac75137082c6214be1f5c532016549a7923ab4b5/numpy-2.4.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5b55aa56165b17aaf15520beb9cbd33c9039810e0d9643dd4379e44294c7303e", size = 6545212, upload-time = "2026-01-10T06:43:15.661Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a4/77333f4d1e4dac4395385482557aeecf4826e6ff517e32ca48e1dafbe42a/numpy-2.4.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0faba4a331195bfa96f93dd9dfaa10b2c7aa8cda3a02b7fd635e588fe821bf5", size = 14402871, upload-time = "2026-01-10T06:43:17.324Z" }, + { url = "https://files.pythonhosted.org/packages/ba/87/d341e519956273b39d8d47969dd1eaa1af740615394fe67d06f1efa68773/numpy-2.4.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e3087f53e2b4428766b54932644d148613c5a595150533ae7f00dab2f319a8", size = 16359305, upload-time = "2026-01-10T06:43:19.376Z" }, + { url = "https://files.pythonhosted.org/packages/32/91/789132c6666288eaa20ae8066bb99eba1939362e8f1a534949a215246e97/numpy-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:49e792ec351315e16da54b543db06ca8a86985ab682602d90c60ef4ff4db2a9c", size = 16181909, upload-time = "2026-01-10T06:43:21.808Z" }, + { url = "https://files.pythonhosted.org/packages/cf/b8/090b8bd27b82a844bb22ff8fdf7935cb1980b48d6e439ae116f53cdc2143/numpy-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79e9e06c4c2379db47f3f6fc7a8652e7498251789bf8ff5bd43bf478ef314ca2", size = 18284380, upload-time = "2026-01-10T06:43:23.957Z" }, + { url = "https://files.pythonhosted.org/packages/67/78/722b62bd31842ff029412271556a1a27a98f45359dea78b1548a3a9996aa/numpy-2.4.1-cp313-cp313-win32.whl", hash = "sha256:3d1a100e48cb266090a031397863ff8a30050ceefd798f686ff92c67a486753d", size = 5957089, upload-time = "2026-01-10T06:43:27.535Z" }, + { url = "https://files.pythonhosted.org/packages/da/a6/cf32198b0b6e18d4fbfa9a21a992a7fca535b9bb2b0cdd217d4a3445b5ca/numpy-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:92a0e65272fd60bfa0d9278e0484c2f52fe03b97aedc02b357f33fe752c52ffb", size = 12307230, upload-time = "2026-01-10T06:43:29.298Z" }, + { url = "https://files.pythonhosted.org/packages/44/6c/534d692bfb7d0afe30611320c5fb713659dcb5104d7cc182aff2aea092f5/numpy-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:20d4649c773f66cc2fc36f663e091f57c3b7655f936a4c681b4250855d1da8f5", size = 10313125, upload-time = "2026-01-10T06:43:31.782Z" }, + { url = "https://files.pythonhosted.org/packages/da/a1/354583ac5c4caa566de6ddfbc42744409b515039e085fab6e0ff942e0df5/numpy-2.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f93bc6892fe7b0663e5ffa83b61aab510aacffd58c16e012bb9352d489d90cb7", size = 12496156, upload-time = "2026-01-10T06:43:34.237Z" }, + { url = "https://files.pythonhosted.org/packages/51/b0/42807c6e8cce58c00127b1dc24d365305189991f2a7917aa694a109c8d7d/numpy-2.4.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:178de8f87948163d98a4c9ab5bee4ce6519ca918926ec8df195af582de28544d", size = 5324663, upload-time = "2026-01-10T06:43:36.211Z" }, + { url = "https://files.pythonhosted.org/packages/fe/55/7a621694010d92375ed82f312b2f28017694ed784775269115323e37f5e2/numpy-2.4.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:98b35775e03ab7f868908b524fc0a84d38932d8daf7b7e1c3c3a1b6c7a2c9f15", size = 6645224, upload-time = "2026-01-10T06:43:37.884Z" }, + { url = "https://files.pythonhosted.org/packages/50/96/9fa8635ed9d7c847d87e30c834f7109fac5e88549d79ef3324ab5c20919f/numpy-2.4.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:941c2a93313d030f219f3a71fd3d91a728b82979a5e8034eb2e60d394a2b83f9", size = 14462352, upload-time = "2026-01-10T06:43:39.479Z" }, + { url = "https://files.pythonhosted.org/packages/03/d1/8cf62d8bb2062da4fb82dd5d49e47c923f9c0738032f054e0a75342faba7/numpy-2.4.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:529050522e983e00a6c1c6b67411083630de8b57f65e853d7b03d9281b8694d2", size = 16407279, upload-time = "2026-01-10T06:43:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/86/1c/95c86e17c6b0b31ce6ef219da00f71113b220bcb14938c8d9a05cee0ff53/numpy-2.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2302dc0224c1cbc49bb94f7064f3f923a971bfae45c33870dcbff63a2a550505", size = 16248316, upload-time = "2026-01-10T06:43:44.121Z" }, + { url = "https://files.pythonhosted.org/packages/30/b4/e7f5ff8697274c9d0fa82398b6a372a27e5cef069b37df6355ccb1f1db1a/numpy-2.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9171a42fcad32dcf3fa86f0a4faa5e9f8facefdb276f54b8b390d90447cff4e2", size = 18329884, upload-time = "2026-01-10T06:43:46.613Z" }, + { url = "https://files.pythonhosted.org/packages/37/a4/b073f3e9d77f9aec8debe8ca7f9f6a09e888ad1ba7488f0c3b36a94c03ac/numpy-2.4.1-cp313-cp313t-win32.whl", hash = "sha256:382ad67d99ef49024f11d1ce5dcb5ad8432446e4246a4b014418ba3a1175a1f4", size = 6081138, upload-time = "2026-01-10T06:43:48.854Z" }, + { url = "https://files.pythonhosted.org/packages/16/16/af42337b53844e67752a092481ab869c0523bc95c4e5c98e4dac4e9581ac/numpy-2.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:62fea415f83ad8fdb6c20840578e5fbaf5ddd65e0ec6c3c47eda0f69da172510", size = 12447478, upload-time = "2026-01-10T06:43:50.476Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f8/fa85b2eac68ec631d0b631abc448552cb17d39afd17ec53dcbcc3537681a/numpy-2.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a7870e8c5fc11aef57d6fea4b4085e537a3a60ad2cdd14322ed531fdca68d261", size = 10382981, upload-time = "2026-01-10T06:43:52.575Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a7/ef08d25698e0e4b4efbad8d55251d20fe2a15f6d9aa7c9b30cd03c165e6f/numpy-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3869ea1ee1a1edc16c29bbe3a2f2a4e515cc3a44d43903ad41e0cacdbaf733dc", size = 16652046, upload-time = "2026-01-10T06:43:54.797Z" }, + { url = "https://files.pythonhosted.org/packages/8f/39/e378b3e3ca13477e5ac70293ec027c438d1927f18637e396fe90b1addd72/numpy-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e867df947d427cdd7a60e3e271729090b0f0df80f5f10ab7dd436f40811699c3", size = 12378858, upload-time = "2026-01-10T06:43:57.099Z" }, + { url = "https://files.pythonhosted.org/packages/c3/74/7ec6154f0006910ed1fdbb7591cf4432307033102b8a22041599935f8969/numpy-2.4.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:e3bd2cb07841166420d2fa7146c96ce00cb3410664cbc1a6be028e456c4ee220", size = 5207417, upload-time = "2026-01-10T06:43:59.037Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b7/053ac11820d84e42f8feea5cb81cc4fcd1091499b45b1ed8c7415b1bf831/numpy-2.4.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:f0a90aba7d521e6954670550e561a4cb925713bd944445dbe9e729b71f6cabee", size = 6542643, upload-time = "2026-01-10T06:44:01.852Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c4/2e7908915c0e32ca636b92e4e4a3bdec4cb1e7eb0f8aedf1ed3c68a0d8cd/numpy-2.4.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d558123217a83b2d1ba316b986e9248a1ed1971ad495963d555ccd75dcb1556", size = 14418963, upload-time = "2026-01-10T06:44:04.047Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c0/3ed5083d94e7ffd7c404e54619c088e11f2e1939a9544f5397f4adb1b8ba/numpy-2.4.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f44de05659b67d20499cbc96d49f2650769afcb398b79b324bb6e297bfe3844", size = 16363811, upload-time = "2026-01-10T06:44:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/0e/68/42b66f1852bf525050a67315a4fb94586ab7e9eaa541b1bef530fab0c5dd/numpy-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:69e7419c9012c4aaf695109564e3387f1259f001b4326dfa55907b098af082d3", size = 16197643, upload-time = "2026-01-10T06:44:08.33Z" }, + { url = "https://files.pythonhosted.org/packages/d2/40/e8714fc933d85f82c6bfc7b998a0649ad9769a32f3494ba86598aaf18a48/numpy-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2ffd257026eb1b34352e749d7cc1678b5eeec3e329ad8c9965a797e08ccba205", size = 18289601, upload-time = "2026-01-10T06:44:10.841Z" }, + { url = "https://files.pythonhosted.org/packages/80/9a/0d44b468cad50315127e884802351723daca7cf1c98d102929468c81d439/numpy-2.4.1-cp314-cp314-win32.whl", hash = "sha256:727c6c3275ddefa0dc078524a85e064c057b4f4e71ca5ca29a19163c607be745", size = 6005722, upload-time = "2026-01-10T06:44:13.332Z" }, + { url = "https://files.pythonhosted.org/packages/7e/bb/c6513edcce5a831810e2dddc0d3452ce84d208af92405a0c2e58fd8e7881/numpy-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:7d5d7999df434a038d75a748275cd6c0094b0ecdb0837342b332a82defc4dc4d", size = 12438590, upload-time = "2026-01-10T06:44:15.006Z" }, + { url = "https://files.pythonhosted.org/packages/e9/da/a598d5cb260780cf4d255102deba35c1d072dc028c4547832f45dd3323a8/numpy-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:ce9ce141a505053b3c7bce3216071f3bf5c182b8b28930f14cd24d43932cd2df", size = 10596180, upload-time = "2026-01-10T06:44:17.386Z" }, + { url = "https://files.pythonhosted.org/packages/de/bc/ea3f2c96fcb382311827231f911723aeff596364eb6e1b6d1d91128aa29b/numpy-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e53170557d37ae404bf8d542ca5b7c629d6efa1117dac6a83e394142ea0a43f", size = 12498774, upload-time = "2026-01-10T06:44:19.467Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ab/ef9d939fe4a812648c7a712610b2ca6140b0853c5efea361301006c02ae5/numpy-2.4.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:a73044b752f5d34d4232f25f18160a1cc418ea4507f5f11e299d8ac36875f8a0", size = 5327274, upload-time = "2026-01-10T06:44:23.189Z" }, + { url = "https://files.pythonhosted.org/packages/bd/31/d381368e2a95c3b08b8cf7faac6004849e960f4a042d920337f71cef0cae/numpy-2.4.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:fb1461c99de4d040666ca0444057b06541e5642f800b71c56e6ea92d6a853a0c", size = 6648306, upload-time = "2026-01-10T06:44:25.012Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e5/0989b44ade47430be6323d05c23207636d67d7362a1796ccbccac6773dd2/numpy-2.4.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423797bdab2eeefbe608d7c1ec7b2b4fd3c58d51460f1ee26c7500a1d9c9ee93", size = 14464653, upload-time = "2026-01-10T06:44:26.706Z" }, + { url = "https://files.pythonhosted.org/packages/10/a7/cfbe475c35371cae1358e61f20c5f075badc18c4797ab4354140e1d283cf/numpy-2.4.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52b5f61bdb323b566b528899cc7db2ba5d1015bda7ea811a8bcf3c89c331fa42", size = 16405144, upload-time = "2026-01-10T06:44:29.378Z" }, + { url = "https://files.pythonhosted.org/packages/f8/a3/0c63fe66b534888fa5177cc7cef061541064dbe2b4b60dcc60ffaf0d2157/numpy-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42d7dd5fa36d16d52a84f821eb96031836fd405ee6955dd732f2023724d0aa01", size = 16247425, upload-time = "2026-01-10T06:44:31.721Z" }, + { url = "https://files.pythonhosted.org/packages/6b/2b/55d980cfa2c93bd40ff4c290bf824d792bd41d2fe3487b07707559071760/numpy-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7b6b5e28bbd47b7532698e5db2fe1db693d84b58c254e4389d99a27bb9b8f6b", size = 18330053, upload-time = "2026-01-10T06:44:34.617Z" }, + { url = "https://files.pythonhosted.org/packages/23/12/8b5fc6b9c487a09a7957188e0943c9ff08432c65e34567cabc1623b03a51/numpy-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:5de60946f14ebe15e713a6f22850c2372fa72f4ff9a432ab44aa90edcadaa65a", size = 6152482, upload-time = "2026-01-10T06:44:36.798Z" }, + { url = "https://files.pythonhosted.org/packages/00/a5/9f8ca5856b8940492fc24fbe13c1bc34d65ddf4079097cf9e53164d094e1/numpy-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:8f085da926c0d491ffff3096f91078cc97ea67e7e6b65e490bc8dcda65663be2", size = 12627117, upload-time = "2026-01-10T06:44:38.828Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0d/eca3d962f9eef265f01a8e0d20085c6dd1f443cbffc11b6dede81fd82356/numpy-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6436cffb4f2bf26c974344439439c95e152c9a527013f26b3577be6c2ca64295", size = 10667121, upload-time = "2026-01-10T06:44:41.644Z" }, +] + [[package]] name = "packaging" version = "25.0" @@ -579,6 +884,75 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, ] +[[package]] +name = "pillow" +version = "12.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/02/d52c733a2452ef1ffcc123b68e6606d07276b0e358db70eabad7e40042b7/pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9", size = 46977283, upload-time = "2026-01-02T09:13:29.892Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/31/dc53fe21a2f2996e1b7d92bf671cdb157079385183ef7c1ae08b485db510/pillow-12.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b", size = 5262642, upload-time = "2026-01-02T09:11:10.138Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c1/10e45ac9cc79419cedf5121b42dcca5a50ad2b601fa080f58c22fb27626e/pillow-12.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551", size = 4657464, upload-time = "2026-01-02T09:11:12.319Z" }, + { url = "https://files.pythonhosted.org/packages/ad/26/7b82c0ab7ef40ebede7a97c72d473bda5950f609f8e0c77b04af574a0ddb/pillow-12.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208", size = 6234878, upload-time = "2026-01-02T09:11:14.096Z" }, + { url = "https://files.pythonhosted.org/packages/76/25/27abc9792615b5e886ca9411ba6637b675f1b77af3104710ac7353fe5605/pillow-12.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5", size = 8044868, upload-time = "2026-01-02T09:11:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ea/f200a4c36d836100e7bc738fc48cd963d3ba6372ebc8298a889e0cfc3359/pillow-12.1.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661", size = 6349468, upload-time = "2026-01-02T09:11:17.631Z" }, + { url = "https://files.pythonhosted.org/packages/11/8f/48d0b77ab2200374c66d344459b8958c86693be99526450e7aee714e03e4/pillow-12.1.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17", size = 7041518, upload-time = "2026-01-02T09:11:19.389Z" }, + { url = "https://files.pythonhosted.org/packages/1d/23/c281182eb986b5d31f0a76d2a2c8cd41722d6fb8ed07521e802f9bba52de/pillow-12.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670", size = 6462829, upload-time = "2026-01-02T09:11:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/25/ef/7018273e0faac099d7b00982abdcc39142ae6f3bd9ceb06de09779c4a9d6/pillow-12.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616", size = 7166756, upload-time = "2026-01-02T09:11:23.559Z" }, + { url = "https://files.pythonhosted.org/packages/8f/c8/993d4b7ab2e341fe02ceef9576afcf5830cdec640be2ac5bee1820d693d4/pillow-12.1.0-cp312-cp312-win32.whl", hash = "sha256:aa0c9cc0b82b14766a99fbe6084409972266e82f459821cd26997a488a7261a7", size = 6328770, upload-time = "2026-01-02T09:11:25.661Z" }, + { url = "https://files.pythonhosted.org/packages/a7/87/90b358775a3f02765d87655237229ba64a997b87efa8ccaca7dd3e36e7a7/pillow-12.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:d70534cea9e7966169ad29a903b99fc507e932069a881d0965a1a84bb57f6c6d", size = 7033406, upload-time = "2026-01-02T09:11:27.474Z" }, + { url = "https://files.pythonhosted.org/packages/5d/cf/881b457eccacac9e5b2ddd97d5071fb6d668307c57cbf4e3b5278e06e536/pillow-12.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c", size = 2452612, upload-time = "2026-01-02T09:11:29.309Z" }, + { url = "https://files.pythonhosted.org/packages/dd/c7/2530a4aa28248623e9d7f27316b42e27c32ec410f695929696f2e0e4a778/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7b5dd7cbae20285cdb597b10eb5a2c13aa9de6cde9bb64a3c1317427b1db1ae1", size = 4062543, upload-time = "2026-01-02T09:11:31.566Z" }, + { url = "https://files.pythonhosted.org/packages/8f/1f/40b8eae823dc1519b87d53c30ed9ef085506b05281d313031755c1705f73/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:29a4cef9cb672363926f0470afc516dbf7305a14d8c54f7abbb5c199cd8f8179", size = 4138373, upload-time = "2026-01-02T09:11:33.367Z" }, + { url = "https://files.pythonhosted.org/packages/d4/77/6fa60634cf06e52139fd0e89e5bbf055e8166c691c42fb162818b7fda31d/pillow-12.1.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:681088909d7e8fa9e31b9799aaa59ba5234c58e5e4f1951b4c4d1082a2e980e0", size = 3601241, upload-time = "2026-01-02T09:11:35.011Z" }, + { url = "https://files.pythonhosted.org/packages/4f/bf/28ab865de622e14b747f0cd7877510848252d950e43002e224fb1c9ababf/pillow-12.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:983976c2ab753166dc66d36af6e8ec15bb511e4a25856e2227e5f7e00a160587", size = 5262410, upload-time = "2026-01-02T09:11:36.682Z" }, + { url = "https://files.pythonhosted.org/packages/1c/34/583420a1b55e715937a85bd48c5c0991598247a1fd2eb5423188e765ea02/pillow-12.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:db44d5c160a90df2d24a24760bbd37607d53da0b34fb546c4c232af7192298ac", size = 4657312, upload-time = "2026-01-02T09:11:38.535Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fd/f5a0896839762885b3376ff04878f86ab2b097c2f9a9cdccf4eda8ba8dc0/pillow-12.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6b7a9d1db5dad90e2991645874f708e87d9a3c370c243c2d7684d28f7e133e6b", size = 6232605, upload-time = "2026-01-02T09:11:40.602Z" }, + { url = "https://files.pythonhosted.org/packages/98/aa/938a09d127ac1e70e6ed467bd03834350b33ef646b31edb7452d5de43792/pillow-12.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6258f3260986990ba2fa8a874f8b6e808cf5abb51a94015ca3dc3c68aa4f30ea", size = 8041617, upload-time = "2026-01-02T09:11:42.721Z" }, + { url = "https://files.pythonhosted.org/packages/17/e8/538b24cb426ac0186e03f80f78bc8dc7246c667f58b540bdd57c71c9f79d/pillow-12.1.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e115c15e3bc727b1ca3e641a909f77f8ca72a64fff150f666fcc85e57701c26c", size = 6346509, upload-time = "2026-01-02T09:11:44.955Z" }, + { url = "https://files.pythonhosted.org/packages/01/9a/632e58ec89a32738cabfd9ec418f0e9898a2b4719afc581f07c04a05e3c9/pillow-12.1.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6741e6f3074a35e47c77b23a4e4f2d90db3ed905cb1c5e6e0d49bff2045632bc", size = 7038117, upload-time = "2026-01-02T09:11:46.736Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a2/d40308cf86eada842ca1f3ffa45d0ca0df7e4ab33c83f81e73f5eaed136d/pillow-12.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:935b9d1aed48fcfb3f838caac506f38e29621b44ccc4f8a64d575cb1b2a88644", size = 6460151, upload-time = "2026-01-02T09:11:48.625Z" }, + { url = "https://files.pythonhosted.org/packages/f1/88/f5b058ad6453a085c5266660a1417bdad590199da1b32fb4efcff9d33b05/pillow-12.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5fee4c04aad8932da9f8f710af2c1a15a83582cfb884152a9caa79d4efcdbf9c", size = 7164534, upload-time = "2026-01-02T09:11:50.445Z" }, + { url = "https://files.pythonhosted.org/packages/19/ce/c17334caea1db789163b5d855a5735e47995b0b5dc8745e9a3605d5f24c0/pillow-12.1.0-cp313-cp313-win32.whl", hash = "sha256:a786bf667724d84aa29b5db1c61b7bfdde380202aaca12c3461afd6b71743171", size = 6332551, upload-time = "2026-01-02T09:11:52.234Z" }, + { url = "https://files.pythonhosted.org/packages/e5/07/74a9d941fa45c90a0d9465098fe1ec85de3e2afbdc15cc4766622d516056/pillow-12.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:461f9dfdafa394c59cd6d818bdfdbab4028b83b02caadaff0ffd433faf4c9a7a", size = 7040087, upload-time = "2026-01-02T09:11:54.822Z" }, + { url = "https://files.pythonhosted.org/packages/88/09/c99950c075a0e9053d8e880595926302575bc742b1b47fe1bbcc8d388d50/pillow-12.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:9212d6b86917a2300669511ed094a9406888362e085f2431a7da985a6b124f45", size = 2452470, upload-time = "2026-01-02T09:11:56.522Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ba/970b7d85ba01f348dee4d65412476321d40ee04dcb51cd3735b9dc94eb58/pillow-12.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:00162e9ca6d22b7c3ee8e61faa3c3253cd19b6a37f126cad04f2f88b306f557d", size = 5264816, upload-time = "2026-01-02T09:11:58.227Z" }, + { url = "https://files.pythonhosted.org/packages/10/60/650f2fb55fdba7a510d836202aa52f0baac633e50ab1cf18415d332188fb/pillow-12.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7d6daa89a00b58c37cb1747ec9fb7ac3bc5ffd5949f5888657dfddde6d1312e0", size = 4660472, upload-time = "2026-01-02T09:12:00.798Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/5273a99478956a099d533c4f46cbaa19fd69d606624f4334b85e50987a08/pillow-12.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2479c7f02f9d505682dc47df8c0ea1fc5e264c4d1629a5d63fe3e2334b89554", size = 6268974, upload-time = "2026-01-02T09:12:02.572Z" }, + { url = "https://files.pythonhosted.org/packages/b4/26/0bf714bc2e73d5267887d47931d53c4ceeceea6978148ed2ab2a4e6463c4/pillow-12.1.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f188d580bd870cda1e15183790d1cc2fa78f666e76077d103edf048eed9c356e", size = 8073070, upload-time = "2026-01-02T09:12:04.75Z" }, + { url = "https://files.pythonhosted.org/packages/43/cf/1ea826200de111a9d65724c54f927f3111dc5ae297f294b370a670c17786/pillow-12.1.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fde7ec5538ab5095cc02df38ee99b0443ff0e1c847a045554cf5f9af1f4aa82", size = 6380176, upload-time = "2026-01-02T09:12:06.626Z" }, + { url = "https://files.pythonhosted.org/packages/03/e0/7938dd2b2013373fd85d96e0f38d62b7a5a262af21ac274250c7ca7847c9/pillow-12.1.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ed07dca4a8464bada6139ab38f5382f83e5f111698caf3191cb8dbf27d908b4", size = 7067061, upload-time = "2026-01-02T09:12:08.624Z" }, + { url = "https://files.pythonhosted.org/packages/86/ad/a2aa97d37272a929a98437a8c0ac37b3cf012f4f8721e1bd5154699b2518/pillow-12.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f45bd71d1fa5e5749587613037b172e0b3b23159d1c00ef2fc920da6f470e6f0", size = 6491824, upload-time = "2026-01-02T09:12:10.488Z" }, + { url = "https://files.pythonhosted.org/packages/a4/44/80e46611b288d51b115826f136fb3465653c28f491068a72d3da49b54cd4/pillow-12.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:277518bf4fe74aa91489e1b20577473b19ee70fb97c374aa50830b279f25841b", size = 7190911, upload-time = "2026-01-02T09:12:12.772Z" }, + { url = "https://files.pythonhosted.org/packages/86/77/eacc62356b4cf81abe99ff9dbc7402750044aed02cfd6a503f7c6fc11f3e/pillow-12.1.0-cp313-cp313t-win32.whl", hash = "sha256:7315f9137087c4e0ee73a761b163fc9aa3b19f5f606a7fc08d83fd3e4379af65", size = 6336445, upload-time = "2026-01-02T09:12:14.775Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3c/57d81d0b74d218706dafccb87a87ea44262c43eef98eb3b164fd000e0491/pillow-12.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:0ddedfaa8b5f0b4ffbc2fa87b556dc59f6bb4ecb14a53b33f9189713ae8053c0", size = 7045354, upload-time = "2026-01-02T09:12:16.599Z" }, + { url = "https://files.pythonhosted.org/packages/ac/82/8b9b97bba2e3576a340f93b044a3a3a09841170ab4c1eb0d5c93469fd32f/pillow-12.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:80941e6d573197a0c28f394753de529bb436b1ca990ed6e765cf42426abc39f8", size = 2454547, upload-time = "2026-01-02T09:12:18.704Z" }, + { url = "https://files.pythonhosted.org/packages/8c/87/bdf971d8bbcf80a348cc3bacfcb239f5882100fe80534b0ce67a784181d8/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:5cb7bc1966d031aec37ddb9dcf15c2da5b2e9f7cc3ca7c54473a20a927e1eb91", size = 4062533, upload-time = "2026-01-02T09:12:20.791Z" }, + { url = "https://files.pythonhosted.org/packages/ff/4f/5eb37a681c68d605eb7034c004875c81f86ec9ef51f5be4a63eadd58859a/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:97e9993d5ed946aba26baf9c1e8cf18adbab584b99f452ee72f7ee8acb882796", size = 4138546, upload-time = "2026-01-02T09:12:23.664Z" }, + { url = "https://files.pythonhosted.org/packages/11/6d/19a95acb2edbace40dcd582d077b991646b7083c41b98da4ed7555b59733/pillow-12.1.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:414b9a78e14ffeb98128863314e62c3f24b8a86081066625700b7985b3f529bd", size = 3601163, upload-time = "2026-01-02T09:12:26.338Z" }, + { url = "https://files.pythonhosted.org/packages/fc/36/2b8138e51cb42e4cc39c3297713455548be855a50558c3ac2beebdc251dd/pillow-12.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e6bdb408f7c9dd2a5ff2b14a3b0bb6d4deb29fb9961e6eb3ae2031ae9a5cec13", size = 5266086, upload-time = "2026-01-02T09:12:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/649056e4d22e1caa90816bf99cef0884aed607ed38075bd75f091a607a38/pillow-12.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3413c2ae377550f5487991d444428f1a8ae92784aac79caa8b1e3b89b175f77e", size = 4657344, upload-time = "2026-01-02T09:12:31.117Z" }, + { url = "https://files.pythonhosted.org/packages/6c/6b/c5742cea0f1ade0cd61485dc3d81f05261fc2276f537fbdc00802de56779/pillow-12.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e5dcbe95016e88437ecf33544ba5db21ef1b8dd6e1b434a2cb2a3d605299e643", size = 6232114, upload-time = "2026-01-02T09:12:32.936Z" }, + { url = "https://files.pythonhosted.org/packages/bf/8f/9f521268ce22d63991601aafd3d48d5ff7280a246a1ef62d626d67b44064/pillow-12.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d0a7735df32ccbcc98b98a1ac785cc4b19b580be1bdf0aeb5c03223220ea09d5", size = 8042708, upload-time = "2026-01-02T09:12:34.78Z" }, + { url = "https://files.pythonhosted.org/packages/1a/eb/257f38542893f021502a1bbe0c2e883c90b5cff26cc33b1584a841a06d30/pillow-12.1.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c27407a2d1b96774cbc4a7594129cc027339fd800cd081e44497722ea1179de", size = 6347762, upload-time = "2026-01-02T09:12:36.748Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5a/8ba375025701c09b309e8d5163c5a4ce0102fa86bbf8800eb0d7ac87bc51/pillow-12.1.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15c794d74303828eaa957ff8070846d0efe8c630901a1c753fdc63850e19ecd9", size = 7039265, upload-time = "2026-01-02T09:12:39.082Z" }, + { url = "https://files.pythonhosted.org/packages/cf/dc/cf5e4cdb3db533f539e88a7bbf9f190c64ab8a08a9bc7a4ccf55067872e4/pillow-12.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c990547452ee2800d8506c4150280757f88532f3de2a58e3022e9b179107862a", size = 6462341, upload-time = "2026-01-02T09:12:40.946Z" }, + { url = "https://files.pythonhosted.org/packages/d0/47/0291a25ac9550677e22eda48510cfc4fa4b2ef0396448b7fbdc0a6946309/pillow-12.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b63e13dd27da389ed9475b3d28510f0f954bca0041e8e551b2a4eb1eab56a39a", size = 7165395, upload-time = "2026-01-02T09:12:42.706Z" }, + { url = "https://files.pythonhosted.org/packages/4f/4c/e005a59393ec4d9416be06e6b45820403bb946a778e39ecec62f5b2b991e/pillow-12.1.0-cp314-cp314-win32.whl", hash = "sha256:1a949604f73eb07a8adab38c4fe50791f9919344398bdc8ac6b307f755fc7030", size = 6431413, upload-time = "2026-01-02T09:12:44.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/af/f23697f587ac5f9095d67e31b81c95c0249cd461a9798a061ed6709b09b5/pillow-12.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f9f6a650743f0ddee5593ac9e954ba1bdbc5e150bc066586d4f26127853ab94", size = 7176779, upload-time = "2026-01-02T09:12:46.727Z" }, + { url = "https://files.pythonhosted.org/packages/b3/36/6a51abf8599232f3e9afbd16d52829376a68909fe14efe29084445db4b73/pillow-12.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:808b99604f7873c800c4840f55ff389936ef1948e4e87645eaf3fccbc8477ac4", size = 2543105, upload-time = "2026-01-02T09:12:49.243Z" }, + { url = "https://files.pythonhosted.org/packages/82/54/2e1dd20c8749ff225080d6ba465a0cab4387f5db0d1c5fb1439e2d99923f/pillow-12.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc11908616c8a283cf7d664f77411a5ed2a02009b0097ff8abbba5e79128ccf2", size = 5268571, upload-time = "2026-01-02T09:12:51.11Z" }, + { url = "https://files.pythonhosted.org/packages/57/61/571163a5ef86ec0cf30d265ac2a70ae6fc9e28413d1dc94fa37fae6bda89/pillow-12.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:896866d2d436563fa2a43a9d72f417874f16b5545955c54a64941e87c1376c61", size = 4660426, upload-time = "2026-01-02T09:12:52.865Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e1/53ee5163f794aef1bf84243f755ee6897a92c708505350dd1923f4afec48/pillow-12.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8e178e3e99d3c0ea8fc64b88447f7cac8ccf058af422a6cedc690d0eadd98c51", size = 6269908, upload-time = "2026-01-02T09:12:54.884Z" }, + { url = "https://files.pythonhosted.org/packages/bc/0b/b4b4106ff0ee1afa1dc599fde6ab230417f800279745124f6c50bcffed8e/pillow-12.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:079af2fb0c599c2ec144ba2c02766d1b55498e373b3ac64687e43849fbbef5bc", size = 8074733, upload-time = "2026-01-02T09:12:56.802Z" }, + { url = "https://files.pythonhosted.org/packages/19/9f/80b411cbac4a732439e629a26ad3ef11907a8c7fc5377b7602f04f6fe4e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdec5e43377761c5dbca620efb69a77f6855c5a379e32ac5b158f54c84212b14", size = 6381431, upload-time = "2026-01-02T09:12:58.823Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b7/d65c45db463b66ecb6abc17c6ba6917a911202a07662247e1355ce1789e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:565c986f4b45c020f5421a4cea13ef294dde9509a8577f29b2fc5edc7587fff8", size = 7068529, upload-time = "2026-01-02T09:13:00.885Z" }, + { url = "https://files.pythonhosted.org/packages/50/96/dfd4cd726b4a45ae6e3c669fc9e49deb2241312605d33aba50499e9d9bd1/pillow-12.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43aca0a55ce1eefc0aefa6253661cb54571857b1a7b2964bd8a1e3ef4b729924", size = 6492981, upload-time = "2026-01-02T09:13:03.314Z" }, + { url = "https://files.pythonhosted.org/packages/4d/1c/b5dc52cf713ae46033359c5ca920444f18a6359ce1020dd3e9c553ea5bc6/pillow-12.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0deedf2ea233722476b3a81e8cdfbad786f7adbed5d848469fa59fe52396e4ef", size = 7191878, upload-time = "2026-01-02T09:13:05.276Z" }, + { url = "https://files.pythonhosted.org/packages/53/26/c4188248bd5edaf543864fe4834aebe9c9cb4968b6f573ce014cc42d0720/pillow-12.1.0-cp314-cp314t-win32.whl", hash = "sha256:b17fbdbe01c196e7e159aacb889e091f28e61020a8abeac07b68079b6e626988", size = 6438703, upload-time = "2026-01-02T09:13:07.491Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0e/69ed296de8ea05cb03ee139cee600f424ca166e632567b2d66727f08c7ed/pillow-12.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27b9baecb428899db6c0de572d6d305cfaf38ca1596b5c0542a5182e3e74e8c6", size = 7182927, upload-time = "2026-01-02T09:13:09.841Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f5/68334c015eed9b5cff77814258717dec591ded209ab5b6fb70e2ae873d1d/pillow-12.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831", size = 2545104, upload-time = "2026-01-02T09:13:12.068Z" }, +] + [[package]] name = "platformdirs" version = "4.5.0" @@ -708,6 +1082,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e6/3d/8888e7ca0c6b093b52aa5c6693b0022e66d5958adcc685ed7a6a8ae615e8/pygments_styles-0.2.0-py3-none-any.whl", hash = "sha256:40fb7f1d34ce2b2792aecabc8d3877ca364eb04bb3b7f7747cfc9a7f0569bae9", size = 34200, upload-time = "2025-09-26T08:39:02.262Z" }, ] +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + [[package]] name = "pytest" version = "8.4.2" @@ -750,6 +1133,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, ] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + [[package]] name = "requests" version = "2.32.5" @@ -826,6 +1221,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/d7/e4831122c1cdaa66cf432d50cf24b38dd161aa69187eebfa41295c3a84ec/shibuya-2025.9.25-py3-none-any.whl", hash = "sha256:93e439181167936dd1950874880c23920d551f90ff475888ce278af1d2efa4eb", size = 97695, upload-time = "2025-09-25T02:54:17.351Z" }, ] +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + [[package]] name = "sniffio" version = "1.3.1" From 70bbbd76ce2b83f6da125ac31a9bac011a16c2a5 Mon Sep 17 00:00:00 2001 From: kolo Date: Sat, 24 Jan 2026 02:18:39 +0300 Subject: [PATCH 14/31] benchs --- metrics/services/diagram_generator.py | 74 +++++++++++++++++---------- 1 file changed, 47 insertions(+), 27 deletions(-) diff --git a/metrics/services/diagram_generator.py b/metrics/services/diagram_generator.py index f3dc82b..9b9d1ee 100644 --- a/metrics/services/diagram_generator.py +++ b/metrics/services/diagram_generator.py @@ -4,14 +4,14 @@ from pathlib import Path import matplotlib import matplotlib.pyplot as plt -import numpy as np from ..benchmarks.core.models import BenchmarkGroupResult class DiagramGenerator: def __init__(self, output_dir: Path | str) -> None: - self.output_dir = output_dir + self.output_dir: Path = Path(output_dir) if isinstance(output_dir, str) else output_dir + matplotlib.use('Agg') plt.style.use('seaborn-v0_8-whitegrid') @@ -19,33 +19,46 @@ class DiagramGenerator: results = benchmark_group.benchmark_results sorted_results = sorted(results, key=lambda br: br.avg_time) - descriptions = [br.description for br in sorted_results] - avg_times = [br.avg_time for br in sorted_results] - median_times = [br.median_time for br in sorted_results] - std_devs = [br.std_dev for br in sorted_results] + descriptions: list[str] = [br.description for br in sorted_results] + avg_times: list[float] = [br.avg_time for br in sorted_results] + median_times: list[float] = [br.median_time for br in sorted_results] + std_devs: list[float] = [br.std_dev for br in sorted_results] - max_value = max(max(avg_times), max(median_times), max(std_devs)) - y_limit = max_value / 0.85 + max_value = max( + max(avg_times) if avg_times else 0, + max(median_times) if median_times else 0, + max(std_devs) if std_devs else 0 + ) + y_limit = max_value / 0.85 if max_value > 0 else 1.0 - x = np.arange(len(descriptions)) - width = 0.25 + items_count = len(descriptions) + x_positions: list[int] = list(range(items_count)) + + bar_width = 0.25 + + x_std_dev = [x - bar_width for x in x_positions] + x_avg = [x for x in x_positions] + x_median = [x + bar_width for x in x_positions] fig, ax = plt.subplots(figsize=(16, 8)) fig.patch.set_facecolor('white') - bars1 = ax.bar(x - width, std_devs, width, label='Std Deviation', - color='#2ecc71', alpha=0.9, edgecolor='#27ae60', linewidth=1.5) - bars2 = ax.bar(x, avg_times, width, label='Average Time', - color='#3498db', alpha=0.9, edgecolor='#2980b9', linewidth=1.5) - bars3 = ax.bar(x + width, median_times, width, label='Median Time', - color='#e74c3c', alpha=0.9, edgecolor='#c0392b', linewidth=1.5) + 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 bars in [bars1, bars2, bars3]: - for bar in bars: + 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.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) @@ -57,14 +70,18 @@ class DiagramGenerator: ax.text(0.5, 1.03, metadata_text, transform=ax.transAxes, fontsize=12, ha='center', color='#7f8c8d', style='italic') - ax.set_xticks(x) + ax.set_xticks(x_positions) ax.set_xticklabels([]) - for i, (pos, desc) in enumerate(zip(x, descriptions)): - bar_x = pos - width - width / 2 - ax.text(bar_x, y_limit * 0.02, desc, - rotation=90, va='bottom', ha='right', fontsize=10, - color='#2c3e50') + 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) @@ -84,6 +101,9 @@ class DiagramGenerator: filename = f"{benchmark_group.type_}_comparison.png" output_path = self.output_dir / filename + + self.output_dir.mkdir(parents=True, exist_ok=True) + plt.savefig(output_path, dpi=200, bbox_inches='tight', facecolor='white') plt.close(fig) From 46c1ec02fd703bc00a8597bd109720ec6ba178bf Mon Sep 17 00:00:00 2001 From: kolo Date: Sat, 24 Jan 2026 02:24:31 +0300 Subject: [PATCH 15/31] comm --- src/argenta/app/autocompleter/entity.py | 2 +- src/argenta/command/flag/models.py | 2 +- tests/unit_tests/test_app.py | 16 ---------------- 3 files changed, 2 insertions(+), 18 deletions(-) diff --git a/src/argenta/app/autocompleter/entity.py b/src/argenta/app/autocompleter/entity.py index fa298c4..6c92982 100644 --- a/src/argenta/app/autocompleter/entity.py +++ b/src/argenta/app/autocompleter/entity.py @@ -100,7 +100,7 @@ class AutoCompleter: if buff.complete_state: buff.complete_next() return - comps_gen = buff.completer.get_completions(buff.document, CompleteEvent()) + comps_gen = iter(buff.completer.get_completions(buff.document, CompleteEvent())) try: first = next(comps_gen) except StopIteration: diff --git a/src/argenta/command/flag/models.py b/src/argenta/command/flag/models.py index 9355362..77500a3 100644 --- a/src/argenta/command/flag/models.py +++ b/src/argenta/command/flag/models.py @@ -35,7 +35,7 @@ class Flag: """ self.name: str = name self.prefix: PREFIX_TYPE = prefix - self.possible_values: list[str] | Pattern[str] | PossibleValues = possible_values + self.possible_values: Container[str] | Pattern[str] | PossibleValues = possible_values def validate_input_flag_value(self, input_flag_value: str) -> bool: """ diff --git a/tests/unit_tests/test_app.py b/tests/unit_tests/test_app.py index 17dd8a0..1e08b65 100644 --- a/tests/unit_tests/test_app.py +++ b/tests/unit_tests/test_app.py @@ -354,22 +354,6 @@ def test_set_exit_command_handler_stores_handler() -> None: assert app._exit_command_handler is handler -# ============================================================================ -# Tests for default view setup -# ============================================================================ - - -def test_setup_default_view_formats_prompt() -> None: - app = App(prompt='>>') - assert app._prompt == '>>' - - -def test_setup_default_view_sets_default_unknown_command_handler() -> None: - app = App() - app._setup_default_view() - assert app._unknown_command_handler(InputCommand('nonexists')) is None - - # ============================================================================ # Tests for command processing # ============================================================================ From dfe482c545a725742ea4043fdbe2f9ba72930241 Mon Sep 17 00:00:00 2001 From: kolo Date: Sun, 25 Jan 2026 16:01:34 +0300 Subject: [PATCH 16/31] release generate --- metrics/handlers.py | 25 ++++++- .../finds_appropriate_handler.json | 42 +++++++++++ .../finds_appropriate_handler_comparison.png | Bin 0 -> 158847 bytes .../flag_validation/flag_validation.json | 70 ++++++++++++++++++ .../flag_validation_comparison.png | Bin 0 -> 190392 bytes .../input_command_parse.json | 56 ++++++++++++++ .../input_command_parse_comparison.png | Bin 0 -> 167402 bytes .../most_similar_command.json | 42 +++++++++++ .../most_similar_command_comparison.png | Bin 0 -> 170609 bytes .../pre_cycle_setup/pre_cycle_setup.json | 42 +++++++++++ .../pre_cycle_setup_comparison.png | Bin 0 -> 139223 bytes .../validate_routers_for_collisions.json | 42 +++++++++++ ...date_routers_for_collisions_comparison.png | Bin 0 -> 186650 bytes metrics/services/__init__.py | 3 +- metrics/services/release_generator.py | 49 ++++++++++++ 15 files changed, 369 insertions(+), 2 deletions(-) create mode 100644 metrics/reports/releases/1.1.2/finds_appropriate_handler/finds_appropriate_handler.json create mode 100644 metrics/reports/releases/1.1.2/finds_appropriate_handler/finds_appropriate_handler_comparison.png create mode 100644 metrics/reports/releases/1.1.2/flag_validation/flag_validation.json create mode 100644 metrics/reports/releases/1.1.2/flag_validation/flag_validation_comparison.png create mode 100644 metrics/reports/releases/1.1.2/input_command_parse/input_command_parse.json create mode 100644 metrics/reports/releases/1.1.2/input_command_parse/input_command_parse_comparison.png create mode 100644 metrics/reports/releases/1.1.2/most_similar_command/most_similar_command.json create mode 100644 metrics/reports/releases/1.1.2/most_similar_command/most_similar_command_comparison.png create mode 100644 metrics/reports/releases/1.1.2/pre_cycle_setup/pre_cycle_setup.json create mode 100644 metrics/reports/releases/1.1.2/pre_cycle_setup/pre_cycle_setup_comparison.png create mode 100644 metrics/reports/releases/1.1.2/validate_routers_for_collisions/validate_routers_for_collisions.json create mode 100644 metrics/reports/releases/1.1.2/validate_routers_for_collisions/validate_routers_for_collisions_comparison.png create mode 100644 metrics/services/release_generator.py diff --git a/metrics/handlers.py b/metrics/handlers.py index b135652..996257c 100644 --- a/metrics/handlers.py +++ b/metrics/handlers.py @@ -1,5 +1,6 @@ import re from datetime import datetime +from importlib.metadata import version from pathlib import Path from rich.console import Console @@ -14,6 +15,7 @@ from .benchmarks.entity import benchmarks as registered_benchmarks from .services.report_generator import ReportGenerator from .services.system_info_reader import get_system_info from .services.diagram_generator import DiagramGenerator +from .services.release_generator import ReleaseGenerator console = Console() router = Router(title="Metrics commands:", disable_redirect_stdout=True) @@ -106,7 +108,28 @@ def run_type_handler(response: Response) -> None: @router.command(Command("release-generate", description="Generate release report")) def release_generate_handler(_: Response) -> None: - console.print("[yellow]Release report generation not implemented yet[/yellow]") + lib_version = version("argenta") + + console.print(f"[cyan]Generating release report for version:[/cyan] [bold]{lib_version}[/bold]") + console.print("[dim]Running benchmarks (1000 iterations, GC disabled)...[/dim]\n") + + type_grouped_benchmarks: list[BenchmarkGroupResult] = registered_benchmarks.run_benchmarks_grouped_by_type( + iterations=1000, + is_gc_disabled=True + ) + + release_generator = ReleaseGenerator(lib_version) + output_dir = release_generator.generate_release(type_grouped_benchmarks) + + console.print(f"[green]✓[/green] Benchmarks completed. Generating release report...\n") + + for benchmark_group in type_grouped_benchmarks: + console.print(f"[cyan]Generated for:[/cyan] [bold]{benchmark_group.type_}[/bold]") + console.print(f" [green]✓[/green] {benchmark_group.type_}_comparison.png") + console.print(f" [green]✓[/green] {benchmark_group.type_}.json\n") + + console.print(f"[bold green]✓ Release report generated successfully[/bold green]") + console.print(f"[cyan]Output directory:[/cyan] [bold]{output_dir}[/bold]") @router.command( diff --git a/metrics/reports/releases/1.1.2/finds_appropriate_handler/finds_appropriate_handler.json b/metrics/reports/releases/1.1.2/finds_appropriate_handler/finds_appropriate_handler.json new file mode 100644 index 0000000..8a7ab02 --- /dev/null +++ b/metrics/reports/releases/1.1.2/finds_appropriate_handler/finds_appropriate_handler.json @@ -0,0 +1,42 @@ +{ + "type": "finds_appropriate_handler", + "iterations": 1000, + "gc_disabled": true, + "benchmarks": [ + { + "name": "benchmark_simple_command", + "description": "Simple command (no flags)", + "avg_time": 0.0354, + "median_time": 0.0345, + "std_dev": 0.0056 + }, + { + "name": "benchmark_command_with_flags", + "description": "Command with flags (3 flags)", + "avg_time": 0.0544, + "median_time": 0.0536, + "std_dev": 0.009 + }, + { + "name": "benchmark_many_commands", + "description": "Many commands (50 commands)", + "avg_time": 1.009, + "median_time": 1.0022, + "std_dev": 0.039 + }, + { + "name": "benchmark_command_with_many_flags", + "description": "Command with many flags (20 flags)", + "avg_time": 0.131, + "median_time": 0.1298, + "std_dev": 0.0054 + }, + { + "name": "benchmark_extreme_router", + "description": "Extreme (100 commands, 10 flags each)", + "avg_time": 3.1818, + "median_time": 3.1619, + "std_dev": 0.1673 + } + ] +} \ No newline at end of file diff --git a/metrics/reports/releases/1.1.2/finds_appropriate_handler/finds_appropriate_handler_comparison.png b/metrics/reports/releases/1.1.2/finds_appropriate_handler/finds_appropriate_handler_comparison.png new file mode 100644 index 0000000000000000000000000000000000000000..404d202d6a32137a2b04920386825c80849ed8c2 GIT binary patch literal 158847 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 literal 0 HcmV?d00001 diff --git a/metrics/reports/releases/1.1.2/flag_validation/flag_validation.json b/metrics/reports/releases/1.1.2/flag_validation/flag_validation.json new file mode 100644 index 0000000..b7174ae --- /dev/null +++ b/metrics/reports/releases/1.1.2/flag_validation/flag_validation.json @@ -0,0 +1,70 @@ +{ + "type": "flag_validation", + "iterations": 1000, + "gc_disabled": true, + "benchmarks": [ + { + "name": "benchmark_validate_all_single_flag", + "description": "Single flag with PossibleValues.ALL", + "avg_time": 0.0009, + "median_time": 0.0009, + "std_dev": 0.0003 + }, + { + "name": "benchmark_validate_neither_single_flag", + "description": "Single flag with PossibleValues.NEITHER", + "avg_time": 0.0008, + "median_time": 0.0008, + "std_dev": 0.0002 + }, + { + "name": "benchmark_validate_list_small", + "description": "List validation (5 possible values)", + "avg_time": 0.001, + "median_time": 0.001, + "std_dev": 0.0003 + }, + { + "name": "benchmark_validate_list_large", + "description": "List validation (50 possible values)", + "avg_time": 0.0079, + "median_time": 0.0078, + "std_dev": 0.0007 + }, + { + "name": "benchmark_validate_regex_simple", + "description": "Regex validation (simple pattern)", + "avg_time": 0.0019, + "median_time": 0.0016, + "std_dev": 0.0073 + }, + { + "name": "benchmark_validate_regex_complex", + "description": "Regex validation (complex pattern)", + "avg_time": 0.0018, + "median_time": 0.0017, + "std_dev": 0.0051 + }, + { + "name": "benchmark_validate_multiple_flags_10", + "description": "Multiple flags validation (10 flags)", + "avg_time": 0.0147, + "median_time": 0.0146, + "std_dev": 0.0013 + }, + { + "name": "benchmark_validate_multiple_flags_50", + "description": "Multiple flags validation (50 flags)", + "avg_time": 0.0669, + "median_time": 0.0661, + "std_dev": 0.0025 + }, + { + "name": "benchmark_validate_extreme_100_flags", + "description": "Extreme (100 flags with regex validation)", + "avg_time": 0.1626, + "median_time": 0.1609, + "std_dev": 0.007 + } + ] +} \ No newline at end of file diff --git a/metrics/reports/releases/1.1.2/flag_validation/flag_validation_comparison.png b/metrics/reports/releases/1.1.2/flag_validation/flag_validation_comparison.png new file mode 100644 index 0000000000000000000000000000000000000000..6008aee7a84f214254767c473e5035c4767d57d7 GIT binary patch literal 190392 zcmd?Rhg(zG7d7mRKXn8ZM`?n(~M=GEeF{{#L3d8?Ut>$^L6!)!e5k7?U@zjSf;c5!<4hmXCd zmy^5O9VwYxQVJ4(Jool~=>?LOcKz=aQtqCP(oN>jKHyVMztk}GI(AI3`S9l#w*e^l z*e}P9Y5sN3z&C|}J5hd_JOSAf(V4bHxCge1oPUGGE)~o)GmDjMtV`~d>nn2s%)v3eH%$56Xj0B*}xzE z@A1WDE#BSmzo)>(wO{%F@2B=Rih&{h_jv5^|LT8}yBqEwEyK9ww10ogHm*CkPP@vZ z#X9)+0P4EoA}MBu+LcJ!KS;-4qK`{Xc|WR)!QxHMi=?r-#><`b2Wh8>kh{#Ca3L46 zkJNsTylqeQ2eoDB`jE_K4Mn#^r8kZKAmxCm%atl0IR0&qjt;gMBgVCqN!MQ%Vhq8; z`;_JvZ0CY8yFFRoxe3bX0Io-XXrwzGD3eneCIcvzn&gH`iSwO zRR8;{-&)_D<)3frNSnwfL-zLGM++#=OfsLCSHOL&!s8_(gboN)k6FjYOSU49CI?%N z0_CZQ@IMhl7}HT$8+blme6Nc}1@9~v6|r8ctH-8P>9W9s1?t!$Gl&xMEWcc9uv?>& z4ray6W7v|Ty*+{-w)^SlAWokKHs0Z*u7bNax&8N38^)f@m4q9u2O;5=&vB~((vis~ zhf_+}+-yQQH5cg52O$s44tBA@AMCr02`jTl?=HSrfXO&@ba|{UU4>8fqv|={%F%VF zu|iGLc2%;D%51Lh%7-Ym<*d!$sE{3Ri@lj)xd3_a*7%+4b>bsM=JGRM)3fJ#-Ik(q z9<;S~4a0VC9ev#RiQj&X@EiB@;adBHrC^Lbe&cE+$jhit zyl70x)3ULem)p_2jG@1yNxCYtoxNs4M`I`s_FLyDv5jg^3d#S`XSqAv5@r{~jf*0o z>sa-ScW2Wz=T*{G>naB`%$HY2Gn_~A^m8Pp9^u5S;P!ew*3}9|g(h5*=5mN%82)Wv z8t8GZ{K@ODY~BLzk=t&6qo%fur*FkP6OYBxlO=|q1h1QrWl; zRl4&8kSFf!Q`SnftCZZC&56nz6^DDzqyqR5xR>_b=Z_)42i=^kauu*tf_AnKe!2H{ zTp)96VB}KSv!Rhorql$(0{lk@?c@i^Mv@s`9X>L}!T4Nj%#6>a?!p?=!GAJgaq`2qDOTflr0-UVk);^ZkKww#BOX(R;);W=4M>3xm}PSe$JEQ@wFX0F<_sc8@rF*C zmHosW9q14@PL~sXp-a zbCBnl=tPIHL(nOBUj{gYSfK6A$IF$}TocvOwznEUwSb)uws<5joD^``2Lnb7$R zAq6EcwS@fzZ1@LKLR26Nz5i3~qs_RzHWxyJz=SfF=PLwce=u$2PW{?}{2+9IfwV?vbvrXs zE1a=y^TKKUR_a3je;(`wH=dVFmJhdV0&?114uOy>H*NlT4#(;V-7mjaKcB>`5?qzf4G%NFQJxaF)OfN0M z#|N0ZVDK)@9m_j*Gq(12;J$N0TbN-%rEeV!yk!!&NH;zlOQ|{%E(hL@BX%S^kG1w~ zPViVomVtNM%tB?w_-O}P|CA*a^LW$^AzCx+#TjRVbD=?$AyzM__vbA8{wH^I@l@Wk z8z>qPuxaafsAcd}!!{v&H1w8H5&RC=YZYO;M8L`iFoqrbKA;1Mj@Ok8UBq+JUfOl} zT@=wpIH7w{6uMS|B;rX%N4ynlTb+w>fM8XF(|ng`H$(AEa+1W&vOAxZ+~I+n=8zAI z;zn*1m^Iwq^WaAq#AVP&L^nWEs&0;>#AAL8seaKal`j}W85{o`;p_G&aBV%o{%dGa z6bdH|U*0Lq=!-YQz*PfhgW-G^Z)oGvw+0b#_#$^`!qG&0P8bRi50T}uV0jUb)b^!KbT~uawuDE zC{*Pbc(ZziSTDkx`4q#s-A$|F#NjNeuUGHn%>>|&`Q}WZ<9dM-5lE&Rx7I?}yGt86 zJv9aV{C$Xgs;4M;0ud663L%?=<2iRVa6S|P3v$xYt{oTnw?MjGg^rn&dixFfMexae z9M(}5i&XGsJ;zr6r*gEejkVu8fiUVJaWF=Vc7n%b#9%1ywt#@|PibQGEI zbMTUR?LB4EbYk^Jqq-J%E8>#z{~ z?#Igl8P@sWz{A|=(dX3RxcnCm1bqgSDpOc!5lvwJhR%l^WKhY%fp=q$?NF92{rSKb zxKB|1Z6~v7-(`mBN`B%58WUy7zcE?a7hI`}nW5c!xc2RK!(Tk{K&@2IiQEtitn3gx zn_&Sz2p2Cn+eR47wMp?9lJQu1O?SZ3!r}@_NeA(DLf`xHo{ga_6QH#<^LPy6Mg(=d zaN7VZk5IN_DXgxvqz7PiCMt(ZYGx_)GB>^~<^A2VpOxvGo7O*Q&gUXbWy^Q4U;pdA zHGKPRy4qlefZxlMz#bcqZAVO8h(Z|j-Oa^X^G(tH)D!?{sx&Hzk0!X;gKGtq%U;q7E(Dkw2;7a_bpt<@x zIv;~AV9p1?PMl8Pm?3|&y}lf@I4txL5%`0UekGFBuPoX}TS=IYps|O;VPn&NT)*=3 zG*Zh6aR;+RY?~0vBWRFl`EoPQV=N#t!u3jsP(FJOVHl%(u%@2iU}?oB;Qy`I1*kg4 z!I9=fzv>kj-lX^@s%E=yueFVcOD~aip1cNU%)7V0rk_QXq7eWL+iNw(Sl=$b3*9f2 zoV;1pc2a9S7(D9(=0YF5<<@rkPQ!2f-oZ& z8ASNXhv@Z3w$-~{LPh7tZhAK<=GfWdp6GyxpVAB@dwQv^Yc7u$y~*-=Xg1YbO>))h^(g$m{PnlfYKiHW9N~MNWBMwm4sK-`Ua2!}D7+sw~v*5hFNnoyun`QlA7J(YO~u=9e4UYRE_S zrpjX$c8*gw&DUkl3O@D<{ouNo*uwD<$bIh(uh)X6>76mao=1QgE?u8mx4s_F&=2)7 zSB^LM2-&EZkYSFBZV&Al&$pGJ?F1BtWTqwzVNz!%9MNWIkyLqe{_&d*P3hNVW`UO- z;&CrLMCx