2 Commits

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-08 11:18:39 +00:00
74 changed files with 793 additions and 3013 deletions
+1 -6
View File
@@ -1,10 +1,6 @@
#### joe made this: http://goel.io/joe
.devin
metrics/reports/diagrams
*.dist
*build
*.exe
#### python ####
# Byte-compiled / optimized / DLL files
@@ -325,5 +321,4 @@ http-client.private.env.json
.idea/.cache/.Apifox_Helper
.idea/ApifoxUploaderProjectSetting.xml
.zed
test.py
.zed
-15
View File
@@ -1,15 +0,0 @@
# Argenta
## Agent skills
### Issue tracker
Issues live as GitHub issues in `koloideal/Argenta` (uses the `gh` CLI). See `docs/agents/issue-tracker.md`.
### Triage labels
Five canonical labels used as-is: `needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, `wontfix`. See `docs/agents/triage-labels.md`.
### Domain docs
Single-context — one `CONTEXT.md` + `docs/adr/` at the repo root. See `docs/agents/domain.md`.
@@ -1,35 +0,0 @@
<!--
A new scriv changelog fragment.
Uncomment the section that is right (remove the HTML comment wrapper).
For top level release notes, leave all the headers commented out.
-->
### Added
- A cli module that implements the ability to launch applications on Argenta, run application benchmarks on Argenta, create a boilerplate for new projects, and much more.
- A new `info` command has been added to the Argenta CLI, providing a quick overview of the installed package and runtime environment.
### Changed
- Refactoring the initialization order of some modules; heavy imports are now imported only when necessary, which resulted in a boost to importtime.
<!--
### Deprecated
- A bullet item for the Deprecated category.
-->
<!--
### Removed
- A bullet item for the Removed category.
-->
<!--
### Fixed
- A bullet item for the Fixed category.
-->
-51
View File
@@ -1,51 +0,0 @@
# Domain Docs
How the engineering skills should consume this repo's domain documentation when exploring the codebase.
## Before exploring, read these
- **`CONTEXT.md`** at the repo root, or
- **`CONTEXT-MAP.md`** at the repo root if it exists — it points at one `CONTEXT.md` per context. Read each one relevant to the topic.
- **`docs/adr/`** — read ADRs that touch the area you're about to work in. In multi-context repos, also check `src/<context>/docs/adr/` for context-scoped decisions.
If any of these files don't exist, **proceed silently**. Don't flag their absence; don't suggest creating them upfront. The `/domain-modeling` skill (reached via `/grill-with-docs` and `/improve-codebase-architecture`) creates them lazily when terms or decisions actually get resolved.
## File structure
Single-context repo (most repos):
```
/
├── CONTEXT.md
├── docs/adr/
│ ├── 0001-event-sourced-orders.md
│ └── 0002-postgres-for-write-model.md
└── src/
```
Multi-context repo (presence of `CONTEXT-MAP.md` at the root):
```
/
├── CONTEXT-MAP.md
├── docs/adr/ ← system-wide decisions
└── src/
├── ordering/
│ ├── CONTEXT.md
│ └── docs/adr/ ← context-specific decisions
└── billing/
├── CONTEXT.md
└── docs/adr/
```
## Use the glossary's vocabulary
When your output names a domain concept (in an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in `CONTEXT.md`. Don't drift to synonyms the glossary explicitly avoids.
If the concept you need isn't in the glossary yet, that's a signal — either you're inventing language the project doesn't use (reconsider) or there's a real gap (note it for `/domain-modeling`).
## Flag ADR conflicts
If your output contradicts an existing ADR, surface it explicitly rather than silently overriding:
> _Contradicts ADR-0007 (event-sourced orders) — but worth reopening because…_
-45
View File
@@ -1,45 +0,0 @@
# Issue tracker: GitHub
Issues and PRDs for this repo live as GitHub issues. Use the `gh` CLI for all operations.
## Conventions
- **Create an issue**: `gh issue create --title "..." --body "..."`. Use a heredoc for multi-line bodies.
- **Read an issue**: `gh issue view <number> --comments`, filtering comments by `jq` and also fetching labels.
- **List issues**: `gh issue list --state open --json number,title,body,labels,comments --jq '[.[] | {number, title, body, labels: [.labels[].name], comments: [.comments[].body]}]'` with appropriate `--label` and `--state` filters.
- **Comment on an issue**: `gh issue comment <number> --body "..."`
- **Apply / remove labels**: `gh issue edit <number> --add-label "..."` / `--remove-label "..."`
- **Close**: `gh issue close <number> --comment "..."`
Infer the repo from `git remote -v``gh` does this automatically when run inside a clone.
## Pull requests as a triage surface
**PRs as a request surface: no.** _(Set to `yes` if this repo treats external PRs as feature requests; `/triage` reads this flag.)_
When set to `yes`, PRs run through the same labels and states as issues, using the `gh pr` equivalents:
- **Read a PR**: `gh pr view <number> --comments` and `gh pr diff <number>` for the diff.
- **List external PRs for triage**: `gh pr list --state open --json number,title,body,labels,author,authorAssociation,comments` then keep only `authorAssociation` of `CONTRIBUTOR`, `FIRST_TIME_CONTRIBUTOR`, or `NONE` (drop `OWNER`/`MEMBER`/`COLLABORATOR`).
- **Comment / label / close**: `gh pr comment`, `gh pr edit --add-label`/`--remove-label`, `gh pr close`.
GitHub shares one number space across issues and PRs, so a bare `#42` may be either — resolve with `gh pr view 42` and fall back to `gh issue view 42`.
## When a skill says "publish to the issue tracker"
Create a GitHub issue.
## When a skill says "fetch the relevant ticket"
Run `gh issue view <number> --comments`.
## Wayfinding operations
Used by `/wayfinder`. The **map** is a single issue with **child** issues as tickets.
- **Map**: a single issue labelled `wayfinder:map`, holding the Notes / Decisions-so-far / Fog body. `gh issue create --label wayfinder:map`.
- **Child ticket**: an issue linked to the map as a GitHub sub-issue (`gh api` on the sub-issues endpoint). Where sub-issues aren't enabled, add the child to a task list in the map body and put `Part of #<map>` at the top of the child body. Labels: `wayfinder:<type>` (`research`/`prototype`/`grilling`/`task`). Once claimed, the ticket is assigned to the driving dev.
- **Blocking**: GitHub's **native issue dependencies** — the canonical, UI-visible representation. Add an edge with `gh api --method POST repos/<owner>/<repo>/issues/<child>/dependencies/blocked_by -F issue_id=<blocker-db-id>`, where `<blocker-db-id>` is the blocker's numeric **database id** (`gh api repos/<owner>/<repo>/issues/<n> --jq .id`, _not_ the `#number` or `node_id`). GitHub reports `issue_dependencies_summary.blocked_by` (open blockers only — the live gate). Where dependencies aren't available, fall back to a `Blocked by: #<n>, #<n>` line at the top of the child body. A ticket is unblocked when every blocker is closed.
- **Frontier query**: list the map's open children (`gh issue list --state open`, scoped to the map's sub-issues / task list), drop any with an open blocker (`issue_dependencies_summary.blocked_by > 0`, or an open issue in the `Blocked by` line) or an assignee; first in map order wins.
- **Claim**: `gh issue edit <n> --add-assignee @me` — the session's first write.
- **Resolve**: `gh issue comment <n> --body "<answer>"`, then `gh issue close <n>`, then append a context pointer (gist + link) to the map's Decisions-so-far.
-15
View File
@@ -1,15 +0,0 @@
# Triage Labels
The skills speak in terms of five canonical triage roles. This file maps those roles to the actual label strings used in this repo's issue tracker.
| Label in mattpocock/skills | Label in our tracker | Meaning |
| -------------------------- | -------------------- | ---------------------------------------- |
| `needs-triage` | `needs-triage` | Maintainer needs to evaluate this issue |
| `needs-info` | `needs-info` | Waiting on reporter for more information |
| `ready-for-agent` | `ready-for-agent` | Fully specified, ready for an AFK agent |
| `ready-for-human` | `ready-for-human` | Requires human implementation |
| `wontfix` | `wontfix` | Will not be actioned |
When a skill mentions a role (e.g. "apply the AFK-ready triage label"), use the corresponding label string from this table.
Edit the right-hand column to match whatever vocabulary you actually use.
+2 -2
View File
@@ -12,6 +12,6 @@ orchestrator = Orchestrator(
if __name__ == "__main__":
if arg_parser.parsed_argspace.get_by_name("dev"):
orchestrator.run_repl(App(initial_message="ArgentaDev"))
orchestrator.start_polling(App(initial_message="ArgentaDev"))
else:
orchestrator.run_repl(App())
orchestrator.start_polling(App())
+1 -1
View File
@@ -22,7 +22,7 @@ def main():
print(f" Host: {host.value}")
print(f" Port: {port.value}")
orchestrator.run_repl(app)
orchestrator.start_polling(app)
if __name__ == "__main__":
-22
View File
@@ -1,22 +0,0 @@
from argenta import App, Orchestrator
from argenta.command import Router, Command, Response
router = Router(title="Example")
@router.command(Command("hello", description="Say hello"))
def hello_handler(response: Response):
print("Hello, world!")
def create_app() -> App:
app = App()
app.include_router(router)
return app
def main() -> None:
orchestrator = Orchestrator()
orchestrator.run_repl(create_app())
if __name__ == "__main__":
main()
-16
View File
@@ -1,16 +0,0 @@
from argenta import App, Orchestrator
from argenta.command import Router, Command, Response
router = Router(title="Example")
@router.command(Command("hello", description="Say hello"))
def hello_handler(response: Response):
print("Hello, world!")
app = App()
app.include_router(router)
orchestrator = Orchestrator()
if __name__ == "__main__":
orchestrator.run_repl(app)
@@ -1,4 +0,0 @@
my_project/
├── main.py
├── handlers.py
└── .gitignore
-11
View File
@@ -1,11 +0,0 @@
my_project/
├── src/
│ └── my_project/
│ └── application/
│ ├── __init__.py
│ ├── __main__.py
│ ├── routers.py
│ └── handlers/
│ ├── __init__.py
│ └── hello_world_handler.py
└── .gitignore
+1 -1
View File
@@ -24,4 +24,4 @@ orchestrator = Orchestrator(custom_providers=[ConnectionProvider()])
# 4. Start the application
if __name__ == "__main__":
orchestrator.run_repl(app)
orchestrator.start_polling(app)
@@ -60,7 +60,7 @@ orchestrator = Orchestrator()
def main():
app.include_router(router)
orchestrator.run_repl(app)
orchestrator.start_polling(app)
if __name__ == "__main__":
+1 -1
View File
@@ -9,7 +9,7 @@ orchestrator: Orchestrator = Orchestrator()
def main() -> None:
app.include_router(router)
orchestrator.run_repl(app)
orchestrator.start_polling(app)
if __name__ == "__main__":
+1 -1
View File
@@ -30,4 +30,4 @@ app.include_router(main_router)
# 5. Start application
if __name__ == "__main__":
orchestrator.run_repl(app)
orchestrator.start_polling(app)
@@ -15,4 +15,4 @@ app.include_router(router)
# 3. Start polling via orchestrator
if __name__ == "__main__":
orchestrator.run_repl(app)
orchestrator.start_polling(app)
+1 -1
View File
@@ -25,7 +25,7 @@ def test_input_incorrect_command(capsys: CaptureFixture[str]):
app.set_unknown_command_handler(lambda command: print(f"Unknown command: {command.trigger}"))
with patch("builtins.input", side_effect=["help", "q"]):
orchestrator.run_repl(app)
orchestrator.start_polling(app)
output = capsys.readouterr().out
assert "\nUnknown command: help\n" in output
-1
View File
@@ -56,7 +56,6 @@ Argenta предназначена для создания приложений,
root/redirect_stdout
root/dependency_injection
root/cli
root/testing
.. toctree::
-600
View File
@@ -1,600 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) 2025, kolo
# This file is distributed under the same license as the Argenta package.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.
#
msgid ""
msgstr ""
"Project-Id-Version: Argenta \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-03 18:51+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: en\n"
"Language-Team: en <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.17.0\n"
#: ../../root/cli.rst:4
msgid "CLI"
msgstr "CLI"
#: ../../root/cli.rst:6
msgid ""
"Помимо библиотеки, ``Argenta`` поставляется с собственным "
"CLI-инструментом. Он берёт на себя рутину, которая сопровождает "
"разработку CLI-приложений: создаёт каркас проекта, запускает приложение, "
"инспектирует зарегистрированные маршруты и собирает standalone-бинарник."
msgstr ""
"In addition to the library, ``Argenta`` ships with its own CLI tool. It "
"takes over the routine that surrounds CLI app development: scaffolds a "
"project, runs the application, inspects registered routes, and builds a "
"standalone binary."
#: ../../root/cli.rst:8
msgid ""
"CLI поставляется как опциональная зависимость — основная библиотека "
"остаётся лёгкой, а инструмент доступен только тем, кому он нужен."
msgstr ""
"The CLI is shipped as an optional dependency — the core library stays "
"light, and the tool is available only to those who need it."
#: ../../root/cli.rst:11
msgid "Установка"
msgstr "Installation"
#: ../../root/cli.rst:13
msgid "CLI доступен как опциональная зависимость ``[cli]``:"
msgstr "The CLI is available as the optional ``[cli]`` dependency:"
#: ../../root/cli.rst:23
msgid "После установки команда ``argenta`` доступна в терминале:"
msgstr "After installation, the ``argenta`` command is available in the terminal:"
#: ../../root/cli.rst:34
msgid ""
"Если ``argenta`` установлена без extras, команда ``argenta`` не будет "
"доступна. Установите с ``[cli]``, чтобы получить доступ к "
"CLI-инструменту."
msgstr ""
"If ``argenta`` is installed without extras, the ``argenta`` command will "
"not be available. Install with ``[cli]`` to get access to the CLI tool."
#: ../../root/cli.rst:37
msgid "Флаг ``--version``"
msgstr "The ``--version`` flag"
#: ../../root/cli.rst:39
msgid "Показать установленную версию ``Argenta``:"
msgstr "Show the installed ``Argenta`` version:"
#: ../../root/cli.rst:45
msgid "Аналогично через короткий флаг:"
msgstr "Equivalently, via the short flag:"
#: ../../root/cli.rst:56
msgid "Формат entrypoint"
msgstr "Entrypoint format"
#: ../../root/cli.rst:58
msgid ""
"Команды ``run``, ``routes`` и ``build`` принимают **entrypoint** — "
"указатель на объект внутри проекта, который нужно запустить, "
"инспектировать или собрать. Единый формат описан здесь, чтобы не "
"повторяться в каждой команде."
msgstr ""
"The ``run``, ``routes`` and ``build`` commands accept an **entrypoint** —"
" a pointer to an object inside the project to run, inspect, or build. The"
" shared format is documented here once instead of repeating it in each "
"command."
#: ../../root/cli.rst:60
msgid "Формат entrypoint:"
msgstr "Entrypoint format:"
#: ../../root/cli.rst:67
msgid "Поддерживаются два способа адресации:"
msgstr "Two addressing styles are supported:"
#: ../../root/cli.rst:69
msgid ""
"**Путь к файлу** — ``app/main.py:main``. Удобно при работе с конкретным "
"файлом."
msgstr ""
"**File path** — ``app/main.py:main``. Convenient when working with a "
"specific file."
#: ../../root/cli.rst:70
msgid ""
"**Dotted-модуль** — ``my_project.application:main``. Естественно для "
"установленных пакетов."
msgstr ""
"**Dotted module** — ``my_project.application:main``. Natural for "
"installed packages."
#: ../../root/cli.rst:72
msgid ""
"Если передан путь к директории с ``__main__.py``, он разрешается "
"автоматически — указывать файл явно не нужно."
msgstr ""
"If a directory path containing ``__main__.py`` is passed, it is resolved "
"automatically — no need to name the file explicitly."
#: ../../root/cli.rst:74
msgid "**Примеры валидных entrypoint-ов:**"
msgstr "**Examples of valid entrypoints:**"
#: ../../root/cli.rst:84
msgid ""
"Тип объекта зависит от команды: ``run`` и ``build`` ожидают callable, "
"``routes`` — инстанс ``App`` или callable, возвращающий ``App``."
msgstr ""
"The expected object type depends on the command: ``run`` and ``build`` "
"expect a callable, ``routes`` expects an ``App`` instance or a callable "
"returning ``App``."
#: ../../root/cli.rst:89
msgid "Создание проектов"
msgstr "Scaffolding projects"
#: ../../root/cli.rst:92
msgid "Команда ``new``"
msgstr "The ``new`` command"
#: ../../root/cli.rst:94
msgid ""
"Создаёт новую директорию проекта с boilerplate-кодом. Это отправная "
"точка: вместо ручной настройки структуры — готовый каркас за одну "
"команду."
msgstr ""
"Creates a new project directory with boilerplate code. It is the starting"
" point: instead of setting up the structure by hand, a ready-made "
"skeleton in a single command."
#: ../../root/cli.rst:100
msgid "``project_name`` — имя директории проекта (обязательный аргумент)."
msgstr "``project_name`` — project directory name (required argument)."
#: ../../root/cli.rst:101 ../../root/cli.rst:133
msgid "``--arch`` — архитектура проекта: ``flat`` (по умолчанию) или ``src``."
msgstr "``--arch`` — project architecture: ``flat`` (default) or ``src``."
#: ../../root/cli.rst:103 ../../root/cli.rst:135 ../../root/cli.rst:161
#: ../../root/cli.rst:191
msgid "**Примеры:**"
msgstr "**Examples:**"
#: ../../root/cli.rst:110
msgid "При архитектуре ``flat`` создаётся следующая структура:"
msgstr "With the ``flat`` architecture, the following structure is created:"
#: ../../root/cli.rst:115
msgid "При архитектуре ``src``:"
msgstr "With the ``src`` architecture:"
#: ../../root/cli.rst:125
msgid "Команда ``init``"
msgstr "The ``init`` command"
#: ../../root/cli.rst:127
msgid ""
"Делает то же, что и ``new``, но в текущей директории. Удобно, когда "
"проект уже существует и нужно добавить структуру Argenta, не создавая "
"лишний уровень вложенности."
msgstr ""
"Does the same as ``new``, but in the current directory. Convenient when "
"the project already exists and the Argenta structure needs to be added "
"without introducing an extra level of nesting."
#: ../../root/cli.rst:143
msgid ""
"Команда ``init`` не перезаписывает существующие файлы — они будут "
"пропущены."
msgstr "The ``init`` command does not overwrite existing files — they are skipped."
#: ../../root/cli.rst:148
msgid "Запуск приложения"
msgstr "Running an application"
#: ../../root/cli.rst:151
msgid "Команда ``run``"
msgstr "The ``run`` command"
#: ../../root/cli.rst:153
msgid ""
"Запускает оркестратор ``Argenta`` из callable-entrypoint. Это "
"альтернатива прямому вызову ``python main.py``, но с автоматической "
"настройкой окружения."
msgstr ""
"Starts the ``Argenta`` orchestrator from a callable entrypoint. It is an "
"alternative to calling ``python main.py`` directly, but with automatic "
"environment setup."
#: ../../root/cli.rst:159 ../../root/cli.rst:189 ../../root/cli.rst:249
msgid "Формат entrypoint — см. :ref:`Формат entrypoint <cli_entrypoint>`."
msgstr "Entrypoint format — see :ref:`Entrypoint format <cli_entrypoint>`."
#: ../../root/cli.rst:173
msgid ""
"Команда ``run`` устанавливает переменную окружения "
"``RUN_FROM_ARGENTA_RUNNER=1``. ``ArgParser`` видит этот флаг и пропускает"
" парсинг ``sys.argv``, поэтому аргументы самого ``argenta`` (типа "
"``--help``, ``--version``) не конфликтуют с аргументами запускаемого "
"приложения. REPL стартует чисто, без ошибок про неизвестные флаги."
msgstr ""
"The ``run`` command sets the ``RUN_FROM_ARGENTA_RUNNER=1`` environment "
"variable. ``ArgParser`` sees this flag and skips parsing ``sys.argv``, so"
" the ``argenta`` arguments themselves (such as ``--help``, ``--version``)"
" do not conflict with the arguments of the application being launched. "
"The REPL starts cleanly, with no errors about unknown flags."
#: ../../root/cli.rst:178
msgid "Инспекция маршрутов"
msgstr "Inspecting routes"
#: ../../root/cli.rst:181
msgid "Команда ``routes``"
msgstr "The ``routes`` command"
#: ../../root/cli.rst:183
msgid ""
"Отображает все зарегистрированные роутеры, команды, алиасы и флаги в виде"
" дерева. Принимает как инстанс ``App``, так и callable, возвращающий "
"``App``."
msgstr ""
"Displays all registered routers, commands, aliases, and flags as a tree. "
"Accepts either an ``App`` instance or a callable returning ``App``."
#: ../../root/cli.rst:198
msgid ""
"Инстанс ``App`` передаётся напрямую, если роутеры подключены на уровне "
"модуля:"
msgstr ""
"An ``App`` instance is passed directly when routers are registered at the"
" module level:"
#: ../../root/cli.rst:204
msgid ""
"Фабрика ``create_app`` передаётся, если роутеры регистрируются внутри "
"функции — например, зависят от конфига или DI:"
msgstr ""
"A ``create_app`` factory is passed when routers are registered inside a "
"function — for example, when they depend on config or DI:"
#: ../../root/cli.rst:211
msgid ""
"При использовании callable-entrypoint REPL не запускается — фабрика "
"вызывается, и маршруты считываются из возвращённого ``App``."
msgstr ""
"When a callable entrypoint is used, the REPL is not started — the factory"
" is called, and routes are read from the returned ``App``."
#: ../../root/cli.rst:213 ../../root/cli.rst:325
msgid "Пример вывода:"
msgstr "Example output:"
#: ../../root/cli.rst:238
msgid "Сборка бинарника"
msgstr "Building a binary"
#: ../../root/cli.rst:241
msgid "Команда ``build``"
msgstr "The ``build`` command"
#: ../../root/cli.rst:243
msgid ""
"Компилирует проект в standalone-бинарник с помощью `Nuitka "
"<https://nuitka.net/>`_, которая входит в ``[cli]`` extra."
msgstr ""
"Compiles a project into a standalone binary using `Nuitka "
"<https://nuitka.net/>`_, which is included in the ``[cli]`` extra."
#: ../../root/cli.rst:251
msgid ""
"``--output`` / ``-o`` — имя выходного бинарника (по умолчанию — имя файла"
" или пакета)."
msgstr ""
"``--output`` / ``-o`` — output binary name (defaults to the file or "
"package name)."
#: ../../root/cli.rst:252
msgid ""
"``--`` — разделитель, после которого передаются **произвольные флаги "
"Nuitka**. Они добавляются к вызову Nuitka после аргументов Argenta, "
"поэтому могут переопределять дефолты и добавлять любые опции, которые "
"Nuitka поддерживает."
msgstr ""
"``--`` — a separator after which **arbitrary Nuitka flags** are passed."
" They are appended to the Nuitka invocation after Argenta's arguments,"
" so they can override defaults and add any options Nuitka supports."
#: ../../root/cli.rst:254
#, fuzzy
msgid "**Базовые примеры:**"
msgstr "**Basic examples:**"
#: ../../root/cli.rst:262
msgid "**Примеры с флагами Nuitka:**"
msgstr "**Examples with Nuitka flags:**"
#: ../../root/cli.rst:271
msgid "Что делает Argenta по умолчанию"
msgstr "What Argenta does by default"
#: ../../root/cli.rst:273
msgid "Команда ``build`` формирует вызов Nuitka со следующими аргументами:"
msgstr "The ``build`` command assembles a Nuitka invocation with the following arguments:"
#: ../../root/cli.rst:275
msgid ""
"``--standalone --onefile`` — собирает единый бинарник со всеми "
"зависимостями внутри."
msgstr ""
"``--standalone --onefile`` — builds a single binary with all dependencies"
" bundled inside."
#: ../../root/cli.rst:276
msgid ""
"``--output-filename=<name>`` — имя выходного файла (из ``--output`` или "
"имени entrypoint)."
msgstr ""
"``--output-filename=<name>`` — output file name (from ``--output`` or the"
" entrypoint name)."
#: ../../root/cli.rst:277
msgid "``--jobs=<cpu_count>`` — параллельная компиляция на всех ядрах."
msgstr "``--jobs=<cpu_count>`` — parallel compilation across all cores."
#: ../../root/cli.rst:278
msgid ""
"``--lto=no`` — LTO отключён по умолчанию (быстрее сборка, медленнее "
"запуск)."
msgstr ""
"``--lto=no`` — LTO is disabled by default (faster build, slower startup)."
#: ../../root/cli.rst:279
msgid ""
"``--include-windows-runtime-dlls=no`` — на Windows не включает runtime "
"DLL в бинарник."
msgstr ""
"``--include-windows-runtime-dlls=no`` — on Windows, runtime DLLs are not"
" bundled into the binary."
#: ../../root/cli.rst:280
msgid ""
"``--python-flag=-m`` — добавляется автоматически, если entrypoint "
"указывает на ``__main__.py``."
msgstr ""
"``--python-flag=-m`` — added automatically when the entrypoint points at"
" a ``__main__.py``."
#: ../../root/cli.rst:282
msgid ""
"Все эти дефолты можно переопределить, передав соответствующий флаг после "
"``--``. Например, ``-- --lto=yes`` включит LTO, а ``-- --jobs=1`` "
"отключит параллельную сборку."
msgstr ""
"Any of these defaults can be overridden by passing the corresponding flag"
" after ``--``. For example, ``-- --lto=yes`` enables LTO, and ``--"
" --jobs=1`` disables parallel compilation."
#: ../../root/cli.rst:285
msgid "Основные флаги Nuitka и их нюансы"
msgstr "Key Nuitka flags and their trade-offs"
#: ../../root/cli.rst:287
msgid ""
"Полный список флагов — в `документации Nuitka <https://nuitka.net/user-"
"documentation/user-manual.html>`_. Ниже — те, с которыми чаще всего "
"сталкиваются при сборке CLI-приложений."
msgstr ""
"The full flag list is in the `Nuitka documentation <https://nuitka.net"
"/user-documentation/user-manual.html>`_. Below are the ones most often"
" encountered when building CLI applications."
#: ../../root/cli.rst:289
#, python-brace-format
msgid "``--lto={yes,no,auto}``"
msgstr "``--lto={yes,no,auto}``"
#: ../../root/cli.rst:290
msgid ""
"Link-Time Optimization. ``yes`` — бинарник меньше и быстрее запускается, "
"но сборка длится заметно дольше. ``no`` (дефолт Argenta) — сборка "
"быстрее, бинарник больше. ``auto`` — Nuitka выбирает сам. Для "
"production-сборки имеет смысл ``yes``, для итеративной разработки — "
"``no``."
msgstr ""
"Link-Time Optimization. ``yes`` — the binary is smaller and starts"
" faster, but the build takes noticeably longer. ``no`` (Argenta's"
" default) — faster build, larger binary. ``auto`` — Nuitka decides. For"
" production builds ``yes`` makes sense; for iterative development, ``no``."
#: ../../root/cli.rst:292
msgid "``--include-package=<package>``"
msgstr "``--include-package=<package>``"
#: ../../root/cli.rst:293
msgid ""
"Явно включает пакет в бинарник. Nuitka отслеживает импорты статически, "
"поэтому пакеты, которые импортируются динамически (через ``importlib``, "
"плагины, ``__import__``), в бинарник не попадают — их нужно добавлять "
"вручную. Типичные кандидаты: ``numpy``, ``pandas``, ``rich``, "
"``prompt_toolkit``."
msgstr ""
"Explicitly includes a package in the binary. Nuitka tracks imports"
" statically, so packages imported dynamically (via ``importlib``, plugins,"
" ``__import__``) do not end up in the binary — they must be added by hand."
" Common candidates: ``numpy``, ``pandas``, ``rich``, ``prompt_toolkit``."
#: ../../root/cli.rst:295
msgid "``--include-data-files=<source>=<dest>``"
msgstr "``--include-data-files=<source>=<dest>``"
#: ../../root/cli.rst:296
msgid ""
"Включает файлы данных (шаблоны, конфиги, ассеты) в бинарник. Формат: "
"``--include-data-files=assets/logo.png=assets/logo.png``. Для директорий "
"целиком — ``--include-data-dir=assets=assets``. Без этого файлы, которые "
"приложение читает во время выполнения, не будут найдены в собранном "
"бинарнике."
msgstr ""
"Includes data files (templates, configs, assets) into the binary. Format:"
" ``--include-data-files=assets/logo.png=assets/logo.png``. For whole"
" directories — ``--include-data-dir=assets=assets``. Without this, files"
" the application reads at runtime will not be found inside the built binary."
#: ../../root/cli.rst:298
msgid "``--enable-plugin=<plugin>``"
msgstr "``--enable-plugin=<plugin>``"
#: ../../root/cli.rst:299
msgid ""
"Включает `плагин Nuitka <https://nuitka.net/user-documentation/user-"
"manual.html#plugins>`_ для поддержки фреймворков, требующих специальной "
"обработки. Распространённые: ``anti-bloat`` (вырезает ненужные части "
"тяжёлых пакетов), ``numpy`` (корректная сборка с numpy), ``tk-inter`` "
"(Tkinter GUI), ``triton`` (PyTorch triton kernels)."
msgstr ""
"Enables a `Nuitka plugin <https://nuitka.net/user-documentation/user-"
"manual.html#plugins>`_ for frameworks that need special handling. Common"
" ones: ``anti-bloat`` (strips unneeded parts of heavy packages), ``numpy``"
" (correct numpy bundling), ``tk-inter`` (Tkinter GUI), ``triton`` (PyTorch"
" triton kernels)."
#: ../../root/cli.rst:301
msgid "``--onefile`` / ``--standalone``"
msgstr "``--onefile`` / ``--standalone``"
#: ../../root/cli.rst:302
msgid ""
"``--onefile`` (дефолт Argenta) — единый бинарник, удобный для "
"дистрибуции. При запуске распаковывается во временную директорию, поэтому"
" стартует медленнее. ``--standalone`` — папка с бинарником и "
"зависимостями, стартует быстрее, но дистрибуция — это вся папка целиком. "
"Чтобы переключиться: ``-- --standalone`` (переопределит дефолтный "
"``--onefile``)."
msgstr ""
"``--onefile`` (Argenta's default) — a single binary, convenient for"
" distribution. It unpacks into a temporary directory on startup, so it"
" starts slower. ``--standalone`` — a folder with the binary and its"
" dependencies; starts faster, but distribution means shipping the whole"
" folder. To switch: ``-- --standalone`` (overrides the default ``--onefile``)."
#: ../../root/cli.rst:304
msgid "``--jobs=<n>``"
msgstr "``--jobs=<n>``"
#: ../../root/cli.rst:305
msgid ""
"Количество параллельных процессов компиляции. Дефолт Argenta — все ядра "
"(``os.cpu_count()``). На машинах с малым объёмом памяти имеет смысл "
"ограничить: ``-- --jobs=2``."
msgstr ""
"Number of parallel compilation processes. Argenta's default is all cores"
" (``os.cpu_count()``). On memory-constrained machines it makes sense to"
" limit it: ``-- --jobs=2``."
#: ../../root/cli.rst:314
msgid "Информация об окружении"
msgstr "Environment information"
#: ../../root/cli.rst:317
msgid "Команда ``info``"
msgstr "The ``info`` command"
#: ../../root/cli.rst:319
msgid ""
"Отображает версию ``Argenta``, версию Python, платформу и ссылку на "
"документацию."
msgstr ""
"Displays the ``Argenta`` version, Python version, platform, and a link to"
" the documentation."
#~ msgid "Командная строка"
#~ msgstr "Command Line Interface"
#~ msgid "Создаёт новую директорию проекта с boilerplate-кодом."
#~ msgstr "Creates a new project directory with boilerplate code."
#~ msgid ""
#~ "``entrypoint`` — путь к callable в "
#~ "формате ``<path/to/file.py>:<callable>`` или "
#~ "``<path.to.module>:<callable>``."
#~ msgstr ""
#~ "``entrypoint`` — path to a callable "
#~ "in the format ``<path/to/file.py>:<callable>`` "
#~ "or ``<path.to.module>:<callable>``."
#~ msgid ""
#~ "Команда ``run`` устанавливает переменную "
#~ "окружения ``RUN_FROM_ARGENTA_RUNNER=1``, что "
#~ "отключает парсинг аргументов командной строки"
#~ " в ``ArgParser``. Это позволяет запустить"
#~ " REPL без конфликтов с CLI-аргументами "
#~ "``argenta``."
#~ msgstr ""
#~ "The ``run`` command sets the "
#~ "``RUN_FROM_ARGENTA_RUNNER=1`` environment variable, "
#~ "which disables command-line argument "
#~ "parsing in ``ArgParser``. This allows "
#~ "starting the REPL without conflicts with"
#~ " ``argenta`` CLI arguments."
#~ msgid ""
#~ "``entrypoint`` — путь к ``App`` или "
#~ "callable в формате "
#~ "``<path/to/file.py>:<app_or_callable>``."
#~ msgstr ""
#~ "``entrypoint`` — path to an ``App`` "
#~ "or callable in the format "
#~ "``<path/to/file.py>:<app_or_callable>``."
#~ msgid "Если передан инстанс ``App``:"
#~ msgstr "If an ``App`` instance is passed:"
#~ msgid ""
#~ "Если передан callable (фабрика), он "
#~ "будет вызван, и результат будет "
#~ "использован для отображения маршрутов:"
#~ msgstr ""
#~ "If a callable (factory) is passed, "
#~ "it will be called, and the result"
#~ " will be used to display routes:"
#~ msgid ""
#~ "``entrypoint`` — путь к callable в "
#~ "формате ``<path/to/file.py>:<callable>``."
#~ msgstr ""
#~ "``entrypoint`` — path to a callable "
#~ "in the format ``<path/to/file.py>:<callable>``."
#~ msgid "Для использования команды ``build`` необходимо установить ``Nuitka``:"
#~ msgstr "To use the ``build`` command, you must install ``Nuitka``:"
#~ msgid "Формат ентрипойнтов"
#~ msgstr "Entrypoint Format"
#~ msgid ""
#~ "Все команды, принимающие ентрипойнт (``run``,"
#~ " ``routes``, ``build``), используют единый "
#~ "формат:"
#~ msgstr ""
#~ "All commands that accept an entrypoint"
#~ " (``run``, ``routes``, ``build``) use a "
#~ "unified format:"
#~ msgid ""
#~ "Поддерживаются как пути к файлам, так"
#~ " и dotted-модули. Если передан путь к"
#~ " директории с ``__main__.py``, он будет "
#~ "разрешён автоматически."
#~ msgstr ""
#~ "Both file paths and dotted modules "
#~ "are supported. If a directory path "
#~ "containing ``__main__.py`` is passed, it "
#~ "will be resolved automatically."
+2 -3
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Argenta \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-03 17:30+0300\n"
"POT-Creation-Date: 2025-12-04 20:39+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: en\n"
@@ -82,9 +82,8 @@ msgid "E2E-тестирование цикла"
msgstr "E2E Testing of the Loop"
#: ../../root/testing.rst:48
#, fuzzy
msgid ""
"Полный запуск цикла ``run_repl`` можно покрывать через подпроцесс с "
"Полный запуск цикла ``start_polling`` можно покрывать через подпроцесс с "
"передачей строк в ``stdin``. Это тяжелее и обычно не требуется. Если всё "
"же необходимо — пример ниже."
msgstr ""
+1 -1
View File
@@ -159,7 +159,7 @@ PredefinedMessages
app.add_message_on_startup(PredefinedMessages.AUTOCOMPLETE)
app.add_message_on_startup(PredefinedMessages.HELP)
orchestrator.run_repl(app)
orchestrator.start_polling(app)
if __name__ == "__main__":
main()
+1 -1
View File
@@ -36,7 +36,7 @@ Orchestrator
Основные методы
----------------
.. py:method:: run_repl(self, app: App) -> None
.. py:method:: start_polling(self, app: App) -> None
Это главный метод, который запускает приложение. Он запускает бесконечный цикл ввода -> вывода.
-330
View File
@@ -1,330 +0,0 @@
.. _root_cli:
CLI
===
Помимо библиотеки, ``Argenta`` поставляется с собственным CLI-инструментом. Он берёт на себя рутину, которая сопровождает разработку CLI-приложений: создаёт каркас проекта, запускает приложение, инспектирует зарегистрированные маршруты и собирает standalone-бинарник.
CLI поставляется как опциональная зависимость — основная библиотека остаётся лёгкой, а инструмент доступен только тем, кому он нужен.
Установка
---------
CLI доступен как опциональная зависимость ``[cli]``:
.. code-block:: shell
pip install argenta[cli]
.. code-block:: shell
uv add argenta[cli]
После установки команда ``argenta`` доступна в терминале:
.. code-block:: shell
argenta --help
.. image:: https://i.ibb.co/p60T7fvh/image.png
:alt: Argenta CLI help
.. note::
Если ``argenta`` установлена без extras, команда ``argenta`` не будет доступна. Установите с ``[cli]``, чтобы получить доступ к CLI-инструменту.
Флаг ``--version``
~~~~~~~~~~~~~~~~~~
Показать установленную версию ``Argenta``:
.. code-block:: shell
argenta --version
Аналогично через короткий флаг:
.. code-block:: shell
argenta -v
-----
.. _cli_entrypoint:
Формат entrypoint
-----------------
Команды ``run``, ``routes`` и ``build`` принимают **entrypoint** — указатель на объект внутри проекта, который нужно запустить, инспектировать или собрать. Единый формат описан здесь, чтобы не повторяться в каждой команде.
Формат entrypoint:
.. code-block:: text
<path/to/file.py>:<object_name>
<path.to.module>:<object_name>
Поддерживаются два способа адресации:
* **Путь к файлу**``app/main.py:main``. Удобно при работе с конкретным файлом.
* **Dotted-модуль**``my_project.application:main``. Естественно для установленных пакетов.
Если передан путь к директории с ``__main__.py``, он разрешается автоматически — указывать файл явно не нужно.
**Примеры валидных entrypoint-ов:**
.. code-block:: text
app/main.py:main
app/main.py:app
app/main.py:create_app
my_project.application:main
my_project/application/__main__.py:main
Тип объекта зависит от команды: ``run`` и ``build`` ожидают callable, ``routes`` — инстанс ``App`` или callable, возвращающий ``App``.
-----
Создание проектов
-----------------
Команда ``new``
~~~~~~~~~~~~~~~~
Создаёт новую директорию проекта с boilerplate-кодом. Это отправная точка: вместо ручной настройки структуры — готовый каркас за одну команду.
.. code-block:: shell
argenta new <project_name> [--arch flat|src]
* ``project_name`` — имя директории проекта (обязательный аргумент).
* ``--arch`` — архитектура проекта: ``flat`` (по умолчанию) или ``src``.
**Примеры:**
.. code-block:: shell
argenta new my-app
argenta new my-app --arch src
При архитектуре ``flat`` создаётся следующая структура:
.. literalinclude:: ../code_snippets/cli/flat_structure.txt
:language: text
При архитектуре ``src``:
.. literalinclude:: ../code_snippets/cli/src_structure.txt
:language: text
.. image:: https://i.ibb.co/gY6zTQd/image.png
:alt: argenta new command output
Команда ``init``
~~~~~~~~~~~~~~~~~
Делает то же, что и ``new``, но в текущей директории. Удобно, когда проект уже существует и нужно добавить структуру Argenta, не создавая лишний уровень вложенности.
.. code-block:: shell
argenta init [--arch flat|src]
* ``--arch`` — архитектура проекта: ``flat`` (по умолчанию) или ``src``.
**Примеры:**
.. code-block:: shell
argenta init
argenta init --arch src
.. note::
Команда ``init`` не перезаписывает существующие файлы — они будут пропущены.
-----
Запуск приложения
-----------------
Команда ``run``
~~~~~~~~~~~~~~~~
Запускает оркестратор ``Argenta`` из callable-entrypoint. Это альтернатива прямому вызову ``python main.py``, но с автоматической настройкой окружения.
.. code-block:: shell
argenta run <entrypoint>
Формат entrypoint — см. :ref:`Формат entrypoint <cli_entrypoint>`.
**Примеры:**
.. code-block:: shell
argenta run app/main.py:main
argenta run my_project.application:main
.. image:: https://i.ibb.co/fVPzxWxp/image.png
:alt: argenta run command output
.. note::
Команда ``run`` устанавливает переменную окружения ``RUN_FROM_ARGENTA_RUNNER=1``. ``ArgParser`` видит этот флаг и пропускает парсинг ``sys.argv``, поэтому аргументы самого ``argenta`` (типа ``--help``, ``--version``) не конфликтуют с аргументами запускаемого приложения. REPL стартует чисто, без ошибок про неизвестные флаги.
-----
Инспекция маршрутов
-------------------
Команда ``routes``
~~~~~~~~~~~~~~~~~~~
Отображает все зарегистрированные роутеры, команды, алиасы и флаги в виде дерева. Принимает как инстанс ``App``, так и callable, возвращающий ``App``.
.. code-block:: shell
argenta routes <entrypoint>
Формат entrypoint — см. :ref:`Формат entrypoint <cli_entrypoint>`.
**Примеры:**
.. code-block:: shell
argenta routes app/main.py:app
argenta routes app/main.py:create_app
Инстанс ``App`` передаётся напрямую, если роутеры подключены на уровне модуля:
.. literalinclude:: ../code_snippets/cli/app_instance.py
:language: python
:linenos:
Фабрика ``create_app`` передаётся, если роутеры регистрируются внутри функции — например, зависят от конфига или DI:
.. literalinclude:: ../code_snippets/cli/app_factory.py
:language: python
:linenos:
.. note::
При использовании callable-entrypoint REPL не запускается — фабрика вызывается, и маршруты считываются из возвращённого ``App``.
Пример вывода:
.. code-block:: text
──────────────────────────────────────────
App Stats
──────────────────────────────────────────
Total Routers: 1
Total Commands: 1
Total Aliases: 0
Total Flags: 0
──────────────────────────────────────────
📦 App object: <App>
└── 📁 Router: Example
└── ⚡ hello
📝 description: Say hello
.. image:: https://i.ibb.co/wNFvKcqM/image.png
:alt: argenta routes command output
-----
Сборка бинарника
----------------
Команда ``build``
~~~~~~~~~~~~~~~~~~
Компилирует проект в standalone-бинарник с помощью `Nuitka <https://nuitka.net/>`_, которая входит в ``[cli]`` extra.
.. code-block:: shell
argenta build <entrypoint> [--output <name>] [-- <nuitka-flags>...]
Формат entrypoint — см. :ref:`Формат entrypoint <cli_entrypoint>`.
* ``--output`` / ``-o`` — имя выходного бинарника (по умолчанию — имя файла или пакета).
* ``--`` — разделитель, после которого передаются **произвольные флаги Nuitka**. Они добавляются к вызову Nuitka после аргументов Argenta, поэтому могут переопределять дефолты и добавлять любые опции, которые Nuitka поддерживает.
**Базовые примеры:**
.. code-block:: shell
argenta build app/main.py:main
argenta build app/main.py:main --output myapp
argenta build app/__main__.py:main -o myapp
**Примеры с флагами Nuitka:**
.. code-block:: shell
argenta build app/main.py:main -- --lto=yes
argenta build app/main.py:main -- --include-package=numpy
argenta build app/main.py:main -o myapp -- --lto=auto --include-data-files=assets/*=assets/
Что делает Argenta по умолчанию
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Команда ``build`` формирует вызов Nuitka со следующими аргументами:
* ``--standalone --onefile`` — собирает единый бинарник со всеми зависимостями внутри.
* ``--output-filename=<name>`` — имя выходного файла (из ``--output`` или имени entrypoint).
* ``--jobs=<cpu_count>`` — параллельная компиляция на всех ядрах.
* ``--lto=no`` — LTO отключён по умолчанию (быстрее сборка, медленнее запуск).
* ``--include-windows-runtime-dlls=no`` — на Windows не включает runtime DLL в бинарник.
* ``--python-flag=-m`` — добавляется автоматически, если entrypoint указывает на ``__main__.py``.
Все эти дефолты можно переопределить, передав соответствующий флаг после ``--``. Например, ``-- --lto=yes`` включит LTO, а ``-- --jobs=1`` отключит параллельную сборку.
Основные флаги Nuitka и их нюансы
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Полный список флагов — в `документации Nuitka <https://nuitka.net/user-documentation/user-manual.html>`_. Ниже — те, с которыми чаще всего сталкиваются при сборке CLI-приложений.
``--lto={yes,no,auto}``
Link-Time Optimization. ``yes`` — бинарник меньше и быстрее запускается, но сборка длится заметно дольше. ``no`` (дефолт Argenta) — сборка быстрее, бинарник больше. ``auto`` — Nuitka выбирает сам. Для production-сборки имеет смысл ``yes``, для итеративной разработки — ``no``.
``--include-package=<package>``
Явно включает пакет в бинарник. Nuitka отслеживает импорты статически, поэтому пакеты, которые импортируются динамически (через ``importlib``, плагины, ``__import__``), в бинарник не попадают — их нужно добавлять вручную. Типичные кандидаты: ``numpy``, ``pandas``, ``rich``, ``prompt_toolkit``.
``--include-data-files=<source>=<dest>``
Включает файлы данных (шаблоны, конфиги, ассеты) в бинарник. Формат: ``--include-data-files=assets/logo.png=assets/logo.png``. Для директорий целиком — ``--include-data-dir=assets=assets``. Без этого файлы, которые приложение читает во время выполнения, не будут найдены в собранном бинарнике.
``--enable-plugin=<plugin>``
Включает `плагин Nuitka <https://nuitka.net/user-documentation/user-manual.html#plugins>`_ для поддержки фреймворков, требующих специальной обработки. Распространённые: ``anti-bloat`` (вырезает ненужные части тяжёлых пакетов), ``numpy`` (корректная сборка с numpy), ``tk-inter`` (Tkinter GUI), ``triton`` (PyTorch triton kernels).
``--onefile`` / ``--standalone``
``--onefile`` (дефолт Argenta) — единый бинарник, удобный для дистрибуции. При запуске распаковывается во временную директорию, поэтому стартует медленнее. ``--standalone`` — папка с бинарником и зависимостями, стартует быстрее, но дистрибуция — это вся папка целиком. Чтобы переключиться: ``-- --standalone`` (переопределит дефолтный ``--onefile``).
``--jobs=<n>``
Количество параллельных процессов компиляции. Дефолт Argenta — все ядра (``os.cpu_count()``). На машинах с малым объёмом памяти имеет смысл ограничить: ``-- --jobs=2``.
.. image:: https://i.ibb.co/VsVXxf7/image.png
:alt: argenta build command output
-----
Информация об окружении
-----------------------
Команда ``info``
~~~~~~~~~~~~~~~~~
Отображает версию ``Argenta``, версию Python, платформу и ссылку на документацию.
.. code-block:: shell
argenta info
Пример вывода:
.. code-block:: text
Argenta 1.2.0
Python 3.13.0
Platform Linux-7.1.5-zen1-2-zen-x86_64-with-glibc2.40
Docs https://argenta.readthedocs.io
.. image:: https://i.ibb.co/B5k8Ftyg/image.png
:alt: argenta info command output
+1 -1
View File
@@ -45,7 +45,7 @@
E2E-тестирование цикла
----------------------
Полный запуск цикла ``run_repl`` можно покрывать через подпроцесс с передачей строк в ``stdin``. Это тяжелее и обычно не требуется. Если всё же необходимо — пример ниже.
Полный запуск цикла ``start_polling`` можно покрывать через подпроцесс с передачей строк в ``stdin``. Это тяжелее и обычно не требуется. Если всё же необходимо — пример ниже.
.. danger::
**Важно:** Обязательно передавайте строковый триггер команды выхода последним элементом в списке ``side_effects`` при патче ``input``.
+8 -29
View File
@@ -1,57 +1,36 @@
set windows-shell := ["powershell.exe", "-NoLogo", "-Command"]
set shell := ["bash", "-c"]
# List all available recipes
# Вывести список всех рецептов
default:
@just --list
# ── Testing ───────────────────────────────────────────────────────────────────
# Run tests via pytest
# Запустить тесты через pytest
tests:
python -m pytest tests
# Run tests with coverage report
# Запустить тесты с отчетом о покрытии
tests-cov:
python -m pytest --cov=argenta tests
# Run tests with coverage HTML report
# Запустить тесты с отчетом о покрытии с html репортом
tests-cov-html:
python -m pytest --cov=argenta tests --cov-report=html
# ── Code quality ──────────────────────────────────────────────────────────────
# Format code (Ruff + isort)
# Отформатировать код (Ruff + isort)
format:
python -m ruff format ./src
python -m isort ./src
# Check types via mypy (strict)
# Проверить типы через mypy (strict)
mypy:
python -m mypy -p argenta --strict
# Check style via wemake-python-styleguide
# Проверить стиль через wemake-python-styleguide
wps:
python -m flake8 --format=wemake ./src
# Run Ruff linter
# Запустить линтер Ruff
ruff:
python -m ruff check ./src
# Run all checks (format, mypy, ruff, wps)
check-format: format mypy ruff wps
# ── Changelog (scriv) ─────────────────────────────────────────────────────────
# Create a new changelog fragment and open it in $EDITOR
frag:
if (-not (Test-Path "./changelog.d")) { New-Item -ItemType Directory -Path "./changelog.d" }
scriv create --add
# Preview collected changelog without writing anything
changelog-preview:
scriv collect --dry-run
# Collect fragments into CHANGELOG.md for release (usage: just release 1.2.3)
release version:
scriv collect --version {{ version }} --add
+6 -6
View File
@@ -1,18 +1,18 @@
from argenta import App, Command, Orchestrator
from argenta import App, Orchestrator, Command
from argenta.app import DynamicDividingLine
from .handlers import router
app = App(initial_message="metrics", exit_command=Command("exit", aliases=["quit"]))
app.include_router(router)
app = App(initial_message="metrics", exit_command=Command('exit', aliases=['quit']))
orchestrator = Orchestrator()
def main() -> None:
app.include_router(router)
app.set_description_message_pattern(
lambda command, description: f"[bold cyan]▸[/bold cyan] [bold white]{command}[/bold white] [dim]│[/dim] [yellow italic]{description}[/yellow italic]"
lambda command, description: f'[bold cyan]▸[/bold cyan] [bold white]{command}[/bold white] [dim]│[/dim] [yellow italic]{description}[/yellow italic]'
)
orchestrator.run_repl(app)
orchestrator.start_polling(app)
if __name__ == "__main__":
-2
View File
@@ -13,10 +13,8 @@ class BenchmarksNotFound(Exception):
def __str__(self) -> str:
return f"Benchmarks with type '{self.type_}' not found"
class BenchmarksWithSameNameAlreadyExists(Exception):
def __init__(self, benchmark_name: str):
self.benchmark_name = benchmark_name
def __str__(self) -> str:
return f"Benchmarks with name '{self.benchmark_name}' already exists"
+30 -25
View File
@@ -1,11 +1,16 @@
__all__ = ["Benchmark", "Benchmarks", "BenchmarkResult", "BenchmarkGroupResult"]
__all__ = [
"Benchmark",
"Benchmarks",
"BenchmarkResult",
"BenchmarkGroupResult"
]
import gc
import io
import statistics
import time
from contextlib import redirect_stdout
from dataclasses import dataclass
import time
import gc
import statistics
from typing import Callable, override
from .exceptions import BenchmarkNotFound, BenchmarksNotFound, BenchmarksWithSameNameAlreadyExists
@@ -35,7 +40,14 @@ class BenchmarkGroupResult:
class Benchmark:
def __init__(self, func: FuncForBenchmark, *, type_: str, name: str, description: str) -> None:
def __init__(
self,
func: FuncForBenchmark,
*,
type_: str,
name: str,
description: str
) -> None:
self.func = func
self.type_ = type_
self.name = name
@@ -66,11 +78,11 @@ class Benchmark:
@override
def __repr__(self) -> str:
return f"Benchmark<{self.type_=}, {self.name=}, {self.description=}>"
return f'Benchmark<{self.type_=}, {self.name=}, {self.description=}>'
@override
def __str__(self) -> str:
return f"benchmark {self.name} with type {self.type_}"
return f'benchmark {self.name} with type {self.type_}'
class Benchmarks:
@@ -80,14 +92,16 @@ class Benchmarks:
self._benchmarks_paired_by_name: dict[str, Benchmark] = {}
def register(
self, type_: str, description: str = ""
self,
type_: str,
description: str = ""
) -> Callable[[FuncForBenchmark], FuncForBenchmark]:
def decorator(func: FuncForBenchmark) -> FuncForBenchmark:
benchmark = Benchmark(
func,
type_=type_,
name=func.__name__,
description=description or f"description for {func.__name__} with type {type_}",
description=description or f'description for {func.__name__} with type {type_}',
)
if self._benchmarks_paired_by_name.get(func.__name__):
raise BenchmarksWithSameNameAlreadyExists(func.__name__)
@@ -96,12 +110,9 @@ class Benchmarks:
self._benchmarks.append(benchmark)
self._benchmarks_grouped_by_type.setdefault(type_, []).append(benchmark)
return func
return decorator
def run_benchmark_by_name(
self, name: str, iterations: int = 100, is_gc_disables: bool = False
) -> BenchmarkResult:
def run_benchmark_by_name(self, name: str, iterations: int = 100, is_gc_disables: bool = False) -> BenchmarkResult:
benchmark = self.get_benchmark_by_name(name)
if not benchmark:
raise BenchmarkNotFound(name)
@@ -119,34 +130,28 @@ class Benchmarks:
is_gc_disabled=is_gc_disables,
avg_time=avg,
median_time=median,
std_dev=std_dev,
std_dev=std_dev
)
def run_benchmarks_by_type(
self, type_: str, iterations: int = 100, is_gc_disabled: bool = False
) -> BenchmarkGroupResult:
def run_benchmarks_by_type(self, type_: str, iterations: int = 100, is_gc_disabled: bool = False) -> BenchmarkGroupResult:
benchmarks = self.get_benchmarks_by_type(type_)
if not benchmarks:
raise BenchmarksNotFound(type_)
benchmark_results: list[BenchmarkResult] = []
for benchmark in benchmarks:
benchmark_results.append(
self.run_benchmark_by_name(benchmark.name, iterations, is_gc_disabled)
)
benchmark_results.append(self.run_benchmark_by_name(benchmark.name, iterations, is_gc_disabled))
return BenchmarkGroupResult(
type_=type_,
iterations=iterations,
is_gc_disabled=is_gc_disabled,
benchmark_results=benchmark_results,
benchmark_results=benchmark_results
)
def run_benchmarks_grouped_by_type(
self, iterations: int = 100, is_gc_disabled: bool = False
) -> list[BenchmarkGroupResult]:
def run_benchmarks_grouped_by_type(self, iterations: int = 100, is_gc_disabled: bool = False) -> list[BenchmarkGroupResult]:
results: list[BenchmarkGroupResult] = []
for type_, _ in self._benchmarks_grouped_by_type.items():
for type_, benchmarks in self._benchmarks_grouped_by_type.items():
results.append(self.run_benchmarks_by_type(type_, iterations, is_gc_disabled))
return results
+16 -21
View File
@@ -3,11 +3,11 @@ __all__ = [
"benchmark_command_with_flags",
"benchmark_many_commands",
"benchmark_command_with_many_flags",
"benchmark_extreme_router",
"benchmark_extreme_router"
]
from argenta.command import Flag, Flags
from argenta.command.models import Command, InputCommand
from argenta.command import Flag, Flags
from argenta.response import Response
from argenta.router import Router
@@ -18,11 +18,11 @@ from .entity import benchmarks
def benchmark_simple_command() -> None:
router = Router()
@router.command(Command("test"))
@router.command(Command('test'))
def handler(_res: Response) -> None:
pass
input_cmd = InputCommand.parse("test")
input_cmd = InputCommand.parse('test')
router.finds_appropriate_handler(input_cmd)
@@ -30,11 +30,11 @@ def benchmark_simple_command() -> None:
def benchmark_command_with_flags() -> None:
router = Router()
@router.command(Command("test", flags=Flags([Flag("a"), Flag("b"), Flag("c")])))
@router.command(Command('test', flags=Flags([Flag('a'), Flag('b'), Flag('c')])))
def handler(_res: Response) -> None:
pass
input_cmd = InputCommand.parse("test -a -b -c")
input_cmd = InputCommand.parse('test -a -b -c')
router.finds_appropriate_handler(input_cmd)
@@ -43,43 +43,38 @@ def benchmark_many_commands() -> None:
router = Router()
for i in range(50):
@router.command(Command(f"cmd{i}"))
@router.command(Command(f'cmd{i}'))
def handler(_res: Response) -> None:
pass
input_cmd = InputCommand.parse("cmd25")
input_cmd = InputCommand.parse('cmd25')
router.finds_appropriate_handler(input_cmd)
@benchmarks.register(
type_="finds_appropriate_handler", description="Command with many flags (20 flags)"
)
@benchmarks.register(type_="finds_appropriate_handler", description="Command with many flags (20 flags)")
def benchmark_command_with_many_flags() -> None:
router = Router()
flags = Flags([Flag(f"flag{i}") for i in range(20)])
flags = Flags([Flag(f'flag{i}') for i in range(20)])
@router.command(Command("test", flags=flags))
@router.command(Command('test', flags=flags))
def handler(_res: Response) -> None:
pass
input_cmd = InputCommand.parse("test " + " ".join(f"-flag{i}" for i in range(10)))
input_cmd = InputCommand.parse('test ' + ' '.join(f'-flag{i}' for i in range(10)))
router.finds_appropriate_handler(input_cmd)
@benchmarks.register(
type_="finds_appropriate_handler", description="Extreme (100 commands, 10 flags each)"
)
@benchmarks.register(type_="finds_appropriate_handler", description="Extreme (100 commands, 10 flags each)")
def benchmark_extreme_router() -> None:
router = Router()
for i in range(100):
flags = Flags([Flag(f"f{i}_{j}") for j in range(10)])
flags = Flags([Flag(f'f{i}_{j}') for j in range(10)])
@router.command(Command(f"cmd{i}", flags=flags))
@router.command(Command(f'cmd{i}', flags=flags))
def handler(_res: Response) -> None:
pass
input_cmd = InputCommand.parse("cmd50 -f50_0 -f50_1 -f50_2")
input_cmd = InputCommand.parse('cmd50 -f50_0 -f50_1 -f50_2')
router.finds_appropriate_handler(input_cmd)
+29 -13
View File
@@ -7,7 +7,7 @@ __all__ = [
"benchmark_validate_regex_complex",
"benchmark_validate_multiple_flags_10",
"benchmark_validate_multiple_flags_50",
"benchmark_validate_extreme_100_flags",
"benchmark_validate_extreme_100_flags"
]
import re
@@ -58,29 +58,45 @@ def benchmark_validate_regex_complex() -> None:
@benchmarks.register(type_="flag_validation", description="Multiple flags validation (10 flags)")
def benchmark_validate_multiple_flags_10() -> None:
flags = [Flag(f"flag{i}", possible_values=PossibleValues.ALL) for i in range(10)]
input_flags = [InputFlag(f"flag{i}", input_value=f"value{i}") for i in range(10)]
flags = [
Flag(f"flag{i}", possible_values=PossibleValues.ALL)
for i in range(10)
]
input_flags = [
InputFlag(f"flag{i}", input_value=f"value{i}")
for i in range(10)
]
for flag, input_flag in zip(flags, input_flags):
flag.validate_input_flag_value(input_flag.input_value)
@benchmarks.register(type_="flag_validation", description="Multiple flags validation (50 flags)")
def benchmark_validate_multiple_flags_50() -> None:
flags = [Flag(f"flag{i}", possible_values=PossibleValues.ALL) for i in range(50)]
input_flags = [InputFlag(f"flag{i}", input_value=f"value{i}") for i in range(50)]
flags = [
Flag(f"flag{i}", possible_values=PossibleValues.ALL)
for i in range(50)
]
input_flags = [
InputFlag(f"flag{i}", input_value=f"value{i}")
for i in range(50)
]
for flag, input_flag in zip(flags, input_flags):
flag.validate_input_flag_value(input_flag.input_value)
@benchmarks.register(
type_="flag_validation", description="Extreme (100 flags with regex validation)"
)
@benchmarks.register(type_="flag_validation", description="Extreme (100 flags with regex validation)")
def benchmark_validate_extreme_100_flags() -> None:
pattern = re.compile(r"^[a-zA-Z0-9_-]+$")
flags = [Flag(f"flag{i}", possible_values=pattern) for i in range(100)]
input_flags = [InputFlag(f"flag{i}", input_value=f"valid_value_{i}") for i in range(100)]
flags = [
Flag(f"flag{i}", possible_values=pattern)
for i in range(100)
]
input_flags = [
InputFlag(f"flag{i}", input_value=f"valid_value_{i}")
for i in range(100)
]
for flag, input_flag in zip(flags, input_flags):
flag.validate_input_flag_value(input_flag.input_value)
+4 -10
View File
@@ -5,7 +5,7 @@ __all__ = [
"benchmark_command_with_mixed_prefixes",
"benchmark_command_with_long_values",
"benchmark_command_with_quoted_values",
"benchmark_extreme_many_flags",
"benchmark_extreme_many_flags"
]
from argenta.command.models import InputCommand
@@ -23,16 +23,12 @@ def benchmark_command_with_few_flags() -> None:
InputCommand.parse("start -a -b -c")
@benchmarks.register(
type_="input_command_parse", description="Command with flags and values (5 flags)"
)
@benchmarks.register(type_="input_command_parse", description="Command with flags and values (5 flags)")
def benchmark_command_with_flags_and_values() -> None:
InputCommand.parse("start --host localhost --port 8080 --debug --verbose -c config.json")
@benchmarks.register(
type_="input_command_parse", description="Command with mixed prefixes (-, --, ---)"
)
@benchmarks.register(type_="input_command_parse", description="Command with mixed prefixes (-, --, ---)")
def benchmark_command_with_mixed_prefixes() -> None:
InputCommand.parse("cmd -a --bb ---ccc -d value --ee value2 ---fff value3")
@@ -44,9 +40,7 @@ def benchmark_command_with_long_values() -> None:
InputCommand.parse(cmd)
@benchmarks.register(
type_="input_command_parse", description="Command with quoted values (5 flags)"
)
@benchmarks.register(type_="input_command_parse", description="Command with quoted values (5 flags)")
def benchmark_command_with_quoted_values() -> None:
InputCommand.parse("cmd --text 'hello world' --path '/usr/local/bin' --msg \"test message\"")
+8 -20
View File
@@ -3,7 +3,7 @@ __all__ = [
"benchmark_many_commands_most_similar",
"benchmark_many_aliases",
"benchmark_partial_match",
"benchmark_extreme_commands",
"benchmark_extreme_commands"
]
from argenta import App
@@ -19,11 +19,9 @@ def setup_app_with_commands(command_count: int, aliases_per_command: int = 0) ->
router = Router()
for i in range(command_count):
aliases = (
{f"alias{i}_{j}" for j in range(aliases_per_command)} if aliases_per_command else set()
)
aliases = {f'alias{i}_{j}' for j in range(aliases_per_command)} if aliases_per_command else set()
@router.command(Command(f"command{i}", aliases=aliases))
@router.command(Command(f'command{i}', aliases=aliases))
def handler(_res: Response) -> None:
pass
@@ -31,41 +29,31 @@ def setup_app_with_commands(command_count: int, aliases_per_command: int = 0) ->
return app
@benchmarks.register(
type_="most_similar_command", description="Few commands (10 commands, no match)"
)
@benchmarks.register(type_="most_similar_command", description="Few commands (10 commands, no match)")
def benchmark_few_commands() -> None:
app = setup_app_with_commands(10)
app._most_similar_command("unknown")
@benchmarks.register(
type_="most_similar_command", description="Many commands (50 commands, no match)"
)
@benchmarks.register(type_="most_similar_command", description="Many commands (50 commands, no match)")
def benchmark_many_commands_most_similar() -> None:
app = setup_app_with_commands(50)
app._most_similar_command("unknown")
@benchmarks.register(
type_="most_similar_command", description="Many aliases (20 commands, 10 aliases each)"
)
@benchmarks.register(type_="most_similar_command", description="Many aliases (20 commands, 10 aliases each)")
def benchmark_many_aliases() -> None:
app = setup_app_with_commands(20, aliases_per_command=10)
app._most_similar_command("unknown")
@benchmarks.register(
type_="most_similar_command", description="Partial match (50 commands, prefix match)"
)
@benchmarks.register(type_="most_similar_command", description="Partial match (50 commands, prefix match)")
def benchmark_partial_match() -> None:
app = setup_app_with_commands(50)
app._most_similar_command("comm")
@benchmarks.register(
type_="most_similar_command", description="Extreme (100 commands, 20 aliases each)"
)
@benchmarks.register(type_="most_similar_command", description="Extreme (100 commands, 20 aliases each)")
def benchmark_extreme_commands() -> None:
app = setup_app_with_commands(100, aliases_per_command=20)
app._most_similar_command("comm")
+31 -31
View File
@@ -3,7 +3,7 @@ __all__ = [
"benchmark_with_many_aliases",
"benchmark_few_aliases",
"benchmark_extreme_aliases",
"benchmark_very_many_aliases",
"benchmark_very_many_aliases"
]
from argenta import App
@@ -19,16 +19,16 @@ def benchmark_no_aliases() -> None:
app = App(override_system_messages=True)
router = Router()
@router.command(Command("command1"))
def handler1(_res: Response) -> None:
@router.command(Command('command1'))
def handler1(_res: Response) -> None:
pass
@router.command(Command("command2"))
def handler2(_res: Response) -> None:
@router.command(Command('command2'))
def handler2(_res: Response) -> None:
pass
@router.command(Command("command3"))
def handler3(_res: Response) -> None:
@router.command(Command('command3'))
def handler3(_res: Response) -> None:
pass
app.include_router(router)
@@ -40,16 +40,16 @@ def benchmark_few_aliases() -> None:
app = App(override_system_messages=True)
router = Router()
@router.command(Command("command1", aliases={"c1", "cmd1"}))
def handler1(_res: Response) -> None:
@router.command(Command('command1', aliases={'c1', 'cmd1'}))
def handler1(_res: Response) -> None:
pass
@router.command(Command("command2", aliases={"c2", "cmd2"}))
def handler2(_res: Response) -> None:
@router.command(Command('command2', aliases={'c2', 'cmd2'}))
def handler2(_res: Response) -> None:
pass
@router.command(Command("command3", aliases={"c3", "cmd3"}))
def handler3(_res: Response) -> None:
@router.command(Command('command3', aliases={'c3', 'cmd3'}))
def handler3(_res: Response) -> None:
pass
app.include_router(router)
@@ -61,16 +61,16 @@ def benchmark_with_many_aliases() -> None:
app = App(override_system_messages=True)
router = Router()
@router.command(Command("command1", aliases={"c1", "cmd1", "com1", "first", "one"}))
def handler1(_res: Response) -> None:
@router.command(Command('command1', aliases={'c1', 'cmd1', 'com1', 'first', 'one'}))
def handler1(_res: Response) -> None:
pass
@router.command(Command("command2", aliases={"c2", "cmd2", "com2", "second", "two"}))
def handler2(_res: Response) -> None:
@router.command(Command('command2', aliases={'c2', 'cmd2', 'com2', 'second', 'two'}))
def handler2(_res: Response) -> None:
pass
@router.command(Command("command3", aliases={"c3", "cmd3", "com3", "third", "three"}))
def handler3(_res: Response) -> None:
@router.command(Command('command3', aliases={'c3', 'cmd3', 'com3', 'third', 'three'}))
def handler3(_res: Response) -> None:
pass
app.include_router(router)
@@ -82,16 +82,16 @@ def benchmark_very_many_aliases() -> None:
app = App(override_system_messages=True)
router = Router()
@router.command(Command("command1", aliases={f"alias1_{i}" for i in range(20)}))
def handler1(_res: Response) -> None:
@router.command(Command('command1', aliases={f'alias1_{i}' for i in range(20)}))
def handler1(_res: Response) -> None:
pass
@router.command(Command("command2", aliases={f"alias2_{i}" for i in range(20)}))
def handler2(_res: Response) -> None:
@router.command(Command('command2', aliases={f'alias2_{i}' for i in range(20)}))
def handler2(_res: Response) -> None:
pass
@router.command(Command("command3", aliases={f"alias3_{i}" for i in range(20)}))
def handler3(_res: Response) -> None:
@router.command(Command('command3', aliases={f'alias3_{i}' for i in range(20)}))
def handler3(_res: Response) -> None:
pass
app.include_router(router)
@@ -103,16 +103,16 @@ def benchmark_extreme_aliases() -> None:
app = App(override_system_messages=True)
router = Router()
@router.command(Command("command1", aliases={f"alias1_{i}" for i in range(100)}))
def handler1(_res: Response) -> None:
@router.command(Command('command1', aliases={f'alias1_{i}' for i in range(100)}))
def handler1(_res: Response) -> None:
pass
@router.command(Command("command2", aliases={f"alias2_{i}" for i in range(100)}))
def handler2(_res: Response) -> None:
@router.command(Command('command2', aliases={f'alias2_{i}' for i in range(100)}))
def handler2(_res: Response) -> None:
pass
@router.command(Command("command3", aliases={f"alias3_{i}" for i in range(100)}))
def handler3(_res: Response) -> None:
@router.command(Command('command3', aliases={f'alias3_{i}' for i in range(100)}))
def handler3(_res: Response) -> None:
pass
app.include_router(router)
@@ -3,7 +3,7 @@ __all__ = [
"benchmark_many_routers",
"benchmark_many_commands_per_router",
"benchmark_many_aliases_per_command",
"benchmark_extreme_routers",
"benchmark_extreme_routers"
]
from argenta import App
@@ -14,17 +14,14 @@ from argenta.router import Router
from .entity import benchmarks
@benchmarks.register(
type_="validate_routers_for_collisions",
description="With few routers (3 routers, 1 command each)",
)
@benchmarks.register(type_="validate_routers_for_collisions", description="With few routers (3 routers, 1 command each)")
def benchmark_few_routers() -> None:
app = App(override_system_messages=True)
for i in range(3):
router = Router()
@router.command(Command(f"cmd{i}"))
@router.command(Command(f'cmd{i}'))
def handler(_res: Response) -> None:
pass
@@ -34,17 +31,14 @@ def benchmark_few_routers() -> None:
app._validate_routers_for_collisions()
@benchmarks.register(
type_="validate_routers_for_collisions",
description="With many routers (10 routers, 1 command each)",
)
@benchmarks.register(type_="validate_routers_for_collisions", description="With many routers (10 routers, 1 command each)")
def benchmark_many_routers() -> None:
app = App(override_system_messages=True)
for i in range(10):
router = Router()
@router.command(Command(f"cmd{i}"))
@router.command(Command(f'cmd{i}'))
def handler(_res: Response) -> None:
pass
@@ -54,10 +48,7 @@ def benchmark_many_routers() -> None:
app._validate_routers_for_collisions()
@benchmarks.register(
type_="validate_routers_for_collisions",
description="With many commands per router (3 routers, 10 commands each)",
)
@benchmarks.register(type_="validate_routers_for_collisions", description="With many commands per router (3 routers, 10 commands each)")
def benchmark_many_commands_per_router() -> None:
app = App(override_system_messages=True)
@@ -65,8 +56,7 @@ def benchmark_many_commands_per_router() -> None:
router = Router()
for j in range(10):
@router.command(Command(f"cmd{i}_{j}"))
@router.command(Command(f'cmd{i}_{j}'))
def handler(_res: Response) -> None:
pass
@@ -76,10 +66,7 @@ def benchmark_many_commands_per_router() -> None:
app._validate_routers_for_collisions()
@benchmarks.register(
type_="validate_routers_for_collisions",
description="With many aliases (3 routers, 5 commands, 10 aliases each)",
)
@benchmarks.register(type_="validate_routers_for_collisions", description="With many aliases (3 routers, 5 commands, 10 aliases each)")
def benchmark_many_aliases_per_command() -> None:
app = App(override_system_messages=True)
@@ -87,10 +74,7 @@ def benchmark_many_aliases_per_command() -> None:
router = Router()
for j in range(5):
@router.command(
Command(f"cmd{i}_{j}", aliases={f"alias{i}_{j}_{k}" for k in range(10)})
)
@router.command(Command(f'cmd{i}_{j}', aliases={f'alias{i}_{j}_{k}' for k in range(10)}))
def handler(_res: Response) -> None:
pass
@@ -100,10 +84,7 @@ def benchmark_many_aliases_per_command() -> None:
app._validate_routers_for_collisions()
@benchmarks.register(
type_="validate_routers_for_collisions",
description="Extreme (20 routers, 10 commands, 20 aliases each)",
)
@benchmarks.register(type_="validate_routers_for_collisions", description="Extreme (20 routers, 10 commands, 20 aliases each)")
def benchmark_extreme_routers() -> None:
app = App(override_system_messages=True)
@@ -111,10 +92,7 @@ def benchmark_extreme_routers() -> None:
router = Router()
for j in range(10):
@router.command(
Command(f"cmd{i}_{j}", aliases={f"alias{i}_{j}_{k}" for k in range(20)})
)
@router.command(Command(f'cmd{i}_{j}', aliases={f'alias{i}_{j}_{k}' for k in range(20)}))
def handler(_res: Response) -> None:
pass
+57 -80
View File
@@ -5,18 +5,17 @@ from pathlib import Path
from rich.console import Console
from argenta.command import Flag, Flags, PossibleValues
from argenta.command import Flag, PossibleValues, Flags
from argenta.command.flag import ValidationStatus
from argenta.command.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.diagram_generator import DiagramGenerator
from .services.release_generator import ReleaseGenerator
from .services.report_table_generator import ReportTableGenerator
from .services.system_info_reader import get_system_info
from .services.diagram_generator import DiagramGenerator
from .services.release_generator import ReleaseGenerator
console = Console()
router = Router(title="Metrics commands:", disable_redirect_stdout=True)
@@ -28,30 +27,22 @@ POSITIVE_INTEGER_PATTERN = re.compile(r"^[1-9]\d*$")
Command(
"run-all",
description="Print all benchmarks results",
flags=Flags(
[
Flag("without-gc", possible_values=PossibleValues.NEITHER),
Flag("without-system-info", possible_values=PossibleValues.NEITHER),
]
),
flags=Flags([
Flag('without-gc', possible_values=PossibleValues.NEITHER),
Flag('without-system-info', possible_values=PossibleValues.NEITHER)
])
)
)
def all_print_handler(response: Response) -> None:
report_generator = ReportTableGenerator(get_system_info())
without_system_info = response.input_flags.get_flag_by_name(
"without-system-info", with_status=ValidationStatus.VALID
)
without_system_info = response.input_flags.get_flag_by_name("without-system-info", with_status=ValidationStatus.VALID)
if not without_system_info:
console.print(report_generator.generate_system_info_header())
console.print(report_generator.generate_system_info_table())
is_gc_disabled = response.input_flags.get_flag_by_name(
"without-gc", with_status=ValidationStatus.VALID
)
type_grouped_benchmarks: list[BenchmarkGroupResult] = (
registered_benchmarks.run_benchmarks_grouped_by_type(is_gc_disabled=bool(is_gc_disabled))
)
is_gc_disabled = response.input_flags.get_flag_by_name("without-gc", with_status=ValidationStatus.VALID)
type_grouped_benchmarks: list[BenchmarkGroupResult] = registered_benchmarks.run_benchmarks_grouped_by_type(is_gc_disabled=bool(is_gc_disabled))
for benchmark_group_result in type_grouped_benchmarks:
console.print(report_generator.generate_benchmark_table_header(benchmark_group_result))
@@ -61,11 +52,11 @@ def all_print_handler(response: Response) -> None:
@router.command(Command("list-types", description="List all benchmark types"))
def list_types_handler(_: Response) -> None:
types = registered_benchmarks.get_types()
if not types:
console.print("[yellow]No benchmark types found[/yellow]")
return
console.print("[bold cyan]Available benchmark types:[/bold cyan]\n")
for type_ in types:
benchmarks_count = len(registered_benchmarks.get_benchmarks_by_type(type_))
@@ -76,25 +67,23 @@ def list_types_handler(_: Response) -> None:
Command(
"run-type",
description="Run benchmarks by specific type",
flags=Flags(
[
Flag("type", possible_values=registered_benchmarks.get_types()),
Flag("without-gc", possible_values=PossibleValues.NEITHER),
Flag("without-system-info", possible_values=PossibleValues.NEITHER),
]
),
flags=Flags([
Flag('type', possible_values=registered_benchmarks.get_types()),
Flag('without-gc', possible_values=PossibleValues.NEITHER),
Flag('without-system-info', possible_values=PossibleValues.NEITHER)
])
)
)
def run_type_handler(response: Response) -> None:
type_flag = response.input_flags.get_flag_by_name("type")
if not type_flag:
console.print("[red]Error: --type flag is required[/red]")
console.print("[yellow]Usage: run-type --type <type_name>[/yellow]")
return
benchmark_type = type_flag.input_value
if not type_flag.status == ValidationStatus.VALID:
console.print(f"[red]Error: No benchmarks found for type '{benchmark_type}'[/red]")
console.print("\n[yellow]Available types:[/yellow]")
@@ -102,23 +91,17 @@ def run_type_handler(response: Response) -> None:
for t in types:
console.print(f"{t}")
return
report_generator = ReportTableGenerator(get_system_info())
without_system_info = response.input_flags.get_flag_by_name(
"without-system-info", with_status=ValidationStatus.VALID
)
without_system_info = response.input_flags.get_flag_by_name("without-system-info", with_status=ValidationStatus.VALID)
if not without_system_info:
console.print(report_generator.generate_system_info_header())
console.print(report_generator.generate_system_info_table())
is_gc_disabled = response.input_flags.get_flag_by_name(
"without-gc", with_status=ValidationStatus.VALID, default=False
)
benchmark_group_result = registered_benchmarks.run_benchmarks_by_type(
benchmark_type, is_gc_disabled=bool(is_gc_disabled)
)
is_gc_disabled = response.input_flags.get_flag_by_name("without-gc", with_status=ValidationStatus.VALID, default=False)
benchmark_group_result = registered_benchmarks.run_benchmarks_by_type(benchmark_type, is_gc_disabled=bool(is_gc_disabled))
console.print(report_generator.generate_benchmark_table_header(benchmark_group_result))
console.print(report_generator.generate_benchmark_report_table(benchmark_group_result))
@@ -126,25 +109,26 @@ def run_type_handler(response: Response) -> None:
@router.command(Command("release-generate", description="Generate release report"))
def release_generate_handler(_: Response) -> None:
lib_version = version("argenta")
console.print(f"[cyan]Generating release report for version:[/cyan] [bold]{lib_version}[/bold]")
console.print("[dim]Running benchmarks (1000 iterations, GC disabled)...[/dim]\n")
type_grouped_benchmarks: list[BenchmarkGroupResult] = (
registered_benchmarks.run_benchmarks_grouped_by_type(iterations=1000, is_gc_disabled=True)
type_grouped_benchmarks: list[BenchmarkGroupResult] = registered_benchmarks.run_benchmarks_grouped_by_type(
iterations=1000,
is_gc_disabled=True
)
release_generator = ReleaseGenerator(lib_version)
output_dir = release_generator.generate_release(type_grouped_benchmarks)
console.print("[green]✓[/green] Benchmarks completed. Generating release report...\n")
console.print(f"[green]✓[/green] Benchmarks completed. Generating release report...\n")
for benchmark_group in type_grouped_benchmarks:
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("[bold green]✓ Release report generated successfully[/bold green]")
console.print(f"[bold green]✓ Release report generated successfully[/bold green]")
console.print(f"[cyan]Output directory:[/cyan] [bold]{output_dir}[/bold]")
@@ -152,33 +136,26 @@ def release_generate_handler(_: Response) -> None:
Command(
"diagrams-generate",
description="Generate diagrams for all benchmarks",
flags=Flags(
[
Flag("without-gc", possible_values=PossibleValues.NEITHER),
Flag("iterations", possible_values=POSITIVE_INTEGER_PATTERN),
]
),
flags=Flags([
Flag('without-gc', possible_values=PossibleValues.NEITHER),
Flag('iterations', possible_values=POSITIVE_INTEGER_PATTERN)
])
)
)
def diagrams_generate_handler(response: Response) -> None:
iterations = 100
iterations_flag = response.input_flags.get_flag_by_name(
"iterations", with_status=ValidationStatus.VALID
)
iterations_flag = response.input_flags.get_flag_by_name("iterations", with_status=ValidationStatus.VALID)
if iterations_flag:
iterations = int(iterations_flag.input_value)
is_gc_disabled = bool(
response.input_flags.get_flag_by_name("without-gc", with_status=ValidationStatus.VALID)
)
is_gc_disabled = bool(response.input_flags.get_flag_by_name("without-gc", with_status=ValidationStatus.VALID))
console.print("[cyan]Running all benchmarks...[/cyan]")
console.print(f"[dim]Iterations: {iterations}, GC Disabled: {is_gc_disabled}[/dim]\n")
type_grouped_benchmarks: list[BenchmarkGroupResult] = (
registered_benchmarks.run_benchmarks_grouped_by_type(
iterations=iterations, is_gc_disabled=is_gc_disabled
)
type_grouped_benchmarks: list[BenchmarkGroupResult] = registered_benchmarks.run_benchmarks_grouped_by_type(
iterations=iterations,
is_gc_disabled=is_gc_disabled
)
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
@@ -186,17 +163,17 @@ def diagrams_generate_handler(response: Response) -> None:
output_dir.mkdir(parents=True, exist_ok=True)
diagram_generator = DiagramGenerator(output_dir)
console.print("[green]✓[/green] Benchmarks completed. Generating diagrams...\n")
console.print(f"[green]✓[/green] Benchmarks completed. Generating diagrams...\n")
generated_count = 0
for benchmark_group in type_grouped_benchmarks:
console.print(f"[cyan]Generating diagram for:[/cyan] [bold]{benchmark_group.type_}[/bold]")
comparison_path = diagram_generator.generate_comparison_diagram(benchmark_group)
generated_count += 1
console.print(f" [green]✓[/green] {comparison_path.name}\n")
console.print(f"[bold green]✓ Successfully generated {generated_count} diagrams[/bold green]")
console.print(f"[cyan]Output directory:[/cyan] [bold]{output_dir}[/bold]")
+1 -1
View File
@@ -1,6 +1,6 @@
from .diagram_generator import DiagramGenerator
from .release_generator import ReleaseGenerator
from .report_table_generator import ReportTableGenerator
from .system_info_reader import get_system_info
from .release_generator import ReleaseGenerator
__all__ = ["DiagramGenerator", "ReportTableGenerator", "get_system_info", "ReleaseGenerator"]
+75 -58
View File
@@ -2,9 +2,8 @@ __all__ = ["DiagramGenerator"]
from pathlib import Path
import cairosvg
import pygal
from pygal.style import Style
import matplotlib
import matplotlib.pyplot as plt
from ..benchmarks.core.models import BenchmarkGroupResult
@@ -13,26 +12,8 @@ class DiagramGenerator:
def __init__(self, output_dir: Path | str) -> None:
self.output_dir: Path = Path(output_dir) if isinstance(output_dir, str) else output_dir
self._style = Style(
background="white",
plot_background="white",
foreground="#2c3e50",
foreground_strong="#000000",
foreground_subtle="#7f8c8d",
opacity=".9",
opacity_hover=".95",
transition="150ms ease-in",
colors=("#2ecc71", "#3498db", "#e74c3c"),
title_font_size=40,
legend_font_size=34,
label_font_size=32, #
major_label_font_size=32,
value_font_size=28,
value_label_font_size=28,
tooltip_font_size=24,
no_data_font_size=28,
font_family="Consolas, 'Courier New', monospace",
)
matplotlib.use('Agg')
plt.style.use('seaborn-v0_8-whitegrid')
def generate_comparison_diagram(self, benchmark_group: BenchmarkGroupResult) -> Path:
results = benchmark_group.benchmark_results
@@ -46,48 +27,84 @@ class DiagramGenerator:
max_value = max(
max(avg_times) if avg_times else 0,
max(median_times) if median_times else 0,
max(std_devs) if std_devs else 0,
max(std_devs) if std_devs else 0
)
y_limit = max_value / 0.85 if max_value > 0 else 1.0
title_text = f"{benchmark_group.type_.replace('_', ' ').title()}"
metadata_text = (
f"Iterations: {benchmark_group.iterations} | GC: "
f"{'Disabled' if benchmark_group.is_gc_disabled else 'Enabled'}"
)
items_count = len(descriptions)
x_positions: list[int] = list(range(items_count))
bar_width = 0.25
x_std_dev = [x - bar_width for x in x_positions]
x_avg = [x for x in x_positions]
x_median = [x + bar_width for x in x_positions]
fig, ax = plt.subplots(figsize=(16, 8))
fig.patch.set_facecolor('white')
bars_std = ax.bar(x_std_dev, std_devs, bar_width, label='Std Deviation',
color='#2ecc71', alpha=0.9, edgecolor='#27ae60', linewidth=1.5)
bars_avg = ax.bar(x_avg, avg_times, bar_width, label='Average Time',
color='#3498db', alpha=0.9, edgecolor='#2980b9', linewidth=1.5)
bars_median = ax.bar(x_median, median_times, bar_width, label='Median Time',
color='#e74c3c', alpha=0.9, edgecolor='#c0392b', linewidth=1.5)
for bar_group in [bars_std, bars_avg, bars_median]:
for bar in bar_group:
height = bar.get_height()
ax.text(
bar.get_x() + bar.get_width() / 2.,
height,
f'{height:.3f}',
ha='center', va='bottom', fontsize=9, fontweight='bold'
)
ax.set_ylabel('Time (ms)', fontsize=14, fontweight='bold', labelpad=10)
title_text = f'{benchmark_group.type_.replace("_", " ").title()}'
metadata_text = f'Iterations: {benchmark_group.iterations} | GC: {"Disabled" if benchmark_group.is_gc_disabled else "Enabled"}'
ax.text(0.5, 1.08, title_text, transform=ax.transAxes,
fontsize=18, fontweight='bold', ha='center', color='#2c3e50')
ax.text(0.5, 1.03, metadata_text, transform=ax.transAxes,
fontsize=12, ha='center', color='#7f8c8d', style='italic')
ax.set_xticks(x_positions)
ax.set_xticklabels([])
for i, (pos, desc) in enumerate(zip(x_positions, descriptions)):
text_x_pos = pos - bar_width - (bar_width / 2)
ax.text(
text_x_pos,
y_limit * 0.02,
desc,
rotation=90, va='bottom', ha='right', fontsize=10,
color='#2c3e50'
)
ax.set_ylim(0, y_limit)
legend = ax.legend(loc='upper left', fontsize=12, framealpha=0.95,
edgecolor='#34495e', fancybox=True, shadow=True)
legend.get_frame().set_facecolor('#ecf0f1')
ax.grid(axis='y', alpha=0.4, linestyle='--', linewidth=0.8)
ax.set_axisbelow(True)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_color('#7f8c8d')
ax.spines['bottom'].set_color('#7f8c8d')
plt.tight_layout()
filename = f"{benchmark_group.type_}_comparison.png"
output_path = self.output_dir / filename
self.output_dir.mkdir(parents=True, exist_ok=True)
dynamic_height = 600 + (len(descriptions) * 150)
chart = pygal.HorizontalBar(
style=self._style,
width=3100,
height=dynamic_height,
explicit_size=True,
show_legend=True,
legend_at_bottom=True,
print_values=True,
print_values_position="top",
legend_at_bottom_columns=3,
range=(0, y_limit),
zero=0,
)
chart.title = f"{title_text}\n{metadata_text}"
chart.x_title = "Time (ms)"
chart.no_data_text = "No data"
chart.value_formatter = lambda x: f"{x:.3f}"
chart.x_labels = descriptions
chart.add("Std Deviation", std_devs)
chart.add("Average Time", avg_times)
chart.add("Median Time", median_times)
svg_bytes = chart.render()
cairosvg.svg2png(bytestring=svg_bytes, write_to=str(output_path))
plt.savefig(output_path, dpi=200, bbox_inches='tight', facecolor='white')
plt.close(fig)
return output_path
+10 -10
View File
@@ -12,20 +12,20 @@ class ReleaseGenerator:
def __init__(self, lib_version: str) -> None:
self.lib_version = lib_version
self.output_dir = Path("metrics/reports/releases") / lib_version
def generate_release(self, benchmark_groups: list[BenchmarkGroupResult]) -> Path:
if self.output_dir.exists():
shutil.rmtree(self.output_dir)
self.output_dir.mkdir(parents=True, exist_ok=True)
for benchmark_group in benchmark_groups:
type_dir = self.output_dir / benchmark_group.type_
type_dir.mkdir(exist_ok=True)
diagram_generator = DiagramGenerator(type_dir)
diagram_generator.generate_comparison_diagram(benchmark_group)
json_data = {
"type": benchmark_group.type_,
"iterations": benchmark_group.iterations,
@@ -36,14 +36,14 @@ class ReleaseGenerator:
"description": br.description,
"avg_time": br.avg_time,
"median_time": br.median_time,
"std_dev": br.std_dev,
"std_dev": br.std_dev
}
for br in benchmark_group.benchmark_results
],
]
}
json_path = type_dir / f"{benchmark_group.type_}.json"
with open(json_path, "w", encoding="utf-8") as f:
with open(json_path, 'w', encoding='utf-8') as f:
json.dump(json_data, f, indent=2, ensure_ascii=False)
return self.output_dir
+14 -22
View File
@@ -3,7 +3,7 @@ from rich.table import Table
from rich.text import Text
from ..benchmarks.core.models import BenchmarkGroupResult
from .system_info_reader import SystemInfo
from metrics.services.system_info_reader import SystemInfo
class ReportTableGenerator:
@@ -12,15 +12,11 @@ class ReportTableGenerator:
self._cached_benchmark_tables: dict[int, Table] = {}
self._cached_system_info_table: Table | None = None
def generate_benchmark_report_table(
self, benchmark_group_result: BenchmarkGroupResult
) -> Table:
def generate_benchmark_report_table(self, benchmark_group_result: BenchmarkGroupResult) -> Table:
if cached_result := self._cached_benchmark_tables.get(id(benchmark_group_result)):
return cached_result
table = Table(
show_header=True, header_style="bold cyan", border_style="blue", show_lines=True
)
table = Table(show_header=True, header_style="bold cyan", border_style="blue", show_lines=True)
table.add_column("Description", style="dim")
table.add_column("Avg Time", justify="right", style="bold yellow")
table.add_column("Median Time", justify="right", style="bold yellow")
@@ -38,22 +34,18 @@ class ReportTableGenerator:
@staticmethod
def generate_benchmark_table_header(benchmark_group_result: BenchmarkGroupResult) -> Panel:
header_text = Text(
f"TYPE: {benchmark_group_result.type_.upper()} ; "
f"ITERATIONS: {benchmark_group_result.iterations} ; "
f"GC {'DISABLED' if benchmark_group_result.is_gc_disabled else 'ENABLED'} ; "
f"ALL TIME IN MS",
style="bold magenta",
)
header_text = Text(f"TYPE: {benchmark_group_result.type_.upper()} ; "
f"ITERATIONS: {benchmark_group_result.iterations} ; "
f"GC {"DISABLED" if benchmark_group_result.is_gc_disabled else "ENABLED"} ; "
f"ALL TIME IN MS",
style="bold magenta")
return Panel(header_text, expand=False, border_style="magenta")
def generate_system_info_table(self) -> Table:
if self._cached_system_info_table is not None:
return self._cached_system_info_table
table = Table(
show_header=True, header_style="bold cyan", border_style="blue", show_lines=True
)
table = Table(show_header=True, header_style="bold cyan", border_style="blue", show_lines=True)
table.add_column("Parameter", style="green")
table.add_column("Value", style="yellow")
@@ -63,10 +55,10 @@ class ReportTableGenerator:
table.add_row("CPU", self.system_info.cpu_info.name)
table.add_row("CPU Physical Cores", str(self.system_info.cpu_info.physical_cores))
table.add_row("CPU Logical Cores", str(self.system_info.cpu_info.logical_cores))
table.add_row("CPU Max Frequency", str(self.system_info.cpu_info.max_frequency) + " GHz")
table.add_row("Total RAM", str(self.system_info.memory_info.total_ram) + " GB")
table.add_row("Used RAM", str(self.system_info.memory_info.used_ram) + " GB")
table.add_row("Available RAM", str(self.system_info.memory_info.available_ram) + " GB")
table.add_row("CPU Max Frequency", str(self.system_info.cpu_info.max_frequency) + ' GHz')
table.add_row("Total RAM", str(self.system_info.memory_info.total_ram) + ' GB')
table.add_row("Used RAM", str(self.system_info.memory_info.used_ram) + ' GB')
table.add_row("Available RAM", str(self.system_info.memory_info.available_ram) + ' GB')
table.add_row("Python Version", self.system_info.python_info.version)
table.add_row("Python Implementation", self.system_info.python_info.implementation)
table.add_row("Python Compiler", self.system_info.python_info.compiler)
@@ -77,4 +69,4 @@ class ReportTableGenerator:
@staticmethod
def generate_system_info_header() -> Panel:
header_text = Text("SYSTEM INFO", style="bold magenta")
return Panel(header_text, expand=False, border_style="magenta")
return Panel(header_text, expand=False, border_style="magenta")
+41 -36
View File
@@ -1,19 +1,28 @@
__all__ = ["SystemInfo", "get_system_info"]
__all__ = [
"SystemInfo",
"get_system_info"
]
from dataclasses import dataclass
import platform
import sys
from dataclasses import dataclass
import cpuinfo
import psutil
@dataclass(frozen=True, slots=True)
class SystemInfo:
os_info: OSInfo
cpu_info: CPUInfo
memory_info: MemoryInfo
python_info: PythonInfo
@dataclass(frozen=True, slots=True)
class OSInfo:
name: str
kernel_version: str
@dataclass(frozen=True, slots=True)
class CPUInfo:
name: str
@@ -22,13 +31,11 @@ class CPUInfo:
logical_cores: int
max_frequency: float
@dataclass(frozen=True, slots=True)
class MemoryInfo:
total_ram: float # in GB
used_ram: float # in GB
available_ram: float # in GB
total_ram: float # in GB
used_ram: float # in GB
available_ram: float # in GB
@dataclass(frozen=True, slots=True)
class PythonInfo:
@@ -37,6 +44,18 @@ class PythonInfo:
compiler: str
def get_system_info() -> SystemInfo:
os_info = get_os_info()
cpu_info = get_cpu_info()
memory_info = get_memory_info()
python_info = get_python_info()
return SystemInfo(
os_info=os_info,
cpu_info=cpu_info,
memory_info=memory_info,
python_info=python_info,
)
def get_os_info() -> OSInfo:
system = platform.system()
@@ -54,17 +73,22 @@ def get_os_info() -> OSInfo:
kernel_version=kernel_version,
)
elif system == "Darwin":
return OSInfo(kernel_version=platform.release(), name=f"macOS {platform.mac_ver()[0]}")
return OSInfo(
kernel_version=platform.release(),
name=f"macOS {platform.mac_ver()[0]}"
)
else:
return OSInfo(kernel_version=platform.release(), name=platform.system())
return OSInfo(
kernel_version=platform.release(),
name=platform.system()
)
def get_cpu_info() -> CPUInfo:
cpu_info = cpuinfo.get_cpu_info()
cpu_name = cpu_info["brand_raw"]
cpu_architecture = cpu_info["arch"]
cpu_physical_cores = psutil.cpu_count(logical=False) or 0
cpu_logical_cores = psutil.cpu_count(logical=True) or 0
cpu_physical_cores = psutil.cpu_count(logical=False)
cpu_logical_cores = psutil.cpu_count(logical=True)
cpu_freq = psutil.cpu_freq()
cpu_max_frequency = cpu_freq.max
@@ -74,10 +98,9 @@ def get_cpu_info() -> CPUInfo:
architecture=cpu_architecture,
physical_cores=cpu_physical_cores,
logical_cores=cpu_logical_cores,
max_frequency=cpu_max_frequency,
max_frequency=cpu_max_frequency
)
def get_memory_info() -> MemoryInfo:
mem = psutil.virtual_memory()
total_ram = round(mem.total / (1024**3), 2)
@@ -90,32 +113,14 @@ def get_memory_info() -> MemoryInfo:
available_ram=available_ram,
)
def get_python_info() -> PythonInfo:
python_version = platform.python_version()
python_implementation = platform.python_implementation()
python_compiler = platform.python_compiler()
return PythonInfo(
version=python_version, implementation=python_implementation, compiler=python_compiler
version=python_version,
implementation=python_implementation,
compiler=python_compiler
)
@dataclass(frozen=True, slots=True)
class SystemInfo:
os_info: OSInfo
cpu_info: CPUInfo
memory_info: MemoryInfo
python_info: PythonInfo
def get_system_info() -> SystemInfo:
os_info = get_os_info()
cpu_info = get_cpu_info()
memory_info = get_memory_info()
python_info = get_python_info()
return SystemInfo(
os_info=os_info,
cpu_info=cpu_info,
memory_info=memory_info,
python_info=python_info,
)
+18
View File
@@ -0,0 +1,18 @@
from argenta import App, Command, Response, Router
app = App(override_system_messages=True)
router = Router()
@router.command(Command('command'))
def handler(_res: Response) -> None:
pass
@router.command(Command('command_other'))
def handler2(_res: Response) -> None:
pass
app.include_routers(router)
app._pre_cycle_setup()
assert app._most_similar_command('command_') == 'command'
@@ -9,7 +9,7 @@ orchestrator: Orchestrator = Orchestrator()
def main() -> None:
app.include_router(router)
orchestrator.run_repl(app)
orchestrator.start_polling(app)
if __name__ == '__main__':
main()
@@ -3,8 +3,7 @@ from prompt_toolkit import HTML
from argenta import App, Orchestrator
from argenta.app import PredefinedMessages, StaticDividingLine, AutoCompleter
from argenta.app.dividing_line.models import DynamicDividingLine
from argenta.orchestrator import ArgParser
from examples.example_app.routers import work_router
from mock.mock_app.routers import work_router
app: App = App(
dividing_line=StaticDividingLine('~')
@@ -19,7 +18,7 @@ def main():
app.add_message_on_startup(PredefinedMessages.AUTOCOMPLETE)
app.add_message_on_startup(PredefinedMessages.HELP)
orchestrator.run_repl(app)
orchestrator.start_polling(app)
if __name__ == "__main__":
main()
+5 -42
View File
@@ -5,24 +5,7 @@ description = "Python library for building modular CLI applications"
authors = [{ name = "kolo", email = "kolo.is.main@gmail.com" }]
requires-python = ">=3.12,<3.15"
readme = "README.md"
license = "MIT"
license-files = ["LICENSE"]
keywords = ["cli", "cli-app", "command-line", "terminal", "modular", "framework"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Software Development :: User Interfaces",
"Topic :: Terminals",
"Environment :: Console",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: 3 :: Only",
"Typing :: Typed",
]
license = { text = "MIT" }
dependencies = [
"rich (>=14.0.0,<15.0.0)",
"art (>=6.4,<7.0)",
@@ -30,29 +13,12 @@ dependencies = [
"prompt-toolkit>=3.0.52",
]
[project.urls]
Homepage = "https://argenta.readthedocs.io"
Documentation = "https://argenta.readthedocs.io"
Repository = "https://github.com/koloideal/Argenta"
Issues = "https://github.com/koloideal/Argenta/issues"
Changelog = "https://github.com/koloideal/Argenta/blob/master/CHANGELOG.md"
[project.scripts]
argenta = "argenta._cli.__main__:main"
[project.optional-dependencies]
cli = [
"nuitka[onefile]>=4.0.5",
"typer>=0.9,!=0.12,<=0.21.1",
]
[dependency-groups]
dev = [
{include-group = "linters"},
{include-group = "typecheckers"},
{include-group = "docs"},
{include-group = "tests"},
{include-group = "metrics"},
"scriv>=1.8.0",
]
linters = [
@@ -80,16 +46,13 @@ metrics = [
"matplotlib>=3.10.8",
"psutil>=7.2.1",
"py-cpuinfo>=9.0.0",
"cairosvg>=2.8.2",
"pygal>=3.1.0",
]
[tool.ruff]
line-length = 100
line-length=100
[tool.pyright]
typeCheckingMode = "strict"
reportMissingTypeStubs = false
[[tool.pyright.executionEnvironments]]
root = "tests/"
@@ -126,8 +89,8 @@ md_header_level = "2"
disable_error_code = "import-untyped"
[tool.isort]
line_length = 90
line_length=90
[build-system]
requires = ["uv_build>=0.4,<0.12"]
build-backend = "uv_build"
requires = ["uv_build"]
build-backend = "uv_build"
View File
-120
View File
@@ -1,120 +0,0 @@
from importlib.metadata import version
import typer
from typer import Context, Typer
from .commands import (
build_handler,
info_handler,
init_handler,
new_handler,
routes_handler,
run_handler,
)
app = Typer(
name="argenta",
help="Argenta CLI — scaffold, run, inspect, and build CLI apps.",
no_args_is_help=True,
)
def _version_callback(value: bool) -> None:
if value:
typer.echo(f"argenta {version('argenta')}")
raise typer.Exit()
@app.callback()
def _root(
version_flag: bool = typer.Option(
None,
"--version",
"-v",
callback=_version_callback,
is_eager=True,
help="Show Argenta version and exit.",
),
) -> None:
"""Argenta CLI — scaffold, run, inspect, and build CLI apps."""
@app.command(
"run",
help="Start the orchestrator REPL from a callable entrypoint.",
short_help="Start the orchestrator REPL",
epilog="Example: argenta run app/main.py:main",
)
def _run(entrypoint_path: str = typer.Argument(help="Entrypoint as <path/to/file.py>:<callable>")) -> None:
run_handler(entrypoint_path)
@app.command(
"init",
help="Scaffold a flat or src boilerplate in the current project directory.",
short_help="Initialize architecture in existing project",
epilog="Run from the project root. Example: argenta init --arch src",
)
def _init(arch: str = typer.Option("flat", "--arch", help="Architecture: flat or src")) -> None:
init_handler(arch=arch) # type: ignore[arg-type]
@app.command(
"new",
help="Create a new project directory with a flat or src boilerplate.",
short_help="Create a new project with boilerplate",
epilog="Example: argenta new my-app --arch src",
)
def _new(
project_name: str = typer.Argument(help="Name of the new project directory"),
arch: str = typer.Option("flat", "--arch", help="Architecture: flat or src"),
) -> None:
new_handler(project_name=project_name, arch=arch) # type: ignore[arg-type]
@app.command(
"routes",
help="Display all registered routes, commands, aliases, and flags. Accepts an App instance or a callable returning App.",
short_help="Show registered routes and commands",
epilog="Examples:\n argenta routes app/main.py:app\n argenta routes app/main.py:create_app",
)
def _routes(entrypoint_path: str = typer.Argument(help="Entrypoint as <path/to/file.py>:<app_or_callable>")) -> None:
routes_handler(entrypoint_path)
@app.command(
name="info",
help="Display Argenta version, Python version, and platform info.",
short_help="Show Argenta version and environment info",
)
def _info() -> None:
info_handler()
@app.command(
name="build",
help="Compile a project entrypoint into a standalone binary using Nuitka. "
"Any Nuitka flags can be passed after a `--` separator, e.g. "
"`argenta build app/main.py:main -- --lto=yes --include-package=numpy`.",
short_help="Build a standalone binary",
epilog=(
"Examples:\n"
" argenta build app/main.py:main --output myapp\n"
" argenta build app/main.py:main -- --lto=yes --include-data-files=assets/*=assets/"
),
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
)
def _build(
ctx: Context,
entry_point: str = typer.Argument(help="Entrypoint as <path/to/file.py>:<callable>"),
output_name: str | None = typer.Option(None, "--output", "-o", help="Output binary name"),
) -> None:
build_handler(entry_point=entry_point, output_name=output_name, extra_nuitka_args=ctx.args)
def main() -> None:
app()
if __name__ == "__main__":
main()
-6
View File
@@ -1,6 +0,0 @@
from .run import run_handler as run_handler
from .init import init_handler as init_handler
from .new import new_handler as new_handler
from .routes import routes_handler as routes_handler
from .info import info_handler as info_handler
from .build import build_handler as build_handler
-88
View File
@@ -1,88 +0,0 @@
__all__ = [
"GITIGNORE_CONTENT",
"FLAT_MAIN_TEMPLATE",
"FLAT_HANDLERS_TEMPLATE",
"SRC_MAIN_TEMPLATE",
"SRC_ROUTERS_TEMPLATE",
"SRC_HANDLER_TEMPLATE",
"create_file",
]
from pathlib import Path
GITIGNORE_CONTENT = """
__pycache__/
*.py[cod]
.env
.venv/
env/
"""
FLAT_MAIN_TEMPLATE = """
from argenta import Orchestrator, App
from handlers import router
def main():
app = App()
app.include_router(router)
orchestrator = Orchestrator()
orchestrator.run_repl(app)
if __name__ == "__main__":
main()
"""
FLAT_HANDLERS_TEMPLATE = """
from argenta import Router, Response
router = Router("Hello command")
@router.command("hello")
def hello_handler(response: Response):
print("Hello world!")
"""
SRC_MAIN_TEMPLATE = """
from argenta import Orchestrator, App
from .routers import router
def main():
app = App()
app.include_router(router)
orchestrator = Orchestrator()
orchestrator.run_repl(app)
if __name__ == "__main__":
main()
"""
SRC_ROUTERS_TEMPLATE = """
from argenta import Router
from .handlers.hello_world_handler import hello_handler
router = Router()
router.command("hello")(hello_handler)
"""
SRC_HANDLER_TEMPLATE = """
from argenta import Response
def hello_handler(response: Response) -> None:
print("Hello world!")
"""
def create_file(path: Path, content: str) -> None:
if not path.exists():
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content.strip(), encoding="utf-8")
else:
print(f"Skipped: {path} (already exists)")
-68
View File
@@ -1,68 +0,0 @@
__all__ = ["build_handler"]
import os
import subprocess
import sys
from pathlib import Path
from rich.console import Console
def build_handler(
entry_point: str,
output_name: str | None = None,
extra_nuitka_args: list[str] | None = None,
) -> None:
console = Console()
file_path, _, callable_name = entry_point.partition(":")
if not file_path or not callable_name:
console.print(
f'[bold red]Error:[/bold red] "{entry_point}" must be in format "<path/to/file.py>:<callable>"'
)
raise SystemExit(1)
path = Path(file_path).resolve()
if not path.exists():
console.print(f'[bold red]Error:[/bold red] File "{file_path}" not found')
raise SystemExit(1)
is_main_module = path.name == "__main__.py"
target = str(path.parent) if is_main_module else str(path)
name = output_name or (path.parent.name if is_main_module else path.stem)
console.print(
f"[bold green]Building[/bold green] [cyan]{entry_point}[/cyan] → [cyan]{name}[/cyan]"
)
args = [
sys.executable,
"-m",
"nuitka",
"--standalone",
"--onefile",
f"--output-filename={name}",
f"--jobs={os.cpu_count() or 1}",
"--lto=no",
"--include-windows-runtime-dlls=no",
]
if is_main_module:
args.append("--python-flag=-m")
# User-provided Nuitka flags are appended last, so they can override
# Argenta's defaults (e.g. --lto=yes, --jobs=1) and add anything else
# Nuitka supports (--include-package, --include-data-files, --enable-plugin, ...).
if extra_nuitka_args:
args.extend(extra_nuitka_args)
args.append(target)
result = subprocess.run(args, check=False)
if result.returncode != 0:
console.print("[bold red]Build failed.[/bold red]")
raise SystemExit(result.returncode)
console.print(f"[bold green]Done![/bold green] Binary: [cyan]{name}[/cyan]")
-36
View File
@@ -1,36 +0,0 @@
__all__ = ["info_handler"]
import sys
import platform
from importlib.metadata import version
from art import text2art # pyright: ignore[reportUnknownVariableType]
from rich.console import Console
from rich.padding import Padding
from rich.table import Table
from rich import box
console = Console()
def info_handler() -> None:
table = Table(
box=box.SIMPLE,
show_header=False,
pad_edge=False,
show_edge=False,
expand=False,
)
table.add_column(style="bold cyan")
table.add_column(style="white", justify="right")
table.add_row("Argenta version", f'[bold red]{version("argenta")}[/bold red]')
table.add_row("Python version", sys.version.split()[0])
table.add_row("Platform", f"{platform.system()} {platform.release()} ({platform.machine()})")
table.add_row("Docs", "https://argenta.readthedocs.io")
console.print(f"[bold red]{text2art('Argenta', font='tarty1')}[/bold red]")
console.print(Padding(table, pad=(2, 5)))
console.print(Padding("[i]made with ❤ by [b]kolo[/b][/i]", pad=(0, 17)))
-39
View File
@@ -1,39 +0,0 @@
__all__ = ["init_handler"]
from pathlib import Path
from typing import Literal
from ._templates import (
FLAT_HANDLERS_TEMPLATE,
FLAT_MAIN_TEMPLATE,
GITIGNORE_CONTENT,
SRC_HANDLER_TEMPLATE,
SRC_MAIN_TEMPLATE,
SRC_ROUTERS_TEMPLATE,
create_file,
)
def init_handler(arch: Literal["flat", "src"] = "flat") -> None:
cwd = Path.cwd()
project_name = cwd.name.lower().replace(" ", "_")
create_file(cwd / ".gitignore", GITIGNORE_CONTENT)
if arch == "flat":
create_file(cwd / "main.py", FLAT_MAIN_TEMPLATE)
create_file(cwd / "handlers.py", FLAT_HANDLERS_TEMPLATE)
elif arch == "src":
base_pkg = cwd / "src" / project_name / "application"
create_file(base_pkg / "__main__.py", SRC_MAIN_TEMPLATE)
create_file(base_pkg / "routers.py", SRC_ROUTERS_TEMPLATE)
create_file(base_pkg / "handlers" / "hello_world_handler.py", SRC_HANDLER_TEMPLATE)
create_file(cwd / "src" / "__init__.py", "")
create_file(cwd / "src" / project_name / "__init__.py", "")
create_file(base_pkg / "__init__.py", "")
create_file(base_pkg / "handlers" / "__init__.py", "")
print("\nInitialization complete.")
-47
View File
@@ -1,47 +0,0 @@
__all__ = ["new_handler"]
import sys
from pathlib import Path
from typing import Literal
from ._templates import (
FLAT_HANDLERS_TEMPLATE,
FLAT_MAIN_TEMPLATE,
GITIGNORE_CONTENT,
SRC_HANDLER_TEMPLATE,
SRC_MAIN_TEMPLATE,
SRC_ROUTERS_TEMPLATE,
create_file,
)
def new_handler(project_name: str, arch: Literal["flat", "src"] = "flat") -> None:
base_dir = Path.cwd() / project_name
if base_dir.exists():
print(f"Error: Directory '{project_name}' already exists.")
raise SystemExit(1)
base_dir.mkdir(parents=True)
print(f"Initialized project directory: {base_dir}")
create_file(base_dir / ".gitignore", GITIGNORE_CONTENT)
if arch == "flat":
create_file(base_dir / "main.py", FLAT_MAIN_TEMPLATE)
create_file(base_dir / "handlers.py", FLAT_HANDLERS_TEMPLATE)
elif arch == "src":
pkg_name = project_name.lower().replace(" ", "_").replace("-", "_")
app_pkg = base_dir / "src" / pkg_name / "application"
create_file(app_pkg / "__main__.py", SRC_MAIN_TEMPLATE)
create_file(app_pkg / "routers.py", SRC_ROUTERS_TEMPLATE)
create_file(app_pkg / "handlers" / "hello_world_handler.py", SRC_HANDLER_TEMPLATE)
create_file(base_dir / "src" / "__init__.py", "")
create_file(base_dir / "src" / pkg_name / "__init__.py", "")
create_file(app_pkg / "__init__.py", "")
create_file(app_pkg / "handlers" / "__init__.py", "")
print(f"\nProject '{project_name}' created successfully! 🚀")
-110
View File
@@ -1,110 +0,0 @@
__all__ = ["routes_handler"]
from collections import defaultdict
from rich.console import Console
from rich.panel import Panel
from rich.tree import Tree
from argenta.app.models import App
from ..infrastructure.entrypoint_resolver.entity import (
CallableEntryPoint,
EntryPointAsApp,
EntrypointResolver,
)
from ..infrastructure.entrypoint_resolver.exceptions import (
EntrypointError,
EntrypointNotAppInstanceError,
ResolveFromStringError,
)
def routes_handler(entrypoint_path: str) -> None:
file_path, _, callable_name = entrypoint_path.partition(":")
if not callable_name:
Console().print(
f'[bold red]Error:[/bold red] "{entrypoint_path}" must be in format '
f'"<path/to/file.py>:<app_object>" or "<path.to.module>:<app_object>"'
)
raise SystemExit(1)
try:
app_instance = EntrypointResolver[EntryPointAsApp](file_path).parse_entrypoint_with_type(
callable_name
)
app = app_instance.instance_object
except EntrypointNotAppInstanceError:
try:
callable_entrypoint = EntrypointResolver[CallableEntryPoint](
file_path
).parse_entrypoint_with_type(
callable_name
)
except (ResolveFromStringError, EntrypointError) as e:
Console().print(f"[bold red]Error:[/bold red] {e}")
raise SystemExit(1)
app = callable_entrypoint.instance_object()
if not isinstance(app, App):
Console().print(
f"[bold red]Error:[/bold red] callable must return an App instance, got {type(app).__name__}"
)
raise SystemExit(1)
except (ResolveFromStringError, EntrypointError) as e:
Console().print(f"[bold red]Error:[/bold red] {e}")
raise SystemExit(1)
routers = app.registered_routers
console = Console()
stats: dict[str, int] = defaultdict(int)
tree = Tree(f"📦 [bold blue]App object:[/bold blue] {app!r}")
for router in routers:
stats["routers"] += 1
router_node = tree.add(f"📁 [bold green]Router:[/bold green] {router.title}")
for command in router.command_handlers:
stats["commands"] += 1
trigger = command.handled_command.trigger
description = command.handled_command.description
aliases = list(command.handled_command.aliases)
flags = list(command.handled_command.registered_flags)
cmd_node = router_node.add(f"⚡ [bold cyan]{trigger}[/bold cyan]")
if description:
cmd_node.add(f"📝 [dim]description:[/dim] {description}")
if aliases:
aliases_str = ", ".join(f"[yellow]{a}[/yellow]" for a in aliases)
cmd_node.add(f"🔀 [dim]aliases:[/dim] {aliases_str}")
stats["aliases"] += len(aliases)
if flags:
flags_node = cmd_node.add(f"🚩 [dim]flags:[/dim] ({len(flags)})")
for flag in flags:
possible = flag.possible_values
flags_node.add(
f"[magenta]{flag.prefix}{flag.name}[/magenta]"
f" [dim]possible_values:[/dim] [italic]{possible!r}[/italic]"
)
stats["flags"] += len(flags)
stats_text = (
f"📁 [bold]Total Routers:[/bold] {stats['routers']}\n"
f"⚡ [bold]Total Commands:[/bold] {stats['commands']}\n"
f"🔀 [bold]Total Aliases:[/bold] {stats['aliases']}\n"
f"🚩 [bold]Total Flags:[/bold] {stats['flags']}"
)
console.print(
Panel(
stats_text,
title="[bold blue]App Stats[/bold blue]",
expand=False,
border_style="blue",
)
)
console.print()
console.print(tree)
-34
View File
@@ -1,34 +0,0 @@
__all__ = ["run_handler"]
import os
from rich.console import Console
from ..infrastructure.entrypoint_resolver.entity import (
CallableEntryPoint,
EntrypointResolver,
)
from ..infrastructure.entrypoint_resolver.exceptions import (
EntrypointError,
ResolveFromStringError,
)
def run_handler(entrypoint_path: str) -> None:
os.environ["RUN_FROM_ARGENTA_RUNNER"] = "1"
file_path, _, callable_name = entrypoint_path.partition(":")
if not callable_name:
Console().print(
f'[bold red]Error:[/bold red] "{entrypoint_path}" must be in format '
f'"<path/to/file.py>:<callable>" or "<path.to.module>:<callable>"'
)
raise SystemExit(1)
try:
runner = EntrypointResolver[CallableEntryPoint](file_path).parse_entrypoint_with_type(
callable_name
)
runner.instance_object()
except (ResolveFromStringError, EntrypointError) as e:
Console().print(f"[bold red]Error:[/bold red] {e}")
raise SystemExit(1)
@@ -1,7 +0,0 @@
from .entity import CallableEntryPoint as CallableEntryPoint
from .entity import EntryPointAsApp as EntryPointAsApp
from .entity import EntrypointResolver as EntrypointResolver
from .exceptions import EntrypointNotCallableError as EntrypointNotCallableError
from .exceptions import ResolveFromStringError as ResolveFromStringError
from .exceptions import CallableEntrypointNotMatchRequiredSignatureError as CallableEntrypointNotMatchRequiredSignatureError
from .exceptions import EntrypointNotAppInstanceError as EntrypointNotAppInstanceError
@@ -1,136 +0,0 @@
__all__ = ["EntrypointResolver", "EntryPointAsApp", "CallableEntryPoint"]
import importlib
import inspect
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Callable, Protocol, cast, get_args
from argenta.app.models import App
from .exceptions import (
CallableEntrypointNotMatchRequiredSignatureError,
EntrypointNotAppInstanceError,
EntrypointNotCallableError,
ResolveFromStringError,
)
class EntryPoint[T](Protocol):
@property
def raw_path(self) -> str: ...
@property
def instance_object(self) -> T: ...
@dataclass(frozen=True, slots=True)
class CallableEntryPoint:
raw_path: str
instance_object: Callable[[], None]
@dataclass(frozen=True, slots=True)
class EntryPointAsApp:
raw_path: str
instance_object: App
@dataclass(frozen=True, slots=True)
class ResolvedEntrypoint:
resolved_source_path: str
instance: Callable[[], None] | App
class EntrypointResolver[T: (CallableEntryPoint, EntryPointAsApp)]:
def __init__(self, path_to_entrypoint: str):
self._path_to_entrypoint = path_to_entrypoint
def parse_entrypoint_with_type(
self,
entrypoint_object_name: str,
) -> T:
entrypoint_type: type[T] = get_args(self.__orig_class__)[0] # pyright: ignore[reportAttributeAccessIssue, reportUnknownMemberType]
if entrypoint_type is CallableEntryPoint:
return cast(T, self._parse_callable_entrypoint(entrypoint_object_name))
elif entrypoint_type is EntryPointAsApp:
return cast(T, self._parse_entrypoint_as_app(entrypoint_object_name))
raise NotImplementedError
def _parse_callable_entrypoint(self, entrypoint_object_name: str) -> CallableEntryPoint:
resolved_entrypoint = self._resolve_from_string(entrypoint_object_name)
instance_object = resolved_entrypoint.instance
if not callable(instance_object):
raise EntrypointNotCallableError(repr(instance_object))
instance_object_signature = inspect.signature(instance_object)
required_params = instance_object_signature.parameters
if required_params:
raise CallableEntrypointNotMatchRequiredSignatureError(repr(instance_object))
return CallableEntryPoint(raw_path=resolved_entrypoint.resolved_source_path, instance_object=instance_object)
def _parse_entrypoint_as_app(self, entrypoint_object_name: str) -> EntryPointAsApp:
resolved_entrypoint = self._resolve_from_string(entrypoint_object_name)
instance_object = resolved_entrypoint.instance
if not isinstance(instance_object, App):
raise EntrypointNotAppInstanceError(repr(instance_object))
return EntryPointAsApp(raw_path=resolved_entrypoint.resolved_source_path, instance_object=instance_object)
def _resolve_from_string(self, entrypoint_object_name: str) -> ResolvedEntrypoint:
raw_path = self._path_to_entrypoint
raw_path_as_dir = Path(raw_path).resolve()
if raw_path_as_dir.is_dir() and (raw_path_as_dir / "__main__.py").exists():
raw_path = str(raw_path_as_dir / "__main__.py")
is_file_path = bool(re.search(r"[\/\\]|\.py$", raw_path))
if is_file_path:
abs_path = Path(raw_path).resolve()
if not abs_path.exists():
raise ResolveFromStringError(f'File "{raw_path}" not found')
package_root = abs_path.parent
while (package_root / "__init__.py").exists():
package_root = package_root.parent
pkg_root_str = str(package_root)
if pkg_root_str not in sys.path:
sys.path.insert(0, pkg_root_str)
module_name = ".".join(abs_path.relative_to(package_root).with_suffix("").parts)
resolved_source_path = str(abs_path)
else:
module_name = raw_path
cwd_str = str(Path.cwd())
if cwd_str not in sys.path:
sys.path.insert(0, cwd_str)
resolved_source_path = module_name
try:
module = importlib.import_module(module_name)
except ImportError as e:
if not is_file_path and not module_name.endswith(".__main__"):
try:
main_module_name = f"{module_name}.__main__"
module = importlib.import_module(main_module_name)
module_name = main_module_name
except ImportError:
raise ResolveFromStringError(f'Cannot import module "{module_name}": {e}')
else:
raise ResolveFromStringError(f'Cannot import module "{module_name}": {e}')
if not is_file_path:
resolved_source_path = getattr(module, "__file__", resolved_source_path)
try:
instance = getattr(module, entrypoint_object_name)
except AttributeError:
raise ResolveFromStringError(f'"{entrypoint_object_name}" not found in "{raw_path}"')
return ResolvedEntrypoint(resolved_source_path, instance)
@@ -1,26 +0,0 @@
class ResolverError(Exception):
pass
class ResolveFromStringError(ResolverError):
pass
class EntrypointError(Exception):
def __init__(self, entrypoint_as_repr: str) -> None:
self.entrypoint_as_repr = entrypoint_as_repr
class EntrypointNotCallableError(EntrypointError):
def __str__(self):
return f"Entrypoint {self.entrypoint_as_repr} is not callable"
class CallableEntrypointNotMatchRequiredSignatureError(EntrypointError):
def __str__(self) -> str:
return f"Callable entrypoint {self.entrypoint_as_repr} not match with required signature Callable[[], ...]"
class EntrypointNotAppInstanceError(EntrypointError):
def __str__(self):
return f"Entrypoint {self.entrypoint_as_repr} is not instance of App"
@@ -1,122 +0,0 @@
__all__ = ['build_session', 'do_prompt']
from typing import Callable, Iterable
from prompt_toolkit import HTML, PromptSession
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
from prompt_toolkit.completion import CompleteEvent, Completer, Completion, ThreadedCompleter
from prompt_toolkit.cursor_shapes import CursorShape
from prompt_toolkit.document import Document
from prompt_toolkit.formatted_text import StyleAndTextTuples
from prompt_toolkit.history import FileHistory, History, InMemoryHistory, ThreadedHistory
from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent
from prompt_toolkit.lexers import Lexer
from prompt_toolkit.styles import Style
class CommandLexer(Lexer):
def __init__(self, valid_commands: set[str]) -> None:
self.valid_commands: set[str] = valid_commands
def lex_document(self, document: Document) -> Callable[[int], StyleAndTextTuples]:
def get_line_tokens(lineno: int) -> StyleAndTextTuples:
if lineno >= len(document.lines):
return []
line_text: str = document.lines[lineno]
if not line_text.strip():
return [("", line_text)]
first_word: str = line_text.split()[0] if line_text.split() else ""
if first_word in self.valid_commands:
return [("class:valid", line_text)]
else:
return [("class:invalid", line_text)]
return get_line_tokens
class HistoryCompleter(Completer):
def __init__(self, history_container: History, static_commands: set[str]) -> None:
self.history_container: History = history_container
self.static_commands: set[str] = static_commands
def get_completions(
self, document: Document, complete_event: CompleteEvent
) -> Iterable[Completion]:
text: str = document.text_before_cursor
history_items: set[str] = set(self.history_container.load_history_strings())
all_candidates: set[str] = history_items.union(self.static_commands)
matches: list[str] = sorted(cmd for cmd in all_candidates if cmd.startswith(text))
if not matches:
return
for match in matches:
yield Completion(match, start_position=-len(text), display=match)
@staticmethod
def _find_common_prefix(matches: list[str]) -> str:
if not matches:
return ""
common: str = matches[0]
for match in matches[1:]:
i: int = 0
while i < len(common) and i < len(match) and common[i] == match[i]:
i += 1
common = common[:i]
return common
def build_session(
history_filename: str | None,
autocomplete_button: str,
command_highlighting: bool,
auto_suggestions: bool,
all_commands: set[str],
) -> PromptSession[str]:
kb = KeyBindings()
def _(event: KeyPressEvent) -> None:
buff = event.app.current_buffer
if buff.complete_state:
buff.complete_next()
return
comps_gen = iter(buff.completer.get_completions(buff.document, CompleteEvent()))
try:
first = next(comps_gen)
except StopIteration:
return
try:
_ = next(comps_gen)
buff.start_completion(select_first=False)
except StopIteration:
buff.apply_completion(first)
kb.add(autocomplete_button)(_)
history: InMemoryHistory | ThreadedHistory
if history_filename:
history = ThreadedHistory(FileHistory(history_filename))
else:
history = InMemoryHistory()
style = Style.from_dict({"valid": "#00ff00", "invalid": "#ff0000"})
return PromptSession(
history=history,
completer=ThreadedCompleter(HistoryCompleter(history, all_commands)),
complete_while_typing=False,
key_bindings=kb,
auto_suggest=AutoSuggestFromHistory() if auto_suggestions else None,
style=style if command_highlighting else None,
lexer=CommandLexer(all_commands) if command_highlighting else None,
)
def do_prompt(session: PromptSession[str], prompt_text: str | HTML) -> str:
return session.prompt(
HTML(prompt_text) if isinstance(prompt_text, str) else prompt_text,
cursor=CursorShape.BLINKING_BEAM,
)
+114 -22
View File
@@ -1,21 +1,86 @@
from __future__ import annotations
__all__ = ["AutoCompleter"]
import sys
from typing import TYPE_CHECKING
from typing import Callable, Iterable
if TYPE_CHECKING:
from prompt_toolkit import PromptSession, HTML
from prompt_toolkit import HTML, PromptSession
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
from prompt_toolkit.completion import (CompleteEvent, Completer, Completion,
ThreadedCompleter)
from prompt_toolkit.cursor_shapes import CursorShape
from prompt_toolkit.document import Document
from prompt_toolkit.formatted_text import StyleAndTextTuples
from prompt_toolkit.history import FileHistory, History, InMemoryHistory, ThreadedHistory
from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent
from prompt_toolkit.lexers import Lexer
from prompt_toolkit.styles import Style
class CommandLexer(Lexer):
def __init__(self, valid_commands: set[str]) -> None:
self.valid_commands: set[str] = valid_commands
def lex_document(self, document: Document) -> Callable[[int], StyleAndTextTuples]:
def get_line_tokens(lineno: int) -> StyleAndTextTuples:
if lineno >= len(document.lines):
return []
line_text: str = document.lines[lineno]
if not line_text.strip():
return [("", line_text)]
first_word: str = line_text.split()[0] if line_text.split() else ""
if first_word in self.valid_commands:
return [("class:valid", line_text)]
else:
return [("class:invalid", line_text)]
return get_line_tokens
class HistoryCompleter(Completer):
def __init__(self, history_container: History, static_commands: set[str]) -> None:
self.history_container: History = history_container
self.static_commands: set[str] = static_commands
def get_completions(self, document: Document, complete_event: CompleteEvent) -> Iterable[Completion]:
text: str = document.text_before_cursor
history_items: set[str] = set(self.history_container.load_history_strings())
all_candidates: set[str] = history_items.union(self.static_commands)
matches: list[str] = sorted(cmd for cmd in all_candidates if cmd.startswith(text))
if not matches:
return
for match in matches:
yield Completion(
match,
start_position=-len(text),
display=match
)
@staticmethod
def _find_common_prefix(matches: list[str]) -> str:
if not matches:
return ""
common: str = matches[0]
for match in matches[1:]:
i: int = 0
while i < len(common) and i < len(match) and common[i] == match[i]:
i += 1
common = common[:i]
return common
class AutoCompleter:
def __init__(
self,
history_filename: str | None = None,
autocomplete_button: str = "tab",
command_highlighting: bool = True,
auto_suggestions: bool = True,
self,
history_filename: str | None = None,
autocomplete_button: str = "tab",
command_highlighting: bool = True,
auto_suggestions: bool = True,
) -> None:
self.history_filename: str | None = history_filename
self.autocomplete_button: str = autocomplete_button
@@ -29,15 +94,42 @@ class AutoCompleter:
self._session = None
self._fallback_mode = True
return
from ._ext_features_impl import build_session
self._session = build_session(
self.history_filename,
self.autocomplete_button,
self.command_highlighting,
self.auto_suggestions,
all_commands
kb = KeyBindings()
def _(event: KeyPressEvent) -> None:
buff = event.app.current_buffer
if buff.complete_state:
buff.complete_next()
return
comps_gen = iter(buff.completer.get_completions(buff.document, CompleteEvent()))
try:
first = next(comps_gen)
except StopIteration:
return
try:
_ = next(comps_gen)
buff.start_completion(select_first=False)
except StopIteration:
buff.apply_completion(first)
kb.add(self.autocomplete_button)(_)
history: InMemoryHistory | ThreadedHistory
if self.history_filename:
history = ThreadedHistory(FileHistory(self.history_filename))
else:
history = InMemoryHistory()
style = Style.from_dict({'valid': '#00ff00', 'invalid': '#ff0000'})
self._session = PromptSession(
history=history,
completer=ThreadedCompleter(HistoryCompleter(history, all_commands)),
complete_while_typing=False,
key_bindings=kb,
auto_suggest=AutoSuggestFromHistory() if self.auto_suggestions else None,
style=style if self.command_highlighting else None,
lexer=CommandLexer(all_commands) if self.command_highlighting else None,
)
def prompt(self, prompt_text: str | HTML = ">>> ") -> str:
@@ -45,7 +137,7 @@ class AutoCompleter:
return input(prompt_text if isinstance(prompt_text, str) else ">>> ")
if self._session is None:
raise RuntimeError("Call initial_setup() before using prompt()")
from ._ext_features_impl import do_prompt
return do_prompt(self._session, prompt_text)
return self._session.prompt(
HTML(prompt_text) if isinstance(prompt_text, str) else prompt_text,
cursor=CursorShape.BLINKING_BEAM
)
+5 -4
View File
@@ -3,6 +3,8 @@ __all__ = ["App"]
import difflib
from typing import Never, TypeAlias
from rich.console import Console
from argenta.app.autocompleter import AutoCompleter
from argenta.app.behavior_handlers.models import (BehaviorHandlersFabric,
BehaviorHandlersSettersMixin)
@@ -142,7 +144,7 @@ class BaseApp(BehaviorHandlersSettersMixin):
is_stdout_redirected_by_router=processing_router.is_redirect_stdout_disabled
)
def _run_repl(self) -> None:
def _run_polling(self) -> None:
self._viewer.view_initial_message(self._initial_message)
self._pre_cycle_setup()
while True:
@@ -187,7 +189,7 @@ class App(BaseApp):
repeat_command_groups_printing: bool = False,
override_system_messages: bool = False,
autocompleter: AutoCompleter | None = None,
printer: Printer | None = None,
printer: Printer = Console().print,
) -> None:
"""
Public. The essence of the application itself.
@@ -204,7 +206,6 @@ class App(BaseApp):
:param printer: system messages text output function
:return: None
"""
from rich.console import Console
super().__init__(
prompt=prompt,
initial_message=initial_message,
@@ -215,7 +216,7 @@ class App(BaseApp):
repeat_command_groups_printing=repeat_command_groups_printing,
override_system_messages=override_system_messages,
autocompleter=autocompleter or AutoCompleter(),
printer=printer or Console().print,
printer=printer,
)
def include_router(self, router: Router) -> None:
+5 -2
View File
@@ -7,8 +7,9 @@ __all__ = [
"HandlerFunc",
]
from typing import Any, Protocol, TypeVar, Callable
from typing import Any, Protocol, TypeVar
from argenta.response import Response
T = TypeVar("T", contravariant=True)
@@ -38,4 +39,6 @@ class DescriptionMessageGenerator(Protocol):
raise NotImplementedError
type HandlerFunc = Callable[..., Any]
class HandlerFunc(Protocol):
def __call__(self, response: Response, /, *args: Any, **kwargs: Any) -> None:
raise NotImplementedError
+6 -8
View File
@@ -1,7 +1,5 @@
__all__ = ["Orchestrator"]
import os
from dishka import Provider, make_container
from argenta.app import App
@@ -9,12 +7,13 @@ from argenta.di.integration import setup_dishka
from argenta.di.providers import SystemProvider
from argenta.orchestrator.argparser import ArgParser
DEFAULT_ARGPARSER: ArgParser = ArgParser(processed_args=[])
class Orchestrator:
def __init__(
self,
arg_parser: ArgParser | None = None,
arg_parser: ArgParser = DEFAULT_ARGPARSER,
custom_providers: list[Provider] | None = None,
auto_inject_handlers: bool = True,
):
@@ -23,14 +22,13 @@ class Orchestrator:
:param arg_parser: Cmd argument parser and configurator at startup
:return: None
"""
self._arg_parser: ArgParser | None = arg_parser if not os.getenv('RUN_FROM_ARGENTA_RUNNER') else None
self._arg_parser: ArgParser = arg_parser
self._custom_providers: list[Provider] = custom_providers or []
self._auto_inject_handlers: bool = auto_inject_handlers
if self._arg_parser is not None:
self._arg_parser._parse_args() # pyright: ignore[reportPrivateUsage]
self._arg_parser._parse_args() # pyright: ignore[reportPrivateUsage]
def run_repl(self, app: App) -> None:
def start_polling(self, app: App) -> None:
"""
Public. Starting the user input processing cycle
:param app: a running application
@@ -41,4 +39,4 @@ class Orchestrator:
)
setup_dishka(app, container, auto_inject=self._auto_inject_handlers)
app._run_repl() # pyright: ignore[reportPrivateUsage]
app._run_polling()
+1 -6
View File
@@ -1,11 +1,6 @@
from __future__ import annotations
__all__ = ["Response"]
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from dishka import Container
from dishka import Container
from argenta.command import InputFlags
from argenta.response.status import ResponseStatus
+3 -2
View File
@@ -3,6 +3,8 @@ __all__ = ["Router"]
from inspect import get_annotations, getfullargspec, getsourcefile, getsourcelines
from typing import Callable
from rich.console import Console
from argenta.app.protocols import HandlerFunc
from argenta.command import Command, InputCommand, InputFlags
from argenta.command.flag import ValidationStatus
@@ -18,7 +20,7 @@ from argenta.router.exceptions import (RepeatedAliasNameException,
class Router:
def __init__(
self,
title: str = "Title",
title: str = "Default title",
*,
disable_redirect_stdout: bool = False,
):
@@ -173,7 +175,6 @@ class Router:
response_arg_annotation = func_annotations.get(response_arg)
if response_arg_annotation is not None and response_arg_annotation is not Response:
from rich.console import Console
source_line: int = getsourcelines(func)[1]
Console().print(
f'\nFile "{getsourcefile(func)}", line {source_line}\n[b red]WARNING:[/b red] [i]The typehint '
@@ -38,7 +38,7 @@ def test_empty_input_triggers_empty_command_handler(monkeypatch: pytest.MonkeyPa
app = App(override_system_messages=True, printer=print)
app.include_router(router)
app.set_empty_command_handler(lambda: print('Empty input command'))
orchestrator.run_repl(app)
orchestrator.start_polling(app)
output = capsys.readouterr().out
@@ -64,7 +64,7 @@ def test_unknown_command_triggers_unknown_command_handler(monkeypatch: pytest.Mo
app = App(override_system_messages=True, printer=print)
app.include_router(router)
app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.trigger}'))
orchestrator.run_repl(app)
orchestrator.start_polling(app)
output = capsys.readouterr().out
@@ -85,7 +85,7 @@ def test_mixed_valid_and_unknown_commands_handled_correctly(monkeypatch: pytest.
app = App(override_system_messages=True, printer=print)
app.include_router(router)
app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.trigger}'))
orchestrator.run_repl(app)
orchestrator.start_polling(app)
output = capsys.readouterr().out
@@ -110,7 +110,7 @@ def test_multiple_commands_with_unknown_command_in_between(monkeypatch: pytest.M
app = App(override_system_messages=True, printer=print)
app.include_router(router)
app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.trigger}'))
orchestrator.run_repl(app)
orchestrator.start_polling(app)
output = capsys.readouterr().out
@@ -137,7 +137,7 @@ def test_unregistered_flag_without_value_is_accessible(monkeypatch: pytest.Monke
app = App(override_system_messages=True, printer=print)
app.include_router(router)
orchestrator.run_repl(app)
orchestrator.start_polling(app)
output = capsys.readouterr().out
@@ -161,7 +161,7 @@ def test_unregistered_flag_with_value_is_accessible(monkeypatch: pytest.MonkeyPa
app = App(override_system_messages=True, printer=print)
app.include_router(router)
orchestrator.run_repl(app)
orchestrator.start_polling(app)
output = capsys.readouterr().out
@@ -184,7 +184,7 @@ def test_registered_and_unregistered_flags_coexist(monkeypatch: pytest.MonkeyPat
app = App(override_system_messages=True, printer=print)
app.include_router(router)
orchestrator.run_repl(app)
orchestrator.start_polling(app)
output = capsys.readouterr().out
@@ -210,7 +210,7 @@ def test_flag_without_value_triggers_incorrect_syntax_handler(monkeypatch: pytes
app = App(override_system_messages=True, printer=print)
app.include_router(router)
app.set_incorrect_input_syntax_handler(lambda command: print(f'Incorrect flag syntax: "{command}"'))
orchestrator.run_repl(app)
orchestrator.start_polling(app)
output = capsys.readouterr().out
@@ -236,7 +236,7 @@ def test_repeated_flags_trigger_repeated_flags_handler(monkeypatch: pytest.Monke
app = App(override_system_messages=True, printer=print)
app.include_router(router)
app.set_repeated_input_flags_handler(lambda command: print(f'Repeated input flags: "{command}"'))
orchestrator.run_repl(app)
orchestrator.start_polling(app)
output = capsys.readouterr().out
@@ -38,7 +38,7 @@ def test_simple_command_executes_successfully(monkeypatch: pytest.MonkeyPatch, c
app = App(override_system_messages=True, printer=print)
app.include_router(router)
orchestrator.run_repl(app)
orchestrator.start_polling(app)
output = capsys.readouterr().out
@@ -62,7 +62,7 @@ def test_two_commands_execute_sequentially(monkeypatch: pytest.MonkeyPatch, caps
app = App(override_system_messages=True, printer=print)
app.include_router(router)
orchestrator.run_repl(app)
orchestrator.start_polling(app)
output = capsys.readouterr().out
@@ -90,7 +90,7 @@ def test_three_commands_execute_sequentially(monkeypatch: pytest.MonkeyPatch, ca
app = App(override_system_messages=True, printer=print)
app.include_router(router)
orchestrator.run_repl(app)
orchestrator.start_polling(app)
output = capsys.readouterr().out
@@ -118,7 +118,7 @@ def test_custom_flag_without_value_is_recognized(monkeypatch: pytest.MonkeyPatch
app = App(override_system_messages=True, printer=print)
app.include_router(router)
orchestrator.run_repl(app)
orchestrator.start_polling(app)
output = capsys.readouterr().out
@@ -141,7 +141,7 @@ def test_custom_flag_with_regex_validation_accepts_valid_value(monkeypatch: pyte
app = App(override_system_messages=True, repeat_command_groups_printing=True, printer=print)
app.include_router(router)
orchestrator.run_repl(app)
orchestrator.start_polling(app)
output = capsys.readouterr().out
@@ -169,7 +169,7 @@ def test_predefined_short_help_flag_is_recognized(monkeypatch: pytest.MonkeyPatc
app = App(override_system_messages=True, printer=print)
app.include_router(router)
orchestrator.run_repl(app)
orchestrator.start_polling(app)
output = capsys.readouterr().out
@@ -192,7 +192,7 @@ def test_predefined_info_flag_is_recognized(monkeypatch: pytest.MonkeyPatch, cap
app = App(override_system_messages=True, printer=print)
app.include_router(router)
orchestrator.run_repl(app)
orchestrator.start_polling(app)
output = capsys.readouterr().out
@@ -215,7 +215,7 @@ def test_predefined_host_flag_with_value_is_recognized(monkeypatch: pytest.Monke
app = App(override_system_messages=True, printer=print)
app.include_router(router)
orchestrator.run_repl(app)
orchestrator.start_polling(app)
output = capsys.readouterr().out
@@ -244,7 +244,7 @@ def test_two_predefined_flags_are_recognized_together(monkeypatch: pytest.Monkey
app = App(override_system_messages=True, printer=print)
app.include_router(router)
orchestrator.run_repl(app)
orchestrator.start_polling(app)
output = capsys.readouterr().out
+159 -119
View File
@@ -10,11 +10,14 @@ from prompt_toolkit.completion import CompleteEvent
from prompt_toolkit.document import Document
from prompt_toolkit.history import InMemoryHistory
from argenta.app.autocompleter._ext_features_impl import CommandLexer, HistoryCompleter
from argenta.app.autocompleter.entity import AutoCompleter
from argenta.app.autocompleter.entity import (
AutoCompleter,
CommandLexer,
HistoryCompleter
)
COMMANDS: set[str] = {"start", "stop", "status"}
_IMPL = "argenta.app.autocompleter._ext_features_impl"
def test_autocompleter_initializes_with_default_params() -> None:
@@ -30,7 +33,7 @@ def test_autocompleter_initializes_with_custom_params() -> None:
history_filename="test.txt",
autocomplete_button="c-space",
command_highlighting=False,
auto_suggestions=False,
auto_suggestions=False
)
assert completer.history_filename == "test.txt"
assert completer.autocomplete_button == "c-space"
@@ -76,13 +79,13 @@ def test_history_completer_returns_matching_commands() -> None:
history = InMemoryHistory()
history.append_string("start server")
history.append_string("stop server")
completer = HistoryCompleter(history, {"status"})
doc = Document("sta")
completions = list(completer.get_completions(doc, CompleteEvent()))
completion_texts = [c.text for c in completions]
assert "start server" in completion_texts
assert "status" in completion_texts
assert "stop server" not in completion_texts
@@ -92,13 +95,13 @@ def test_history_completer_returns_all_when_empty_input() -> None:
history = InMemoryHistory()
history.append_string("start")
history.append_string("stop")
completer = HistoryCompleter(history, {"status"})
doc = Document("")
completions = list(completer.get_completions(doc, CompleteEvent()))
completion_texts = [c.text for c in completions]
assert len(completion_texts) == 3
assert "start" in completion_texts
assert "stop" in completion_texts
@@ -108,10 +111,10 @@ def test_history_completer_returns_all_when_empty_input() -> None:
def test_history_completer_returns_empty_when_no_matches() -> None:
history = InMemoryHistory()
history.append_string("start")
completer = HistoryCompleter(history, {"stop"})
doc = Document("xyz")
completions = list(completer.get_completions(doc, CompleteEvent()))
assert len(completions) == 0
@@ -120,10 +123,10 @@ def test_history_completer_deduplicates_commands() -> None:
history = InMemoryHistory()
history.append_string("start")
history.append_string("start")
completer = HistoryCompleter(history, {"start"})
doc = Document("sta")
completions = list(completer.get_completions(doc, CompleteEvent()))
assert len(completions) == 1
@@ -133,13 +136,13 @@ def test_history_completer_sorts_results() -> None:
history.append_string("stop")
history.append_string("start")
history.append_string("status")
completer = HistoryCompleter(history, set())
doc = Document("st")
completions = list(completer.get_completions(doc, CompleteEvent()))
completion_texts = [c.text for c in completions]
assert completion_texts == ["start", "status", "stop"]
@@ -179,7 +182,7 @@ def test_history_completer_returns_early_when_no_matches() -> None:
history = InMemoryHistory()
completer = HistoryCompleter(history, {"start", "stop"})
doc = Document("xyz")
result = completer.get_completions(doc, CompleteEvent())
completions = list(result)
assert completions == []
@@ -187,32 +190,28 @@ def test_history_completer_returns_early_when_no_matches() -> None:
def test_autocompleter_initial_setup_with_commands() -> None:
completer = AutoCompleter()
with (
patch.object(sys.stdin, "isatty", return_value=True),
patch(f"{_IMPL}.PromptSession") as mock_session,
):
with patch.object(sys.stdin, 'isatty', return_value=True), \
patch('argenta.app.autocompleter.entity.PromptSession') as mock_session:
completer.initial_setup({"start", "stop", "status"})
assert completer._session is not None
assert completer._fallback_mode is False
mock_session.assert_called_once()
def test_autocompleter_initial_setup_with_history_file() -> None:
with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".txt") as f:
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt') as f:
history_file = f.name
try:
completer = AutoCompleter(history_filename=history_file)
with (
patch.object(sys.stdin, "isatty", return_value=True),
patch(f"{_IMPL}.PromptSession"),
patch(f"{_IMPL}.ThreadedHistory") as mock_threaded_history,
):
with patch.object(sys.stdin, 'isatty', return_value=True), \
patch('argenta.app.autocompleter.entity.PromptSession'), \
patch('argenta.app.autocompleter.entity.ThreadedHistory') as mock_threaded_history:
completer.initial_setup({"start", "stop"})
assert completer._session is not None
assert completer._fallback_mode is False
mock_threaded_history.assert_called_once()
@@ -223,14 +222,12 @@ def test_autocompleter_initial_setup_with_history_file() -> None:
def test_autocompleter_initial_setup_without_history_file() -> None:
completer = AutoCompleter(history_filename=None)
with (
patch.object(sys.stdin, "isatty", return_value=True),
patch(f"{_IMPL}.PromptSession"),
patch(f"{_IMPL}.InMemoryHistory") as mock_in_memory,
):
with patch.object(sys.stdin, 'isatty', return_value=True), \
patch('argenta.app.autocompleter.entity.PromptSession'), \
patch('argenta.app.autocompleter.entity.InMemoryHistory') as mock_in_memory:
completer.initial_setup({"start", "stop"})
assert completer._session is not None
assert completer._fallback_mode is False
mock_in_memory.assert_called_once()
@@ -238,90 +235,96 @@ def test_autocompleter_initial_setup_without_history_file() -> None:
def test_autocompleter_initial_setup_with_custom_autocomplete_button() -> None:
completer = AutoCompleter(autocomplete_button="c-space")
with (
patch.object(sys.stdin, "isatty", return_value=True),
patch(f"{_IMPL}.PromptSession"),
):
with patch.object(sys.stdin, 'isatty', return_value=True), \
patch('argenta.app.autocompleter.entity.PromptSession'):
completer.initial_setup({"start", "stop"})
assert completer._session is not None
assert completer.autocomplete_button == "c-space"
def test_autocompleter_initial_setup_without_auto_suggestions() -> None:
completer = AutoCompleter(auto_suggestions=False)
with (
patch.object(sys.stdin, "isatty", return_value=True),
patch(f"{_IMPL}.PromptSession") as mock_session,
):
with patch.object(sys.stdin, 'isatty', return_value=True), \
patch('argenta.app.autocompleter.entity.PromptSession') as mock_session:
completer.initial_setup({"start", "stop"})
assert completer._session is not None
call_kwargs = mock_session.call_args[1]
assert call_kwargs["auto_suggest"] is None
assert call_kwargs['auto_suggest'] is None
def test_autocompleter_initial_setup_without_command_highlighting() -> None:
completer = AutoCompleter(command_highlighting=False)
with (
patch.object(sys.stdin, "isatty", return_value=True),
patch(f"{_IMPL}.PromptSession") as mock_session,
):
with patch.object(sys.stdin, 'isatty', return_value=True), \
patch('argenta.app.autocompleter.entity.PromptSession') as mock_session:
completer.initial_setup({"start", "stop"})
assert completer._session is not None
call_kwargs = mock_session.call_args[1]
assert call_kwargs["style"] is None
assert call_kwargs["lexer"] is None
assert call_kwargs['style'] is None
assert call_kwargs['lexer'] is None
def _setup_captured_handler(completer: AutoCompleter) -> Callable[[Any], None] | None:
"""Вспомогательная функция: поднимает initial_setup и захватывает kb-хендлер."""
def test_autocompleter_key_binding_handler_with_complete_state() -> None:
completer = AutoCompleter()
captured_handler: Callable[[Any], None] | None = None
def capture_kb_add(key: str) -> Callable[[Callable[[Any], None]], Callable[[Any], None]]:
def decorator(func: Callable[[Any], None]) -> Callable[[Any], None]:
nonlocal captured_handler
captured_handler = func
return func
return decorator
with (
patch.object(sys.stdin, "isatty", return_value=True),
patch(f"{_IMPL}.PromptSession"),
patch(f"{_IMPL}.KeyBindings") as mock_kb_class,
):
with patch.object(sys.stdin, 'isatty', return_value=True), \
patch('argenta.app.autocompleter.entity.PromptSession'), \
patch('argenta.app.autocompleter.entity.KeyBindings') as mock_kb_class:
mock_kb = MagicMock()
mock_kb.add = capture_kb_add
mock_kb_class.return_value = mock_kb
completer.initial_setup({"start", "stop"})
return captured_handler
def test_autocompleter_key_binding_handler_with_complete_state() -> None:
completer = AutoCompleter()
captured_handler = _setup_captured_handler(completer)
assert captured_handler is not None
mock_event = MagicMock()
mock_buff = MagicMock()
mock_buff.complete_state = True
mock_event.app.current_buffer = mock_buff
captured_handler(mock_event)
mock_buff.complete_next.assert_called_once()
def test_autocompleter_key_binding_handler_no_completions() -> None:
completer = AutoCompleter()
captured_handler = _setup_captured_handler(completer)
assert captured_handler is not None
captured_handler: Callable[[Any], None] | None = None
def capture_kb_add(key: str) -> Callable[[Callable[[Any], None]], Callable[[Any], None]]:
def decorator(func: Callable[[Any], None]) -> Callable[[Any], None]:
nonlocal captured_handler
captured_handler = func
return func
return decorator
with patch.object(sys.stdin, 'isatty', return_value=True), \
patch('argenta.app.autocompleter.entity.PromptSession'), \
patch('argenta.app.autocompleter.entity.KeyBindings') as mock_kb_class:
mock_kb = MagicMock()
mock_kb.add = capture_kb_add
mock_kb_class.return_value = mock_kb
completer.initial_setup({"start", "stop"})
mock_event = MagicMock()
mock_buff = MagicMock()
mock_buff.complete_state = False
@@ -329,18 +332,36 @@ def test_autocompleter_key_binding_handler_no_completions() -> None:
mock_completer.get_completions.return_value = iter([])
mock_buff.completer = mock_completer
mock_event.app.current_buffer = mock_buff
assert captured_handler is not None
captured_handler(mock_event)
mock_buff.start_completion.assert_not_called()
mock_buff.apply_completion.assert_not_called()
def test_autocompleter_key_binding_handler_single_completion() -> None:
completer = AutoCompleter()
captured_handler = _setup_captured_handler(completer)
assert captured_handler is not None
captured_handler: Callable[[Any], None] | None = None
def capture_kb_add(key: str) -> Callable[[Callable[[Any], None]], Callable[[Any], None]]:
def decorator(func: Callable[[Any], None]) -> Callable[[Any], None]:
nonlocal captured_handler
captured_handler = func
return func
return decorator
with patch.object(sys.stdin, 'isatty', return_value=True), \
patch('argenta.app.autocompleter.entity.PromptSession'), \
patch('argenta.app.autocompleter.entity.KeyBindings') as mock_kb_class:
mock_kb = MagicMock()
mock_kb.add = capture_kb_add
mock_kb_class.return_value = mock_kb
completer.initial_setup({"start", "stop"})
mock_event = MagicMock()
mock_buff = MagicMock()
mock_buff.complete_state = False
@@ -349,18 +370,36 @@ def test_autocompleter_key_binding_handler_single_completion() -> None:
mock_completer.get_completions.return_value = iter([mock_completion])
mock_buff.completer = mock_completer
mock_event.app.current_buffer = mock_buff
assert captured_handler is not None
captured_handler(mock_event)
mock_buff.apply_completion.assert_called_once_with(mock_completion)
mock_buff.start_completion.assert_not_called()
def test_autocompleter_key_binding_handler_multiple_completions() -> None:
completer = AutoCompleter()
captured_handler = _setup_captured_handler(completer)
assert captured_handler is not None
captured_handler: Callable[[Any], None] | None = None
def capture_kb_add(key: str) -> Callable[[Callable[[Any], None]], Callable[[Any], None]]:
def decorator(func: Callable[[Any], None]) -> Callable[[Any], None]:
nonlocal captured_handler
captured_handler = func
return func
return decorator
with patch.object(sys.stdin, 'isatty', return_value=True), \
patch('argenta.app.autocompleter.entity.PromptSession'), \
patch('argenta.app.autocompleter.entity.KeyBindings') as mock_kb_class:
mock_kb = MagicMock()
mock_kb.add = capture_kb_add
mock_kb_class.return_value = mock_kb
completer.initial_setup({"start", "stop"})
mock_event = MagicMock()
mock_buff = MagicMock()
mock_buff.complete_state = False
@@ -370,53 +409,54 @@ def test_autocompleter_key_binding_handler_multiple_completions() -> None:
mock_completer.get_completions.return_value = iter([mock_completion1, mock_completion2])
mock_buff.completer = mock_completer
mock_event.app.current_buffer = mock_buff
assert captured_handler is not None
captured_handler(mock_event)
mock_buff.start_completion.assert_called_once_with(select_first=False)
mock_buff.apply_completion.assert_not_called()
def test_autocompleter_prompt_in_fallback_mode_with_string() -> None:
completer = AutoCompleter()
with patch.object(sys.stdin, "isatty", return_value=False):
with patch.object(sys.stdin, 'isatty', return_value=False):
completer.initial_setup({"start", "stop"})
assert completer._fallback_mode is True
with patch("builtins.input", return_value="test input"):
with patch('builtins.input', return_value='test input'):
result = completer.prompt(">>> ")
assert result == "test input"
assert result == 'test input'
def test_autocompleter_prompt_in_fallback_mode_with_html() -> None:
completer = AutoCompleter()
with patch.object(sys.stdin, "isatty", return_value=False):
with patch.object(sys.stdin, 'isatty', return_value=False):
completer.initial_setup({"start", "stop"})
assert completer._fallback_mode is True
with patch("builtins.input", return_value="test input"):
with patch('builtins.input', return_value='test input'):
result = completer.prompt(HTML("<b>>>> </b>"))
assert result == "test input"
assert result == 'test input'
def test_autocompleter_prompt_with_html_in_normal_mode() -> None:
completer = AutoCompleter()
mock_session = MagicMock()
mock_session.prompt.return_value = "test result"
mock_session.prompt.return_value = 'test result'
completer._session = mock_session
completer._fallback_mode = False
html_prompt = HTML("<b>>>> </b>")
result = completer.prompt(html_prompt)
assert result == "test result"
assert result == 'test result'
mock_session.prompt.assert_called_once()
call_args = mock_session.prompt.call_args
assert call_args[0][0] == html_prompt
@@ -424,15 +464,15 @@ def test_autocompleter_prompt_with_html_in_normal_mode() -> None:
def test_autocompleter_prompt_with_string_in_normal_mode() -> None:
completer = AutoCompleter()
mock_session = MagicMock()
mock_session.prompt.return_value = "test result"
mock_session.prompt.return_value = 'test result'
completer._session = mock_session
completer._fallback_mode = False
result = completer.prompt(">>> ")
assert result == "test result"
assert result == 'test result'
mock_session.prompt.assert_called_once()
call_args = mock_session.prompt.call_args
assert isinstance(call_args[0][0], HTML)
+36 -35
View File
@@ -45,11 +45,12 @@ def sample_router() -> Router:
# ============================================================================
def test_orchestrator_initializes_with_no_argparser(mocker: MockerFixture) -> None:
"""Test Orchestrator initialization with no ArgParser"""
def test_orchestrator_initializes_with_default_argparser(mocker: MockerFixture) -> None:
"""Test Orchestrator initialization with default ArgParser"""
mocker.patch('sys.argv', ['test_program'])
orchestrator = Orchestrator()
assert orchestrator._arg_parser is None
assert orchestrator._arg_parser is not None
assert isinstance(orchestrator._arg_parser, ArgParser)
def test_orchestrator_initializes_with_custom_argparser(mock_argparser: ArgParser) -> None:
@@ -88,80 +89,80 @@ def test_orchestrator_parses_args_on_initialization(mocker: MockerFixture, mock_
# ============================================================================
# Tests for run_repl method
# Tests for start_polling method
# ============================================================================
def test_run_repl_creates_dishka_container(
def test_start_polling_creates_dishka_container(
mocker: MockerFixture, mock_argparser: ArgParser, sample_app: App
) -> None:
"""Test that run_repl creates a dishka container"""
"""Test that start_polling creates a dishka container"""
mock_make_container = mocker.patch('argenta.orchestrator.entity.make_container')
_mock_setup_dishka = mocker.patch('argenta.orchestrator.entity.setup_dishka')
mocker.patch.object(sample_app, '_run_repl')
mocker.patch.object(sample_app, '_run_polling')
orchestrator = Orchestrator(arg_parser=mock_argparser)
orchestrator.run_repl(sample_app)
orchestrator.start_polling(sample_app)
mock_make_container.assert_called_once()
assert mock_make_container.call_args[1]['context'] == {ArgParser: mock_argparser}
def test_run_repl_calls_setup_dishka_with_auto_inject_enabled(
def test_start_polling_calls_setup_dishka_with_auto_inject_enabled(
mocker: MockerFixture, mock_argparser: ArgParser, sample_app: App
) -> None:
"""Test that run_repl calls setup_dishka with auto_inject=True"""
"""Test that start_polling calls setup_dishka with auto_inject=True"""
mock_container = mocker.MagicMock() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
mocker.patch('argenta.orchestrator.entity.make_container', return_value=mock_container)
mock_setup_dishka = mocker.patch('argenta.orchestrator.entity.setup_dishka')
mocker.patch.object(sample_app, '_run_repl')
mocker.patch.object(sample_app, '_run_polling')
orchestrator = Orchestrator(arg_parser=mock_argparser, auto_inject_handlers=True)
orchestrator.run_repl(sample_app)
orchestrator.start_polling(sample_app)
mock_setup_dishka.assert_called_once_with(sample_app, mock_container, auto_inject=True)
def test_run_repl_calls_setup_dishka_with_auto_inject_disabled(
def test_start_polling_calls_setup_dishka_with_auto_inject_disabled(
mocker: MockerFixture, mock_argparser: ArgParser, sample_app: App
) -> None:
"""Test that run_repl calls setup_dishka with auto_inject=False"""
"""Test that start_polling calls setup_dishka with auto_inject=False"""
mock_container = mocker.MagicMock() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
mocker.patch('argenta.orchestrator.entity.make_container', return_value=mock_container)
mock_setup_dishka = mocker.patch('argenta.orchestrator.entity.setup_dishka')
mocker.patch.object(sample_app, '_run_repl')
mocker.patch.object(sample_app, '_run_polling')
orchestrator = Orchestrator(arg_parser=mock_argparser, auto_inject_handlers=False)
orchestrator.run_repl(sample_app)
orchestrator.start_polling(sample_app)
mock_setup_dishka.assert_called_once_with(sample_app, mock_container, auto_inject=False)
def test_run_repl_calls_app_run_repl(
def test_start_polling_calls_app_run_polling(
mocker: MockerFixture, mock_argparser: ArgParser, sample_app: App
) -> None:
"""Test that run_repl calls app.run_polling()"""
"""Test that start_polling calls app.run_polling()"""
mocker.patch('argenta.orchestrator.entity.make_container')
mocker.patch('argenta.orchestrator.entity.setup_dishka')
mock_run_repl = mocker.patch.object(sample_app, '_run_repl')
mock_run_polling = mocker.patch.object(sample_app, '_run_polling')
orchestrator = Orchestrator(arg_parser=mock_argparser)
orchestrator.run_repl(sample_app)
orchestrator.start_polling(sample_app)
mock_run_repl.assert_called_once()
mock_run_polling.assert_called_once()
def test_run_repl_includes_custom_providers_in_container(
def test_start_polling_includes_custom_providers_in_container(
mocker: MockerFixture, mock_argparser: ArgParser, sample_app: App
) -> None:
"""Test that run_repl includes custom providers in container"""
"""Test that start_polling includes custom providers in container"""
custom_provider = Provider()
mock_make_container = mocker.patch('argenta.orchestrator.entity.make_container')
mocker.patch('argenta.orchestrator.entity.setup_dishka')
mocker.patch.object(sample_app, '_run_repl')
mocker.patch.object(sample_app, '_run_polling')
orchestrator = Orchestrator(arg_parser=mock_argparser, custom_providers=[custom_provider])
orchestrator.run_repl(sample_app)
orchestrator.start_polling(sample_app)
# Check that custom_provider was passed to make_container
call_args = mock_make_container.call_args[0]
@@ -179,14 +180,14 @@ def test_orchestrator_integrates_with_app_with_router(
"""Test that Orchestrator properly integrates with App that has routers"""
mocker.patch('argenta.orchestrator.entity.make_container')
mocker.patch('argenta.orchestrator.entity.setup_dishka')
mock_run_repl = mocker.patch.object(sample_app, '_run_repl')
mock_run_polling = mocker.patch.object(sample_app, '_run_polling')
sample_app.include_router(sample_router)
orchestrator = Orchestrator(arg_parser=mock_argparser)
orchestrator.run_repl(sample_app)
orchestrator.start_polling(sample_app)
mock_run_repl.assert_called_once()
mock_run_polling.assert_called_once()
assert len(sample_app.registered_routers.registered_routers) == 1
@@ -201,10 +202,10 @@ def test_orchestrator_passes_argparser_to_container_context(
"""Test that Orchestrator passes ArgParser instance to container context"""
mock_make_container = mocker.patch('argenta.orchestrator.entity.make_container')
mocker.patch('argenta.orchestrator.entity.setup_dishka')
mocker.patch.object(sample_app, '_run_repl')
mocker.patch.object(sample_app, '_run_polling')
orchestrator = Orchestrator(arg_parser=mock_argparser)
orchestrator.run_repl(sample_app)
orchestrator.start_polling(sample_app)
# Verify that ArgParser was passed in context
call_kwargs = mock_make_container.call_args[1]
@@ -218,18 +219,18 @@ def test_orchestrator_passes_argparser_to_container_context(
# ============================================================================
def test_orchestrator_handles_app_run_repl_exception(
def test_orchestrator_handles_app_run_polling_exception(
mocker: MockerFixture, mock_argparser: ArgParser, sample_app: App
) -> None:
"""Test that Orchestrator propagates exceptions from app.run_polling()"""
mocker.patch('argenta.orchestrator.entity.make_container')
mocker.patch('argenta.orchestrator.entity.setup_dishka')
mocker.patch.object(sample_app, '_run_repl', side_effect=RuntimeError("Test error"))
mocker.patch.object(sample_app, '_run_polling', side_effect=RuntimeError("Test error"))
orchestrator = Orchestrator(arg_parser=mock_argparser)
with pytest.raises(RuntimeError, match="Test error"):
orchestrator.run_repl(sample_app)
orchestrator.start_polling(sample_app)
# ============================================================================
@@ -245,13 +246,13 @@ def test_orchestrator_accepts_multiple_custom_providers(
provider2 = Provider()
mock_make_container = mocker.patch('argenta.orchestrator.entity.make_container')
mocker.patch('argenta.orchestrator.entity.setup_dishka')
mocker.patch.object(sample_app, '_run_repl')
mocker.patch.object(sample_app, '_run_polling')
orchestrator = Orchestrator(
arg_parser=mock_argparser,
custom_providers=[provider1, provider2]
)
orchestrator.run_repl(sample_app)
orchestrator.start_polling(sample_app)
call_args = mock_make_container.call_args[0]
assert provider1 in call_args
Generated
+64 -349
View File
@@ -1,6 +1,6 @@
version = 1
revision = 3
requires-python = ">=3.12, <3.15"
requires-python = ">=3.12"
[[package]]
name = "aiosqlite"
@@ -48,23 +48,12 @@ dependencies = [
{ name = "rich" },
]
[package.optional-dependencies]
cli = [
{ name = "nuitka", extra = ["onefile"] },
{ name = "typer" },
]
[package.dev-dependencies]
dev = [
{ name = "cairosvg" },
{ name = "esbonio" },
{ name = "isort" },
{ name = "matplotlib" },
{ name = "mypy" },
{ name = "psutil" },
{ name = "py-cpuinfo" },
{ name = "pyfakefs" },
{ name = "pygal" },
{ name = "pytest" },
{ name = "pytest-cov" },
{ name = "pytest-mock" },
@@ -89,11 +78,9 @@ linters = [
{ name = "wemake-python-styleguide" },
]
metrics = [
{ name = "cairosvg" },
{ name = "matplotlib" },
{ name = "psutil" },
{ name = "py-cpuinfo" },
{ name = "pygal" },
]
tests = [
{ name = "pyfakefs" },
@@ -109,24 +96,16 @@ typecheckers = [
requires-dist = [
{ name = "art", specifier = ">=6.4,<7.0" },
{ name = "dishka", specifier = ">=1.7.2" },
{ name = "nuitka", extras = ["onefile"], marker = "extra == 'cli'", specifier = ">=4.0.5" },
{ name = "prompt-toolkit", specifier = ">=3.0.52" },
{ name = "rich", specifier = ">=14.0.0,<15.0.0" },
{ name = "typer", marker = "extra == 'cli'", specifier = ">=0.9,!=0.12,<=0.21.1" },
]
provides-extras = ["cli"]
[package.metadata.requires-dev]
dev = [
{ name = "cairosvg", specifier = ">=2.8.2" },
{ name = "esbonio", specifier = ">=1.0.0" },
{ name = "isort", specifier = ">=7.0.0" },
{ name = "matplotlib", specifier = ">=3.10.8" },
{ name = "mypy", specifier = ">=1.14.1" },
{ name = "psutil", specifier = ">=7.2.1" },
{ name = "py-cpuinfo", specifier = ">=9.0.0" },
{ name = "pyfakefs", specifier = ">=5.5.0" },
{ name = "pygal", specifier = ">=3.1.0" },
{ name = "pytest", specifier = ">=8.3.2" },
{ name = "pytest-cov", specifier = ">=7.0.0" },
{ name = "pytest-mock", specifier = ">=3.15.1" },
@@ -151,11 +130,9 @@ linters = [
{ name = "wemake-python-styleguide", specifier = ">=0.17.0" },
]
metrics = [
{ name = "cairosvg", specifier = ">=2.8.2" },
{ name = "matplotlib", specifier = ">=3.10.8" },
{ name = "psutil", specifier = ">=7.2.1" },
{ name = "py-cpuinfo", specifier = ">=9.0.0" },
{ name = "pygal", specifier = ">=3.1.0" },
]
tests = [
{ name = "pyfakefs", specifier = ">=5.5.0" },
@@ -192,34 +169,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" },
]
[[package]]
name = "cairocffi"
version = "1.7.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi" },
]
sdist = { url = "https://files.pythonhosted.org/packages/70/c5/1a4dc131459e68a173cbdab5fad6b524f53f9c1ef7861b7698e998b837cc/cairocffi-1.7.1.tar.gz", hash = "sha256:2e48ee864884ec4a3a34bfa8c9ab9999f688286eb714a15a43ec9d068c36557b", size = 88096, upload-time = "2024-06-18T10:56:06.741Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/93/d8/ba13451aa6b745c49536e87b6bf8f629b950e84bd0e8308f7dc6883b67e2/cairocffi-1.7.1-py3-none-any.whl", hash = "sha256:9803a0e11f6c962f3b0ae2ec8ba6ae45e957a146a004697a1ac1bbf16b073b3f", size = 75611, upload-time = "2024-06-18T10:55:59.489Z" },
]
[[package]]
name = "cairosvg"
version = "2.8.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cairocffi" },
{ name = "cssselect2" },
{ name = "defusedxml" },
{ name = "pillow" },
{ name = "tinycss2" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ab/b9/5106168bd43d7cd8b7cc2a2ee465b385f14b63f4c092bb89eee2d48c8e67/cairosvg-2.8.2.tar.gz", hash = "sha256:07cbf4e86317b27a92318a4cac2a4bb37a5e9c1b8a27355d06874b22f85bef9f", size = 8398590, upload-time = "2025-05-15T06:56:32.653Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/67/48/816bd4aaae93dbf9e408c58598bc32f4a8c65f4b86ab560864cb3ee60adb/cairosvg-2.8.2-py3-none-any.whl", hash = "sha256:eab46dad4674f33267a671dce39b64be245911c901c70d65d2b7b0821e852bf5", size = 45773, upload-time = "2025-05-15T06:56:28.552Z" },
]
[[package]]
name = "cattrs"
version = "25.2.0"
@@ -242,63 +191,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" },
]
[[package]]
name = "cffi"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
{ url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
{ url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
{ url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
{ url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
{ url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
{ url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
{ url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
{ url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
{ url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
{ url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
{ url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
{ url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
{ url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
{ url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
{ url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
{ url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
{ url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
{ url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
{ url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
{ url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
{ url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
{ url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
{ url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
{ url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
{ url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
{ url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
{ url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
{ url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
{ url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
{ url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
{ url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
{ url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
{ url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
{ url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
{ url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
{ url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
{ url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
{ url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
{ url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
{ url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
{ url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
{ url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
{ url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
{ url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
]
[[package]]
name = "charset-normalizer"
version = "3.4.4"
@@ -529,19 +421,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5f/04/642c1d8a448ae5ea1369eac8495740a79eb4e581a9fb0cbdce56bbf56da1/coverage-7.11.0-py3-none-any.whl", hash = "sha256:4b7589765348d78fb4e5fb6ea35d07564e387da2fc5efff62e0222971f155f68", size = 207761, upload-time = "2025-10-15T15:15:06.439Z" },
]
[[package]]
name = "cssselect2"
version = "0.9.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "tinycss2" },
{ name = "webencodings" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e0/20/92eaa6b0aec7189fa4b75c890640e076e9e793095721db69c5c81142c2e1/cssselect2-0.9.0.tar.gz", hash = "sha256:759aa22c216326356f65e62e791d66160a0f9c91d1424e8d8adc5e74dddfc6fb", size = 35595, upload-time = "2026-02-12T17:16:39.614Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/21/0e/8459ca4413e1a21a06c97d134bfaf18adfd27cea068813dc0faae06cbf00/cssselect2-0.9.0-py3-none-any.whl", hash = "sha256:6a99e5f91f9a016a304dd929b0966ca464bcfda15177b6fb4a118fc0fb5d9563", size = 15453, upload-time = "2026-02-12T17:16:38.317Z" },
]
[[package]]
name = "cycler"
version = "0.12.1"
@@ -551,15 +430,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" },
]
[[package]]
name = "defusedxml"
version = "0.7.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" },
]
[[package]]
name = "dishka"
version = "1.7.2"
@@ -676,18 +546,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" },
]
[[package]]
name = "importlib-metadata"
version = "8.7.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "zipp" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" },
]
[[package]]
name = "iniconfig"
version = "2.1.0"
@@ -991,17 +849,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
]
[[package]]
name = "nuitka"
version = "4.0.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f8/40/f73e578922084f9e465b30abdb9963aadcc087b5a9399033472d9ef641ab/nuitka-4.0.5.tar.gz", hash = "sha256:45e7d90266e76fe64eeb8d196c17666d7cd7cffbf68d6a24f233c3c03b6feaa8", size = 4420878, upload-time = "2026-03-12T10:03:39.562Z" }
[package.optional-dependencies]
onefile = [
{ name = "zstandard" },
]
[[package]]
name = "numpy"
version = "2.4.1"
@@ -1083,71 +930,71 @@ wheels = [
[[package]]
name = "pillow"
version = "12.1.1"
version = "12.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" }
sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" },
{ url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" },
{ url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" },
{ url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" },
{ url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" },
{ url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" },
{ url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" },
{ url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" },
{ url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" },
{ url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" },
{ url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" },
{ url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" },
{ url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" },
{ url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" },
{ url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" },
{ url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" },
{ url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" },
{ url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" },
{ url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" },
{ url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" },
{ url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" },
{ url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" },
{ url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" },
{ url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" },
{ url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" },
{ url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" },
{ url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" },
{ url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" },
{ url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" },
{ url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" },
{ url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" },
{ url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" },
{ url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" },
{ url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" },
{ url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" },
{ url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" },
{ url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" },
{ url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" },
{ url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" },
{ url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" },
{ url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" },
{ url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" },
{ url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" },
{ url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" },
{ url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" },
{ url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" },
{ url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" },
{ url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" },
{ url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" },
{ url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" },
{ url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" },
{ url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" },
{ url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" },
{ url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" },
{ url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" },
{ url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" },
{ url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" },
{ url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" },
{ url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" },
{ url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" },
{ url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" },
{ url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" },
{ url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" },
{ url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" },
{ url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" },
{ url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" },
{ url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" },
{ url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" },
{ url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" },
{ url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" },
{ url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" },
{ url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" },
{ url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" },
{ url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" },
{ url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" },
{ url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" },
{ url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" },
{ url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" },
{ url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" },
{ url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" },
{ url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" },
{ url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" },
{ url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" },
{ url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" },
{ url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" },
{ url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" },
{ url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" },
{ url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" },
{ url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" },
{ url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" },
{ url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" },
{ url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" },
{ url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" },
{ url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" },
{ url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" },
{ url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" },
{ url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" },
{ url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" },
{ url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" },
{ url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" },
{ url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" },
{ url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" },
{ url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" },
{ url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" },
{ url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" },
{ url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" },
{ url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" },
{ url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" },
{ url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" },
{ url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" },
{ url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" },
{ url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" },
{ url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" },
{ url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" },
{ url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" },
{ url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" },
{ url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" },
{ url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" },
{ url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" },
{ url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" },
{ url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" },
{ url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" },
]
[[package]]
@@ -1226,15 +1073,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" },
]
[[package]]
name = "pycparser"
version = "3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
]
[[package]]
name = "pyfakefs"
version = "5.10.0"
@@ -1253,18 +1091,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" },
]
[[package]]
name = "pygal"
version = "3.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "importlib-metadata" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b0/b6/04176faeb312c84d7f9bc1a810f96ee38d15597e226bb9bda59f3a5cb122/pygal-3.1.0.tar.gz", hash = "sha256:fbdee7351a7423e7907fb8a9c3b77305f6b5678cb2e6fd0db36a8825e42955ec", size = 81006, upload-time = "2025-12-09T10:29:19.587Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/4c/2862dd25352fe4b22ec7760d4fa12cb692587cd7ec3e378cbf644fc0d2a8/pygal-3.1.0-py3-none-any.whl", hash = "sha256:4e923490f3490c90c481f4535fa3adcda20ff374257ab9d8ae897f91b632c0bb", size = 130171, upload-time = "2025-12-09T10:29:16.721Z" },
]
[[package]]
name = "pygls"
version = "2.0.0"
@@ -1443,15 +1269,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/02/e7/062480ede84ecb56ee0f8f2e5b5a3b2a5bceeb73bbdf909d3c13f5438749/scriv-1.8.0-py3-none-any.whl", hash = "sha256:f00f51325b2f4bc96b16fbb1239d4ab577cc2422301a5dd4f5f9378aae2549e0", size = 39085, upload-time = "2025-12-30T00:01:08.599Z" },
]
[[package]]
name = "shellingham"
version = "1.5.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
]
[[package]]
name = "shibuya"
version = "2025.9.25"
@@ -1618,33 +1435,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/be/72/2db2f49247d0a18b4f1bb9a5a39a0162869acf235f3a96418363947b3d46/starlette-0.48.0-py3-none-any.whl", hash = "sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659", size = 73736, upload-time = "2025-09-13T08:41:03.869Z" },
]
[[package]]
name = "tinycss2"
version = "1.5.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "webencodings" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a3/ae/2ca4913e5c0f09781d75482874c3a95db9105462a92ddd303c7d285d3df2/tinycss2-1.5.1.tar.gz", hash = "sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957", size = 88195, upload-time = "2025-11-23T10:29:10.082Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl", hash = "sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661", size = 28404, upload-time = "2025-11-23T10:29:08.676Z" },
]
[[package]]
name = "typer"
version = "0.21.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "rich" },
{ name = "shellingham" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/36/bf/8825b5929afd84d0dabd606c67cd57b8388cb3ec385f7ef19c5cc2202069/typer-0.21.1.tar.gz", hash = "sha256:ea835607cd752343b6b2b7ce676893e5a0324082268b48f27aa058bdb7d2145d", size = 110371, upload-time = "2026-01-06T11:21:10.989Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-py3-none-any.whl", hash = "sha256:7985e89081c636b88d172c2ee0cfe33c253160994d47bdfdc302defd7d1f1d01", size = 47381, upload-time = "2026-01-06T11:21:09.824Z" },
]
[[package]]
name = "typing-extensions"
version = "4.13.2"
@@ -1755,15 +1545,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/18/0e/a5f0257ab47492b7afb5fb60347d14ba19445e2773fc8352d4be6bd2f6f8/wcwidth-0.3.0-py3-none-any.whl", hash = "sha256:073a1acb250e4add96cfd5ef84e0036605cd6e0d0782c8c15c80e42202348458", size = 85520, upload-time = "2026-01-21T17:44:08.002Z" },
]
[[package]]
name = "webencodings"
version = "0.5.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" },
]
[[package]]
name = "websockets"
version = "15.0.1"
@@ -1808,69 +1589,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/b8/08/c0776aa654dc43cb3
wheels = [
{ url = "https://files.pythonhosted.org/packages/9b/58/98c4aa00e3de8e45726029799d8facbdcd75347b2f48b285857577e8efd8/wemake_python_styleguide-1.4.0-py3-none-any.whl", hash = "sha256:c0727475a20a1b7d59f1d806040e84768bdb0935d1147023453aa44c14b65c95", size = 215985, upload-time = "2025-08-25T10:15:06.713Z" },
]
[[package]]
name = "zipp"
version = "3.23.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" },
]
[[package]]
name = "zstandard"
version = "0.25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738, upload-time = "2025-09-14T22:16:56.237Z" },
{ url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436, upload-time = "2025-09-14T22:16:57.774Z" },
{ url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019, upload-time = "2025-09-14T22:16:59.302Z" },
{ url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012, upload-time = "2025-09-14T22:17:01.156Z" },
{ url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148, upload-time = "2025-09-14T22:17:03.091Z" },
{ url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652, upload-time = "2025-09-14T22:17:04.979Z" },
{ url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993, upload-time = "2025-09-14T22:17:06.781Z" },
{ url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806, upload-time = "2025-09-14T22:17:08.415Z" },
{ url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659, upload-time = "2025-09-14T22:17:10.164Z" },
{ url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933, upload-time = "2025-09-14T22:17:11.857Z" },
{ url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008, upload-time = "2025-09-14T22:17:13.627Z" },
{ url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517, upload-time = "2025-09-14T22:17:16.103Z" },
{ url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292, upload-time = "2025-09-14T22:17:17.827Z" },
{ url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237, upload-time = "2025-09-14T22:17:19.954Z" },
{ url = "https://files.pythonhosted.org/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", size = 436922, upload-time = "2025-09-14T22:17:24.398Z" },
{ url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276, upload-time = "2025-09-14T22:17:21.429Z" },
{ url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679, upload-time = "2025-09-14T22:17:23.147Z" },
{ url = "https://files.pythonhosted.org/packages/35/0b/8df9c4ad06af91d39e94fa96cc010a24ac4ef1378d3efab9223cc8593d40/zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", size = 795735, upload-time = "2025-09-14T22:17:26.042Z" },
{ url = "https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", size = 640440, upload-time = "2025-09-14T22:17:27.366Z" },
{ url = "https://files.pythonhosted.org/packages/d9/14/933d27204c2bd404229c69f445862454dcc101cd69ef8c6068f15aaec12c/zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", size = 5343070, upload-time = "2025-09-14T22:17:28.896Z" },
{ url = "https://files.pythonhosted.org/packages/6d/db/ddb11011826ed7db9d0e485d13df79b58586bfdec56e5c84a928a9a78c1c/zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", size = 5063001, upload-time = "2025-09-14T22:17:31.044Z" },
{ url = "https://files.pythonhosted.org/packages/db/00/87466ea3f99599d02a5238498b87bf84a6348290c19571051839ca943777/zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", size = 5394120, upload-time = "2025-09-14T22:17:32.711Z" },
{ url = "https://files.pythonhosted.org/packages/2b/95/fc5531d9c618a679a20ff6c29e2b3ef1d1f4ad66c5e161ae6ff847d102a9/zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", size = 5451230, upload-time = "2025-09-14T22:17:34.41Z" },
{ url = "https://files.pythonhosted.org/packages/63/4b/e3678b4e776db00f9f7b2fe58e547e8928ef32727d7a1ff01dea010f3f13/zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", size = 5547173, upload-time = "2025-09-14T22:17:36.084Z" },
{ url = "https://files.pythonhosted.org/packages/4e/d5/ba05ed95c6b8ec30bd468dfeab20589f2cf709b5c940483e31d991f2ca58/zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", size = 5046736, upload-time = "2025-09-14T22:17:37.891Z" },
{ url = "https://files.pythonhosted.org/packages/50/d5/870aa06b3a76c73eced65c044b92286a3c4e00554005ff51962deef28e28/zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", size = 5576368, upload-time = "2025-09-14T22:17:40.206Z" },
{ url = "https://files.pythonhosted.org/packages/5d/35/398dc2ffc89d304d59bc12f0fdd931b4ce455bddf7038a0a67733a25f550/zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", size = 4954022, upload-time = "2025-09-14T22:17:41.879Z" },
{ url = "https://files.pythonhosted.org/packages/9a/5c/36ba1e5507d56d2213202ec2b05e8541734af5f2ce378c5d1ceaf4d88dc4/zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", size = 5267889, upload-time = "2025-09-14T22:17:43.577Z" },
{ url = "https://files.pythonhosted.org/packages/70/e8/2ec6b6fb7358b2ec0113ae202647ca7c0e9d15b61c005ae5225ad0995df5/zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", size = 5433952, upload-time = "2025-09-14T22:17:45.271Z" },
{ url = "https://files.pythonhosted.org/packages/7b/01/b5f4d4dbc59ef193e870495c6f1275f5b2928e01ff5a81fecb22a06e22fb/zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", size = 5814054, upload-time = "2025-09-14T22:17:47.08Z" },
{ url = "https://files.pythonhosted.org/packages/b2/e5/fbd822d5c6f427cf158316d012c5a12f233473c2f9c5fe5ab1ae5d21f3d8/zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", size = 5360113, upload-time = "2025-09-14T22:17:48.893Z" },
{ url = "https://files.pythonhosted.org/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", size = 436936, upload-time = "2025-09-14T22:17:52.658Z" },
{ url = "https://files.pythonhosted.org/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", size = 506232, upload-time = "2025-09-14T22:17:50.402Z" },
{ url = "https://files.pythonhosted.org/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", size = 462671, upload-time = "2025-09-14T22:17:51.533Z" },
{ url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887, upload-time = "2025-09-14T22:17:54.198Z" },
{ url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658, upload-time = "2025-09-14T22:17:55.423Z" },
{ url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849, upload-time = "2025-09-14T22:17:57.372Z" },
{ url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095, upload-time = "2025-09-14T22:17:59.498Z" },
{ url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751, upload-time = "2025-09-14T22:18:01.618Z" },
{ url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818, upload-time = "2025-09-14T22:18:03.769Z" },
{ url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402, upload-time = "2025-09-14T22:18:05.954Z" },
{ url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108, upload-time = "2025-09-14T22:18:07.68Z" },
{ url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248, upload-time = "2025-09-14T22:18:09.753Z" },
{ url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330, upload-time = "2025-09-14T22:18:11.966Z" },
{ url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123, upload-time = "2025-09-14T22:18:13.907Z" },
{ url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591, upload-time = "2025-09-14T22:18:16.465Z" },
{ url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513, upload-time = "2025-09-14T22:18:20.61Z" },
{ url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" },
{ url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" },
]