├── .github
└── workflows
│ └── ci.yaml
├── .gitignore
├── LICENSE
├── README.md
├── examples
├── argv.py
├── demo.py
├── nogroup_demo.py
└── typer.py
├── poetry.lock
├── pyproject.toml
├── tests
├── __init__.py
├── test_help.py
└── test_run_command.py
└── trogon
├── __init__.py
├── constants.py
├── detect_run_string.py
├── introspect.py
├── run_command.py
├── trogon.py
├── trogon.scss
├── typer.py
└── widgets
├── __init__.py
├── about.py
├── command_info.py
├── command_tree.py
├── form.py
├── multiple_choice.py
└── parameter_controls.py
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | name: Test Trogon
2 |
3 | on:
4 | pull_request:
5 | paths:
6 | - '.github/workflows/pythonpackage.yml'
7 | - '**.py'
8 | - '**.pyi'
9 | - '**.css'
10 | - '**.lock'
11 |
12 | jobs:
13 | build:
14 | runs-on: ${{ matrix.os }}
15 | strategy:
16 | matrix:
17 | os: [ubuntu-latest, macos-13, windows-latest]
18 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
19 | defaults:
20 | run:
21 | shell: bash
22 | steps:
23 | - uses: actions/checkout@v4.1.7
24 | - name: Install and configure Poetry
25 | uses: snok/install-poetry@v1.4.1
26 | with:
27 | version: 1.8.3
28 | virtualenvs-in-project: true
29 | - name: Set up Python ${{ matrix.python-version }}
30 | uses: actions/setup-python@v5.2.0
31 | with:
32 | python-version: ${{ matrix.python-version }}
33 | architecture: x64
34 | - name: Load cached venv
35 | id: cached-poetry-dependencies
36 | uses: actions/cache@v4
37 | with:
38 | path: .venv
39 | key: venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock') }}
40 | - name: Install dependencies
41 | run: poetry install
42 | if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
43 | - name: Test with pytest
44 | run: |
45 | source $VENV
46 | pytest tests
47 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | .vscode
3 | .DS_store
4 | dist/*
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Textualize
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | [](https://discord.gg/Enf6Z3qhVr)
7 |
8 |
9 | # Trogon
10 |
11 | Auto-generate friendly terminal user interfaces for command line apps.
12 |
13 |
14 |
15 | 🎬 Video demonstration
16 |
17 |
18 |
19 | A quick tour of a Trogon app applied to [sqlite-utils](https://github.com/simonw/sqlite-utils).
20 |
21 | https://github.com/Textualize/trogon/assets/554369/c9e5dabb-5624-45cb-8612-f6ecfde70362
22 |
23 |
24 |
25 |
26 | Trogon works with the popular [Click](https://click.palletsprojects.com/) library for Python, but will support other libraries and languages in the future.
27 |
28 | ## How it works
29 |
30 | Trogon inspects your (command line) app and extracts a *schema* which describes the options / switches / help etc.
31 | It then uses that information to build a [Textual](https://github.com/textualize/textual) UI you can use to edit and run the command.
32 |
33 | Ultimately we would like to formalize this schema and a protocol to extract or expose it from apps.
34 | This which would allow Trogon to build TUIs for any CLI app, regardless of how it was built.
35 | If you are familiar with Swagger, think Swagger for CLIs.
36 |
37 | ## Screenshots
38 |
39 |
40 |
41 |
42 |
43 |
44 | |
45 |
46 |
47 |
48 | |
49 |
50 |
51 |
52 |
53 |
54 |
55 | |
56 |
57 |
58 |
59 | |
60 |
61 |
62 |
63 |
64 |
65 | ## Why?
66 |
67 | Command line apps reward repeated use, but they lack in *discoverability*.
68 | If you don't use a CLI app frequently, or there are too many options to commit to memory, a Trogon TUI interface can help you (re)discover options and switches.
69 |
70 | ## What does the name mean?
71 |
72 | This project started life as a [Textual](https://github.com/Textualize/textual) experiment, which we have been giving birds' names to.
73 | A [Trogon](https://www.willmcgugan.com/blog/photography/post/costa-rica-trip-report-2017/#bird) is a beautiful bird I was lucky enough to photograph in 2017.
74 |
75 | See also [Frogmouth](https://github.com/Textualize/frogmouth), a Markdown browser for the terminal.
76 |
77 | ## Roadmap
78 |
79 | Trogon is usable now. It is only 2 lines (!) of code to add to an existing project.
80 |
81 | It is still in an early stage of development, and we have lots of improvements planned for it.
82 |
83 | ## Installing
84 |
85 | Trogon may be installed with PyPI.
86 |
87 | ```bash
88 | pip install trogon
89 | ```
90 |
91 | ## Quickstart
92 |
93 | ### Click
94 | 1. Import `from trogon import tui`
95 | 2. Add the `@tui` decorator above your click app, e.g.
96 | ```python
97 | from trogon import tui
98 |
99 | @tui()
100 | @click.group(...)
101 | def cli():
102 | ...
103 | ```
104 | 3. Your click app will have a new `tui` command available.
105 |
106 | ### Typer
107 | 1. Import `from trogon.typer import init_tui`
108 | 2. Pass your Typer CLI app into the `init_tui` function, e.g.
109 | ```python
110 | cli = typer.Typer(...)
111 | init_tui(cli)
112 | ```
113 | 3. Your Typer app will have a new `tui` command available.
114 |
115 | See also the `examples` folder for two example apps.
116 |
117 | ## Custom command name and custom help
118 |
119 | By default the command added will be called `tui` and the help text for it will be `Open Textual TUI.`
120 |
121 | You can customize one or both of these using the `command=` and `help=` parameters:
122 |
123 | ```python
124 | @tui(command="ui", help="Open terminal UI")
125 | @click.group(...)
126 | def cli():
127 | ...
128 | ```
129 |
130 | ## Follow this project
131 |
132 | If this app interests you, you may want to join the Textual [Discord server](https://discord.gg/Enf6Z3qhVr) where you can talk to Textual developers / community.
133 |
--------------------------------------------------------------------------------
/examples/argv.py:
--------------------------------------------------------------------------------
1 | import sys
2 | print(sys.orig_argv)
3 |
--------------------------------------------------------------------------------
/examples/demo.py:
--------------------------------------------------------------------------------
1 | import click
2 |
3 | from trogon import tui
4 |
5 |
6 | @tui()
7 | @click.group()
8 | @click.option(
9 | "--verbose", "-v", count=True, default=1, help="Increase verbosity level."
10 | )
11 | @click.pass_context
12 | def cli(ctx, verbose):
13 | ctx.ensure_object(dict)
14 | ctx.obj["verbose"] = verbose
15 |
16 |
17 | @cli.command()
18 | @click.argument("task")
19 | @click.option(
20 | "--priority", "-p", type=int, default=1, help="Set task priority (default: 1)"
21 | )
22 | @click.option("--tags", "-t", multiple=True, help="Add tags to the task (repeatable)")
23 | @click.option(
24 | "--extra",
25 | "-e",
26 | nargs=2,
27 | type=(str, int),
28 | multiple=True,
29 | default=[("one", 1), ("two", 2)],
30 | help="Add extra data as key-value pairs (repeatable)",
31 | )
32 | @click.option(
33 | "--category",
34 | "-c",
35 | default="home",
36 | type=click.Choice(["work", "home", "leisure"], case_sensitive=False),
37 | help="Choose a category for the task",
38 | )
39 | @click.option(
40 | "--labels",
41 | "-l",
42 | type=click.Choice(["important", "urgent", "later"], case_sensitive=False),
43 | multiple=True,
44 | default=["urgent"],
45 | help="Add labels to the task (repeatable)",
46 | )
47 | @click.pass_context
48 | def add(ctx, task, priority, tags, extra, category, labels):
49 | """Add a new task to the to-do list.
50 | Note:
51 | Control the output of this using the verbosity option.
52 | """
53 | if ctx.obj["verbose"] >= 2:
54 | click.echo(f"Adding task: {task}")
55 | click.echo(f"Priority: {priority}")
56 | click.echo(f'Tags: {", ".join(tags)}')
57 | click.echo(f"Extra data: {extra}")
58 | elif ctx.obj["verbose"] >= 1:
59 | click.echo(f"Adding task: {task}")
60 | else:
61 | pass
62 | # Implement the task adding functionality here
63 |
64 |
65 | @cli.command()
66 | @click.argument("task_id", type=int)
67 | @click.pass_context
68 | def remove(ctx, task_id):
69 | """Remove a task from the to-do list by its ID."""
70 | if ctx.obj["verbose"] >= 1:
71 | click.echo(f"Removing task with ID: {task_id}")
72 | # Implement the task removal functionality here
73 |
74 |
75 | @cli.command()
76 | @click.option(
77 | "--all/--not-all", default=True, help="List all tasks, including completed ones."
78 | )
79 | @click.option("--completed", "-c", is_flag=True, help="List only completed tasks.")
80 | @click.pass_context
81 | def list_tasks(ctx, all, completed):
82 | """List tasks from the to-do list."""
83 | if ctx.obj["verbose"] >= 1:
84 | click.echo(f"Listing tasks:")
85 | # Implement the task listing functionality here
86 |
87 |
88 | if __name__ == "__main__":
89 | cli(obj={})
90 |
--------------------------------------------------------------------------------
/examples/nogroup_demo.py:
--------------------------------------------------------------------------------
1 | import click
2 |
3 | from trogon import tui
4 |
5 |
6 | @tui()
7 | @click.option("--verbose", "-v", count=True, help="Increase verbosity level.")
8 | @click.option(
9 | "--priority", "-p", type=int, default=1, help="Set task priority (default: 1)"
10 | )
11 | @click.option("--tags", "-t", multiple=True, help="Add tags to the task (repeatable)")
12 | @click.option(
13 | "--extra",
14 | "-e",
15 | nargs=2,
16 | type=(str, int),
17 | multiple=True,
18 | help="Add extra data as key-value pairs (repeatable)",
19 | )
20 | @click.option(
21 | "--category",
22 | "-c",
23 | type=click.Choice(["work", "home", "leisure"], case_sensitive=False),
24 | help="Choose a category for the task",
25 | )
26 | @click.option(
27 | "--labels",
28 | "-l",
29 | type=click.Choice(["important", "urgent", "later"], case_sensitive=False),
30 | multiple=True,
31 | help="Add labels to the task (repeatable)",
32 | )
33 | @click.argument("task")
34 | @click.command()
35 | def add(verbose, task, priority, tags, extra, category, labels):
36 | """Add a new task to the to-do list."""
37 | if verbose >= 2:
38 | click.echo(f"Adding task: {task}")
39 | click.echo(f"Priority: {priority}")
40 | click.echo(f'Tags: {", ".join(tags)}')
41 | click.echo(f"Extra data: {extra}")
42 | click.echo(f"Category: {category}")
43 | click.echo(f'Labels: {", ".join(labels)}')
44 | elif verbose >= 1:
45 | click.echo(f"Adding task: {task}")
46 | else:
47 | pass
48 | # Implement the task adding functionality here
49 |
50 |
51 | if __name__ == "__main__":
52 | add()
53 |
--------------------------------------------------------------------------------
/examples/typer.py:
--------------------------------------------------------------------------------
1 | from typing import Annotated
2 |
3 | import typer
4 | from trogon.typer import init_tui
5 |
6 |
7 | app = typer.Typer()
8 |
9 |
10 | @app.command()
11 | def hello(name: Annotated[str, typer.Argument(help="The person to greet")]):
12 | typer.echo(f"Hello, {name}!")
13 |
14 |
15 | init_tui(app)
16 |
17 |
18 | if __name__ == "__main__":
19 | app()
20 |
--------------------------------------------------------------------------------
/poetry.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
2 |
3 | [[package]]
4 | name = "aiohappyeyeballs"
5 | version = "2.5.0"
6 | description = "Happy Eyeballs for asyncio"
7 | optional = false
8 | python-versions = ">=3.9"
9 | files = [
10 | {file = "aiohappyeyeballs-2.5.0-py3-none-any.whl", hash = "sha256:0850b580748c7071db98bffff6d4c94028d0d3035acc20fd721a0ce7e8cac35d"},
11 | {file = "aiohappyeyeballs-2.5.0.tar.gz", hash = "sha256:18fde6204a76deeabc97c48bdd01d5801cfda5d6b9c8bbeb1aaaee9d648ca191"},
12 | ]
13 |
14 | [[package]]
15 | name = "aiohttp"
16 | version = "3.11.13"
17 | description = "Async http client/server framework (asyncio)"
18 | optional = false
19 | python-versions = ">=3.9"
20 | files = [
21 | {file = "aiohttp-3.11.13-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a4fe27dbbeec445e6e1291e61d61eb212ee9fed6e47998b27de71d70d3e8777d"},
22 | {file = "aiohttp-3.11.13-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9e64ca2dbea28807f8484c13f684a2f761e69ba2640ec49dacd342763cc265ef"},
23 | {file = "aiohttp-3.11.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9840be675de208d1f68f84d578eaa4d1a36eee70b16ae31ab933520c49ba1325"},
24 | {file = "aiohttp-3.11.13-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28a772757c9067e2aee8a6b2b425d0efaa628c264d6416d283694c3d86da7689"},
25 | {file = "aiohttp-3.11.13-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b88aca5adbf4625e11118df45acac29616b425833c3be7a05ef63a6a4017bfdb"},
26 | {file = "aiohttp-3.11.13-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce10ddfbe26ed5856d6902162f71b8fe08545380570a885b4ab56aecfdcb07f4"},
27 | {file = "aiohttp-3.11.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa48dac27f41b36735c807d1ab093a8386701bbf00eb6b89a0f69d9fa26b3671"},
28 | {file = "aiohttp-3.11.13-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89ce611b1eac93ce2ade68f1470889e0173d606de20c85a012bfa24be96cf867"},
29 | {file = "aiohttp-3.11.13-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:78e4dd9c34ec7b8b121854eb5342bac8b02aa03075ae8618b6210a06bbb8a115"},
30 | {file = "aiohttp-3.11.13-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:66047eacbc73e6fe2462b77ce39fc170ab51235caf331e735eae91c95e6a11e4"},
31 | {file = "aiohttp-3.11.13-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5ad8f1c19fe277eeb8bc45741c6d60ddd11d705c12a4d8ee17546acff98e0802"},
32 | {file = "aiohttp-3.11.13-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64815c6f02e8506b10113ddbc6b196f58dbef135751cc7c32136df27b736db09"},
33 | {file = "aiohttp-3.11.13-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:967b93f21b426f23ca37329230d5bd122f25516ae2f24a9cea95a30023ff8283"},
34 | {file = "aiohttp-3.11.13-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cf1f31f83d16ec344136359001c5e871915c6ab685a3d8dee38e2961b4c81730"},
35 | {file = "aiohttp-3.11.13-cp310-cp310-win32.whl", hash = "sha256:00c8ac69e259c60976aa2edae3f13d9991cf079aaa4d3cd5a49168ae3748dee3"},
36 | {file = "aiohttp-3.11.13-cp310-cp310-win_amd64.whl", hash = "sha256:90d571c98d19a8b6e793b34aa4df4cee1e8fe2862d65cc49185a3a3d0a1a3996"},
37 | {file = "aiohttp-3.11.13-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6b35aab22419ba45f8fc290d0010898de7a6ad131e468ffa3922b1b0b24e9d2e"},
38 | {file = "aiohttp-3.11.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f81cba651db8795f688c589dd11a4fbb834f2e59bbf9bb50908be36e416dc760"},
39 | {file = "aiohttp-3.11.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f55d0f242c2d1fcdf802c8fabcff25a9d85550a4cf3a9cf5f2a6b5742c992839"},
40 | {file = "aiohttp-3.11.13-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4bea08a6aad9195ac9b1be6b0c7e8a702a9cec57ce6b713698b4a5afa9c2e33"},
41 | {file = "aiohttp-3.11.13-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c6070bcf2173a7146bb9e4735b3c62b2accba459a6eae44deea0eb23e0035a23"},
42 | {file = "aiohttp-3.11.13-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:718d5deb678bc4b9d575bfe83a59270861417da071ab44542d0fcb6faa686636"},
43 | {file = "aiohttp-3.11.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f6b2c5b4a4d22b8fb2c92ac98e0747f5f195e8e9448bfb7404cd77e7bfa243f"},
44 | {file = "aiohttp-3.11.13-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:747ec46290107a490d21fe1ff4183bef8022b848cf9516970cb31de6d9460088"},
45 | {file = "aiohttp-3.11.13-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:01816f07c9cc9d80f858615b1365f8319d6a5fd079cd668cc58e15aafbc76a54"},
46 | {file = "aiohttp-3.11.13-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:a08ad95fcbd595803e0c4280671d808eb170a64ca3f2980dd38e7a72ed8d1fea"},
47 | {file = "aiohttp-3.11.13-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c97be90d70f7db3aa041d720bfb95f4869d6063fcdf2bb8333764d97e319b7d0"},
48 | {file = "aiohttp-3.11.13-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ab915a57c65f7a29353c8014ac4be685c8e4a19e792a79fe133a8e101111438e"},
49 | {file = "aiohttp-3.11.13-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:35cda4e07f5e058a723436c4d2b7ba2124ab4e0aa49e6325aed5896507a8a42e"},
50 | {file = "aiohttp-3.11.13-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:af55314407714fe77a68a9ccaab90fdb5deb57342585fd4a3a8102b6d4370080"},
51 | {file = "aiohttp-3.11.13-cp311-cp311-win32.whl", hash = "sha256:42d689a5c0a0c357018993e471893e939f555e302313d5c61dfc566c2cad6185"},
52 | {file = "aiohttp-3.11.13-cp311-cp311-win_amd64.whl", hash = "sha256:b73a2b139782a07658fbf170fe4bcdf70fc597fae5ffe75e5b67674c27434a9f"},
53 | {file = "aiohttp-3.11.13-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2eabb269dc3852537d57589b36d7f7362e57d1ece308842ef44d9830d2dc3c90"},
54 | {file = "aiohttp-3.11.13-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b77ee42addbb1c36d35aca55e8cc6d0958f8419e458bb70888d8c69a4ca833d"},
55 | {file = "aiohttp-3.11.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55789e93c5ed71832e7fac868167276beadf9877b85697020c46e9a75471f55f"},
56 | {file = "aiohttp-3.11.13-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c929f9a7249a11e4aa5c157091cfad7f49cc6b13f4eecf9b747104befd9f56f2"},
57 | {file = "aiohttp-3.11.13-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d33851d85537bbf0f6291ddc97926a754c8f041af759e0aa0230fe939168852b"},
58 | {file = "aiohttp-3.11.13-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9229d8613bd8401182868fe95688f7581673e1c18ff78855671a4b8284f47bcb"},
59 | {file = "aiohttp-3.11.13-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:669dd33f028e54fe4c96576f406ebb242ba534dd3a981ce009961bf49960f117"},
60 | {file = "aiohttp-3.11.13-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c1b20a1ace54af7db1f95af85da530fe97407d9063b7aaf9ce6a32f44730778"},
61 | {file = "aiohttp-3.11.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5724cc77f4e648362ebbb49bdecb9e2b86d9b172c68a295263fa072e679ee69d"},
62 | {file = "aiohttp-3.11.13-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:aa36c35e94ecdb478246dd60db12aba57cfcd0abcad43c927a8876f25734d496"},
63 | {file = "aiohttp-3.11.13-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9b5b37c863ad5b0892cc7a4ceb1e435e5e6acd3f2f8d3e11fa56f08d3c67b820"},
64 | {file = "aiohttp-3.11.13-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e06cf4852ce8c4442a59bae5a3ea01162b8fcb49ab438d8548b8dc79375dad8a"},
65 | {file = "aiohttp-3.11.13-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5194143927e494616e335d074e77a5dac7cd353a04755330c9adc984ac5a628e"},
66 | {file = "aiohttp-3.11.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:afcb6b275c2d2ba5d8418bf30a9654fa978b4f819c2e8db6311b3525c86fe637"},
67 | {file = "aiohttp-3.11.13-cp312-cp312-win32.whl", hash = "sha256:7104d5b3943c6351d1ad7027d90bdd0ea002903e9f610735ac99df3b81f102ee"},
68 | {file = "aiohttp-3.11.13-cp312-cp312-win_amd64.whl", hash = "sha256:47dc018b1b220c48089b5b9382fbab94db35bef2fa192995be22cbad3c5730c8"},
69 | {file = "aiohttp-3.11.13-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9862d077b9ffa015dbe3ce6c081bdf35135948cb89116e26667dd183550833d1"},
70 | {file = "aiohttp-3.11.13-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fbfef0666ae9e07abfa2c54c212ac18a1f63e13e0760a769f70b5717742f3ece"},
71 | {file = "aiohttp-3.11.13-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:93a1f7d857c4fcf7cabb1178058182c789b30d85de379e04f64c15b7e88d66fb"},
72 | {file = "aiohttp-3.11.13-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba40b7ae0f81c7029583a338853f6607b6d83a341a3dcde8bed1ea58a3af1df9"},
73 | {file = "aiohttp-3.11.13-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b5b95787335c483cd5f29577f42bbe027a412c5431f2f80a749c80d040f7ca9f"},
74 | {file = "aiohttp-3.11.13-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7d474c5c1f0b9405c1565fafdc4429fa7d986ccbec7ce55bc6a330f36409cad"},
75 | {file = "aiohttp-3.11.13-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e83fb1991e9d8982b3b36aea1e7ad27ea0ce18c14d054c7a404d68b0319eebb"},
76 | {file = "aiohttp-3.11.13-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4586a68730bd2f2b04a83e83f79d271d8ed13763f64b75920f18a3a677b9a7f0"},
77 | {file = "aiohttp-3.11.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fe4eb0e7f50cdb99b26250d9328faef30b1175a5dbcfd6d0578d18456bac567"},
78 | {file = "aiohttp-3.11.13-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2a8a6bc19818ac3e5596310ace5aa50d918e1ebdcc204dc96e2f4d505d51740c"},
79 | {file = "aiohttp-3.11.13-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f27eec42f6c3c1df09cfc1f6786308f8b525b8efaaf6d6bd76c1f52c6511f6a"},
80 | {file = "aiohttp-3.11.13-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:2a4a13dfbb23977a51853b419141cd0a9b9573ab8d3a1455c6e63561387b52ff"},
81 | {file = "aiohttp-3.11.13-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:02876bf2f69b062584965507b07bc06903c2dc93c57a554b64e012d636952654"},
82 | {file = "aiohttp-3.11.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b992778d95b60a21c4d8d4a5f15aaab2bd3c3e16466a72d7f9bfd86e8cea0d4b"},
83 | {file = "aiohttp-3.11.13-cp313-cp313-win32.whl", hash = "sha256:507ab05d90586dacb4f26a001c3abf912eb719d05635cbfad930bdbeb469b36c"},
84 | {file = "aiohttp-3.11.13-cp313-cp313-win_amd64.whl", hash = "sha256:5ceb81a4db2decdfa087381b5fc5847aa448244f973e5da232610304e199e7b2"},
85 | {file = "aiohttp-3.11.13-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:51c3ff9c7a25f3cad5c09d9aacbc5aefb9267167c4652c1eb737989b554fe278"},
86 | {file = "aiohttp-3.11.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e271beb2b1dabec5cd84eb488bdabf9758d22ad13471e9c356be07ad139b3012"},
87 | {file = "aiohttp-3.11.13-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0e9eb7e5764abcb49f0e2bd8f5731849b8728efbf26d0cac8e81384c95acec3f"},
88 | {file = "aiohttp-3.11.13-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baae005092e3f200de02699314ac8933ec20abf998ec0be39448f6605bce93df"},
89 | {file = "aiohttp-3.11.13-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1982c98ac62c132d2b773d50e2fcc941eb0b8bad3ec078ce7e7877c4d5a2dce7"},
90 | {file = "aiohttp-3.11.13-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2b25b2eeb35707113b2d570cadc7c612a57f1c5d3e7bb2b13870fe284e08fc0"},
91 | {file = "aiohttp-3.11.13-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b27961d65639128336b7a7c3f0046dcc62a9443d5ef962e3c84170ac620cec47"},
92 | {file = "aiohttp-3.11.13-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a01fe9f1e05025eacdd97590895e2737b9f851d0eb2e017ae9574d9a4f0b6252"},
93 | {file = "aiohttp-3.11.13-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa1fb1b61881c8405829c50e9cc5c875bfdbf685edf57a76817dfb50643e4a1a"},
94 | {file = "aiohttp-3.11.13-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:25de43bb3cf83ad83efc8295af7310219af6dbe4c543c2e74988d8e9c8a2a917"},
95 | {file = "aiohttp-3.11.13-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fe7065e2215e4bba63dc00db9ae654c1ba3950a5fff691475a32f511142fcddb"},
96 | {file = "aiohttp-3.11.13-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:7836587eef675a17d835ec3d98a8c9acdbeb2c1d72b0556f0edf4e855a25e9c1"},
97 | {file = "aiohttp-3.11.13-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:85fa0b18558eb1427090912bd456a01f71edab0872f4e0f9e4285571941e4090"},
98 | {file = "aiohttp-3.11.13-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a86dc177eb4c286c19d1823ac296299f59ed8106c9536d2b559f65836e0fb2c6"},
99 | {file = "aiohttp-3.11.13-cp39-cp39-win32.whl", hash = "sha256:684eea71ab6e8ade86b9021bb62af4bf0881f6be4e926b6b5455de74e420783a"},
100 | {file = "aiohttp-3.11.13-cp39-cp39-win_amd64.whl", hash = "sha256:82c249f2bfa5ecbe4a1a7902c81c0fba52ed9ebd0176ab3047395d02ad96cfcb"},
101 | {file = "aiohttp-3.11.13.tar.gz", hash = "sha256:8ce789231404ca8fff7f693cdce398abf6d90fd5dae2b1847477196c243b1fbb"},
102 | ]
103 |
104 | [package.dependencies]
105 | aiohappyeyeballs = ">=2.3.0"
106 | aiosignal = ">=1.1.2"
107 | async-timeout = {version = ">=4.0,<6.0", markers = "python_version < \"3.11\""}
108 | attrs = ">=17.3.0"
109 | frozenlist = ">=1.1.1"
110 | multidict = ">=4.5,<7.0"
111 | propcache = ">=0.2.0"
112 | yarl = ">=1.17.0,<2.0"
113 |
114 | [package.extras]
115 | speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"]
116 |
117 | [[package]]
118 | name = "aiohttp-jinja2"
119 | version = "1.6"
120 | description = "jinja2 template renderer for aiohttp.web (http server for asyncio)"
121 | optional = false
122 | python-versions = ">=3.8"
123 | files = [
124 | {file = "aiohttp-jinja2-1.6.tar.gz", hash = "sha256:a3a7ff5264e5bca52e8ae547bbfd0761b72495230d438d05b6c0915be619b0e2"},
125 | {file = "aiohttp_jinja2-1.6-py3-none-any.whl", hash = "sha256:0df405ee6ad1b58e5a068a105407dc7dcc1704544c559f1938babde954f945c7"},
126 | ]
127 |
128 | [package.dependencies]
129 | aiohttp = ">=3.9.0"
130 | jinja2 = ">=3.0.0"
131 |
132 | [[package]]
133 | name = "aiosignal"
134 | version = "1.3.2"
135 | description = "aiosignal: a list of registered asynchronous callbacks"
136 | optional = false
137 | python-versions = ">=3.9"
138 | files = [
139 | {file = "aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5"},
140 | {file = "aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54"},
141 | ]
142 |
143 | [package.dependencies]
144 | frozenlist = ">=1.1.0"
145 |
146 | [[package]]
147 | name = "async-timeout"
148 | version = "5.0.1"
149 | description = "Timeout context manager for asyncio programs"
150 | optional = false
151 | python-versions = ">=3.8"
152 | files = [
153 | {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"},
154 | {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"},
155 | ]
156 |
157 | [[package]]
158 | name = "attrs"
159 | version = "25.1.0"
160 | description = "Classes Without Boilerplate"
161 | optional = false
162 | python-versions = ">=3.8"
163 | files = [
164 | {file = "attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a"},
165 | {file = "attrs-25.1.0.tar.gz", hash = "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e"},
166 | ]
167 |
168 | [package.extras]
169 | benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
170 | cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
171 | dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
172 | docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"]
173 | tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
174 | tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"]
175 |
176 | [[package]]
177 | name = "black"
178 | version = "24.10.0"
179 | description = "The uncompromising code formatter."
180 | optional = false
181 | python-versions = ">=3.9"
182 | files = [
183 | {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"},
184 | {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"},
185 | {file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"},
186 | {file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"},
187 | {file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"},
188 | {file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"},
189 | {file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"},
190 | {file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"},
191 | {file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"},
192 | {file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"},
193 | {file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"},
194 | {file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"},
195 | {file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"},
196 | {file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"},
197 | {file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"},
198 | {file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"},
199 | {file = "black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd"},
200 | {file = "black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f"},
201 | {file = "black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800"},
202 | {file = "black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7"},
203 | {file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"},
204 | {file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"},
205 | ]
206 |
207 | [package.dependencies]
208 | click = ">=8.0.0"
209 | mypy-extensions = ">=0.4.3"
210 | packaging = ">=22.0"
211 | pathspec = ">=0.9.0"
212 | platformdirs = ">=2"
213 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
214 | typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""}
215 |
216 | [package.extras]
217 | colorama = ["colorama (>=0.4.3)"]
218 | d = ["aiohttp (>=3.10)"]
219 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
220 | uvloop = ["uvloop (>=0.15.2)"]
221 |
222 | [[package]]
223 | name = "click"
224 | version = "8.1.8"
225 | description = "Composable command line interface toolkit"
226 | optional = false
227 | python-versions = ">=3.7"
228 | files = [
229 | {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"},
230 | {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"},
231 | ]
232 |
233 | [package.dependencies]
234 | colorama = {version = "*", markers = "platform_system == \"Windows\""}
235 |
236 | [[package]]
237 | name = "colorama"
238 | version = "0.4.6"
239 | description = "Cross-platform colored terminal text."
240 | optional = false
241 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
242 | files = [
243 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
244 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
245 | ]
246 |
247 | [[package]]
248 | name = "exceptiongroup"
249 | version = "1.2.2"
250 | description = "Backport of PEP 654 (exception groups)"
251 | optional = false
252 | python-versions = ">=3.7"
253 | files = [
254 | {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
255 | {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
256 | ]
257 |
258 | [package.extras]
259 | test = ["pytest (>=6)"]
260 |
261 | [[package]]
262 | name = "frozenlist"
263 | version = "1.5.0"
264 | description = "A list-like structure which implements collections.abc.MutableSequence"
265 | optional = false
266 | python-versions = ">=3.8"
267 | files = [
268 | {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a"},
269 | {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb"},
270 | {file = "frozenlist-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15538c0cbf0e4fa11d1e3a71f823524b0c46299aed6e10ebb4c2089abd8c3bec"},
271 | {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e79225373c317ff1e35f210dd5f1344ff31066ba8067c307ab60254cd3a78ad5"},
272 | {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9272fa73ca71266702c4c3e2d4a28553ea03418e591e377a03b8e3659d94fa76"},
273 | {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:498524025a5b8ba81695761d78c8dd7382ac0b052f34e66939c42df860b8ff17"},
274 | {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92b5278ed9d50fe610185ecd23c55d8b307d75ca18e94c0e7de328089ac5dcba"},
275 | {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f3c8c1dacd037df16e85227bac13cca58c30da836c6f936ba1df0c05d046d8d"},
276 | {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2ac49a9bedb996086057b75bf93538240538c6d9b38e57c82d51f75a73409d2"},
277 | {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e66cc454f97053b79c2ab09c17fbe3c825ea6b4de20baf1be28919460dd7877f"},
278 | {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3ba5f9a0dfed20337d3e966dc359784c9f96503674c2faf015f7fe8e96798c"},
279 | {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6321899477db90bdeb9299ac3627a6a53c7399c8cd58d25da094007402b039ab"},
280 | {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76e4753701248476e6286f2ef492af900ea67d9706a0155335a40ea21bf3b2f5"},
281 | {file = "frozenlist-1.5.0-cp310-cp310-win32.whl", hash = "sha256:977701c081c0241d0955c9586ffdd9ce44f7a7795df39b9151cd9a6fd0ce4cfb"},
282 | {file = "frozenlist-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:189f03b53e64144f90990d29a27ec4f7997d91ed3d01b51fa39d2dbe77540fd4"},
283 | {file = "frozenlist-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fd74520371c3c4175142d02a976aee0b4cb4a7cc912a60586ffd8d5929979b30"},
284 | {file = "frozenlist-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2f3f7a0fbc219fb4455264cae4d9f01ad41ae6ee8524500f381de64ffaa077d5"},
285 | {file = "frozenlist-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f47c9c9028f55a04ac254346e92977bf0f166c483c74b4232bee19a6697e4778"},
286 | {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0996c66760924da6e88922756d99b47512a71cfd45215f3570bf1e0b694c206a"},
287 | {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2fe128eb4edeabe11896cb6af88fca5346059f6c8d807e3b910069f39157869"},
288 | {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a8ea951bbb6cacd492e3948b8da8c502a3f814f5d20935aae74b5df2b19cf3d"},
289 | {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de537c11e4aa01d37db0d403b57bd6f0546e71a82347a97c6a9f0dcc532b3a45"},
290 | {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c2623347b933fcb9095841f1cc5d4ff0b278addd743e0e966cb3d460278840d"},
291 | {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cee6798eaf8b1416ef6909b06f7dc04b60755206bddc599f52232606e18179d3"},
292 | {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f5f9da7f5dbc00a604fe74aa02ae7c98bcede8a3b8b9666f9f86fc13993bc71a"},
293 | {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:90646abbc7a5d5c7c19461d2e3eeb76eb0b204919e6ece342feb6032c9325ae9"},
294 | {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bdac3c7d9b705d253b2ce370fde941836a5f8b3c5c2b8fd70940a3ea3af7f4f2"},
295 | {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03d33c2ddbc1816237a67f66336616416e2bbb6beb306e5f890f2eb22b959cdf"},
296 | {file = "frozenlist-1.5.0-cp311-cp311-win32.whl", hash = "sha256:237f6b23ee0f44066219dae14c70ae38a63f0440ce6750f868ee08775073f942"},
297 | {file = "frozenlist-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:0cc974cc93d32c42e7b0f6cf242a6bd941c57c61b618e78b6c0a96cb72788c1d"},
298 | {file = "frozenlist-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21"},
299 | {file = "frozenlist-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d"},
300 | {file = "frozenlist-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e"},
301 | {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a"},
302 | {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a"},
303 | {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee"},
304 | {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6"},
305 | {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e"},
306 | {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9"},
307 | {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039"},
308 | {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784"},
309 | {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631"},
310 | {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f"},
311 | {file = "frozenlist-1.5.0-cp312-cp312-win32.whl", hash = "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8"},
312 | {file = "frozenlist-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f"},
313 | {file = "frozenlist-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953"},
314 | {file = "frozenlist-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0"},
315 | {file = "frozenlist-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2"},
316 | {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f"},
317 | {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608"},
318 | {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b"},
319 | {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840"},
320 | {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439"},
321 | {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de"},
322 | {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641"},
323 | {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e"},
324 | {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9"},
325 | {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03"},
326 | {file = "frozenlist-1.5.0-cp313-cp313-win32.whl", hash = "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c"},
327 | {file = "frozenlist-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28"},
328 | {file = "frozenlist-1.5.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:dd94994fc91a6177bfaafd7d9fd951bc8689b0a98168aa26b5f543868548d3ca"},
329 | {file = "frozenlist-1.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d0da8bbec082bf6bf18345b180958775363588678f64998c2b7609e34719b10"},
330 | {file = "frozenlist-1.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:73f2e31ea8dd7df61a359b731716018c2be196e5bb3b74ddba107f694fbd7604"},
331 | {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:828afae9f17e6de596825cf4228ff28fbdf6065974e5ac1410cecc22f699d2b3"},
332 | {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1577515d35ed5649d52ab4319db757bb881ce3b2b796d7283e6634d99ace307"},
333 | {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2150cc6305a2c2ab33299453e2968611dacb970d2283a14955923062c8d00b10"},
334 | {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a72b7a6e3cd2725eff67cd64c8f13335ee18fc3c7befc05aed043d24c7b9ccb9"},
335 | {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c16d2fa63e0800723139137d667e1056bee1a1cf7965153d2d104b62855e9b99"},
336 | {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:17dcc32fc7bda7ce5875435003220a457bcfa34ab7924a49a1c19f55b6ee185c"},
337 | {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:97160e245ea33d8609cd2b8fd997c850b56db147a304a262abc2b3be021a9171"},
338 | {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f1e6540b7fa044eee0bb5111ada694cf3dc15f2b0347ca125ee9ca984d5e9e6e"},
339 | {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:91d6c171862df0a6c61479d9724f22efb6109111017c87567cfeb7b5d1449fdf"},
340 | {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c1fac3e2ace2eb1052e9f7c7db480818371134410e1f5c55d65e8f3ac6d1407e"},
341 | {file = "frozenlist-1.5.0-cp38-cp38-win32.whl", hash = "sha256:b97f7b575ab4a8af9b7bc1d2ef7f29d3afee2226bd03ca3875c16451ad5a7723"},
342 | {file = "frozenlist-1.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:374ca2dabdccad8e2a76d40b1d037f5bd16824933bf7bcea3e59c891fd4a0923"},
343 | {file = "frozenlist-1.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9bbcdfaf4af7ce002694a4e10a0159d5a8d20056a12b05b45cea944a4953f972"},
344 | {file = "frozenlist-1.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1893f948bf6681733aaccf36c5232c231e3b5166d607c5fa77773611df6dc336"},
345 | {file = "frozenlist-1.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2b5e23253bb709ef57a8e95e6ae48daa9ac5f265637529e4ce6b003a37b2621f"},
346 | {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f253985bb515ecd89629db13cb58d702035ecd8cfbca7d7a7e29a0e6d39af5f"},
347 | {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04a5c6babd5e8fb7d3c871dc8b321166b80e41b637c31a995ed844a6139942b6"},
348 | {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9fe0f1c29ba24ba6ff6abf688cb0b7cf1efab6b6aa6adc55441773c252f7411"},
349 | {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:226d72559fa19babe2ccd920273e767c96a49b9d3d38badd7c91a0fdeda8ea08"},
350 | {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15b731db116ab3aedec558573c1a5eec78822b32292fe4f2f0345b7f697745c2"},
351 | {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:366d8f93e3edfe5a918c874702f78faac300209a4d5bf38352b2c1bdc07a766d"},
352 | {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1b96af8c582b94d381a1c1f51ffaedeb77c821c690ea5f01da3d70a487dd0a9b"},
353 | {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c03eff4a41bd4e38415cbed054bbaff4a075b093e2394b6915dca34a40d1e38b"},
354 | {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:50cf5e7ee9b98f22bdecbabf3800ae78ddcc26e4a435515fc72d97903e8488e0"},
355 | {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1e76bfbc72353269c44e0bc2cfe171900fbf7f722ad74c9a7b638052afe6a00c"},
356 | {file = "frozenlist-1.5.0-cp39-cp39-win32.whl", hash = "sha256:666534d15ba8f0fda3f53969117383d5dc021266b3c1a42c9ec4855e4b58b9d3"},
357 | {file = "frozenlist-1.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:5c28f4b5dbef8a0d8aad0d4de24d1e9e981728628afaf4ea0792f5d0939372f0"},
358 | {file = "frozenlist-1.5.0-py3-none-any.whl", hash = "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3"},
359 | {file = "frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817"},
360 | ]
361 |
362 | [[package]]
363 | name = "idna"
364 | version = "3.10"
365 | description = "Internationalized Domain Names in Applications (IDNA)"
366 | optional = false
367 | python-versions = ">=3.6"
368 | files = [
369 | {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"},
370 | {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
371 | ]
372 |
373 | [package.extras]
374 | all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
375 |
376 | [[package]]
377 | name = "iniconfig"
378 | version = "2.0.0"
379 | description = "brain-dead simple config-ini parsing"
380 | optional = false
381 | python-versions = ">=3.7"
382 | files = [
383 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
384 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
385 | ]
386 |
387 | [[package]]
388 | name = "jinja2"
389 | version = "3.1.6"
390 | description = "A very fast and expressive template engine."
391 | optional = false
392 | python-versions = ">=3.7"
393 | files = [
394 | {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"},
395 | {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"},
396 | ]
397 |
398 | [package.dependencies]
399 | MarkupSafe = ">=2.0"
400 |
401 | [package.extras]
402 | i18n = ["Babel (>=2.7)"]
403 |
404 | [[package]]
405 | name = "linkify-it-py"
406 | version = "2.0.3"
407 | description = "Links recognition library with FULL unicode support."
408 | optional = false
409 | python-versions = ">=3.7"
410 | files = [
411 | {file = "linkify-it-py-2.0.3.tar.gz", hash = "sha256:68cda27e162e9215c17d786649d1da0021a451bdc436ef9e0fa0ba5234b9b048"},
412 | {file = "linkify_it_py-2.0.3-py3-none-any.whl", hash = "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79"},
413 | ]
414 |
415 | [package.dependencies]
416 | uc-micro-py = "*"
417 |
418 | [package.extras]
419 | benchmark = ["pytest", "pytest-benchmark"]
420 | dev = ["black", "flake8", "isort", "pre-commit", "pyproject-flake8"]
421 | doc = ["myst-parser", "sphinx", "sphinx-book-theme"]
422 | test = ["coverage", "pytest", "pytest-cov"]
423 |
424 | [[package]]
425 | name = "markdown-it-py"
426 | version = "3.0.0"
427 | description = "Python port of markdown-it. Markdown parsing, done right!"
428 | optional = false
429 | python-versions = ">=3.8"
430 | files = [
431 | {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"},
432 | {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"},
433 | ]
434 |
435 | [package.dependencies]
436 | linkify-it-py = {version = ">=1,<3", optional = true, markers = "extra == \"linkify\""}
437 | mdit-py-plugins = {version = "*", optional = true, markers = "extra == \"plugins\""}
438 | mdurl = ">=0.1,<1.0"
439 |
440 | [package.extras]
441 | benchmarking = ["psutil", "pytest", "pytest-benchmark"]
442 | code-style = ["pre-commit (>=3.0,<4.0)"]
443 | compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"]
444 | linkify = ["linkify-it-py (>=1,<3)"]
445 | plugins = ["mdit-py-plugins"]
446 | profiling = ["gprof2dot"]
447 | rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"]
448 | testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
449 |
450 | [[package]]
451 | name = "markupsafe"
452 | version = "3.0.2"
453 | description = "Safely add untrusted strings to HTML/XML markup."
454 | optional = false
455 | python-versions = ">=3.9"
456 | files = [
457 | {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"},
458 | {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"},
459 | {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"},
460 | {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"},
461 | {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"},
462 | {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"},
463 | {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"},
464 | {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"},
465 | {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"},
466 | {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"},
467 | {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"},
468 | {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"},
469 | {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"},
470 | {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"},
471 | {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"},
472 | {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"},
473 | {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"},
474 | {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"},
475 | {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"},
476 | {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"},
477 | {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"},
478 | {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"},
479 | {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"},
480 | {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"},
481 | {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"},
482 | {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"},
483 | {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"},
484 | {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"},
485 | {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"},
486 | {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"},
487 | {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"},
488 | {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"},
489 | {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"},
490 | {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"},
491 | {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"},
492 | {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"},
493 | {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"},
494 | {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"},
495 | {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"},
496 | {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"},
497 | {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"},
498 | {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"},
499 | {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"},
500 | {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"},
501 | {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"},
502 | {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"},
503 | {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"},
504 | {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"},
505 | {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"},
506 | {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"},
507 | {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"},
508 | {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"},
509 | {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"},
510 | {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"},
511 | {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"},
512 | {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"},
513 | {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"},
514 | {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"},
515 | {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"},
516 | {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"},
517 | {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"},
518 | ]
519 |
520 | [[package]]
521 | name = "mdit-py-plugins"
522 | version = "0.4.2"
523 | description = "Collection of plugins for markdown-it-py"
524 | optional = false
525 | python-versions = ">=3.8"
526 | files = [
527 | {file = "mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636"},
528 | {file = "mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5"},
529 | ]
530 |
531 | [package.dependencies]
532 | markdown-it-py = ">=1.0.0,<4.0.0"
533 |
534 | [package.extras]
535 | code-style = ["pre-commit"]
536 | rtd = ["myst-parser", "sphinx-book-theme"]
537 | testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
538 |
539 | [[package]]
540 | name = "mdurl"
541 | version = "0.1.2"
542 | description = "Markdown URL utilities"
543 | optional = false
544 | python-versions = ">=3.7"
545 | files = [
546 | {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
547 | {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
548 | ]
549 |
550 | [[package]]
551 | name = "msgpack"
552 | version = "1.1.0"
553 | description = "MessagePack serializer"
554 | optional = false
555 | python-versions = ">=3.8"
556 | files = [
557 | {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7ad442d527a7e358a469faf43fda45aaf4ac3249c8310a82f0ccff9164e5dccd"},
558 | {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:74bed8f63f8f14d75eec75cf3d04ad581da6b914001b474a5d3cd3372c8cc27d"},
559 | {file = "msgpack-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:914571a2a5b4e7606997e169f64ce53a8b1e06f2cf2c3a7273aa106236d43dd5"},
560 | {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c921af52214dcbb75e6bdf6a661b23c3e6417f00c603dd2070bccb5c3ef499f5"},
561 | {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8ce0b22b890be5d252de90d0e0d119f363012027cf256185fc3d474c44b1b9e"},
562 | {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:73322a6cc57fcee3c0c57c4463d828e9428275fb85a27aa2aa1a92fdc42afd7b"},
563 | {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e1f3c3d21f7cf67bcf2da8e494d30a75e4cf60041d98b3f79875afb5b96f3a3f"},
564 | {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64fc9068d701233effd61b19efb1485587560b66fe57b3e50d29c5d78e7fef68"},
565 | {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:42f754515e0f683f9c79210a5d1cad631ec3d06cea5172214d2176a42e67e19b"},
566 | {file = "msgpack-1.1.0-cp310-cp310-win32.whl", hash = "sha256:3df7e6b05571b3814361e8464f9304c42d2196808e0119f55d0d3e62cd5ea044"},
567 | {file = "msgpack-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:685ec345eefc757a7c8af44a3032734a739f8c45d1b0ac45efc5d8977aa4720f"},
568 | {file = "msgpack-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d364a55082fb2a7416f6c63ae383fbd903adb5a6cf78c5b96cc6316dc1cedc7"},
569 | {file = "msgpack-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79ec007767b9b56860e0372085f8504db5d06bd6a327a335449508bbee9648fa"},
570 | {file = "msgpack-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6ad622bf7756d5a497d5b6836e7fc3752e2dd6f4c648e24b1803f6048596f701"},
571 | {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e59bca908d9ca0de3dc8684f21ebf9a690fe47b6be93236eb40b99af28b6ea6"},
572 | {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1da8f11a3dd397f0a32c76165cf0c4eb95b31013a94f6ecc0b280c05c91b59"},
573 | {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:452aff037287acb1d70a804ffd022b21fa2bb7c46bee884dbc864cc9024128a0"},
574 | {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8da4bf6d54ceed70e8861f833f83ce0814a2b72102e890cbdfe4b34764cdd66e"},
575 | {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:41c991beebf175faf352fb940bf2af9ad1fb77fd25f38d9142053914947cdbf6"},
576 | {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a52a1f3a5af7ba1c9ace055b659189f6c669cf3657095b50f9602af3a3ba0fe5"},
577 | {file = "msgpack-1.1.0-cp311-cp311-win32.whl", hash = "sha256:58638690ebd0a06427c5fe1a227bb6b8b9fdc2bd07701bec13c2335c82131a88"},
578 | {file = "msgpack-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd2906780f25c8ed5d7b323379f6138524ba793428db5d0e9d226d3fa6aa1788"},
579 | {file = "msgpack-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d46cf9e3705ea9485687aa4001a76e44748b609d260af21c4ceea7f2212a501d"},
580 | {file = "msgpack-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5dbad74103df937e1325cc4bfeaf57713be0b4f15e1c2da43ccdd836393e2ea2"},
581 | {file = "msgpack-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58dfc47f8b102da61e8949708b3eafc3504509a5728f8b4ddef84bd9e16ad420"},
582 | {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676e5be1b472909b2ee6356ff425ebedf5142427842aa06b4dfd5117d1ca8a2"},
583 | {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17fb65dd0bec285907f68b15734a993ad3fc94332b5bb21b0435846228de1f39"},
584 | {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a51abd48c6d8ac89e0cfd4fe177c61481aca2d5e7ba42044fd218cfd8ea9899f"},
585 | {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2137773500afa5494a61b1208619e3871f75f27b03bcfca7b3a7023284140247"},
586 | {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:398b713459fea610861c8a7b62a6fec1882759f308ae0795b5413ff6a160cf3c"},
587 | {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06f5fd2f6bb2a7914922d935d3b8bb4a7fff3a9a91cfce6d06c13bc42bec975b"},
588 | {file = "msgpack-1.1.0-cp312-cp312-win32.whl", hash = "sha256:ad33e8400e4ec17ba782f7b9cf868977d867ed784a1f5f2ab46e7ba53b6e1e1b"},
589 | {file = "msgpack-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:115a7af8ee9e8cddc10f87636767857e7e3717b7a2e97379dc2054712693e90f"},
590 | {file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:071603e2f0771c45ad9bc65719291c568d4edf120b44eb36324dcb02a13bfddf"},
591 | {file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0f92a83b84e7c0749e3f12821949d79485971f087604178026085f60ce109330"},
592 | {file = "msgpack-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1964df7b81285d00a84da4e70cb1383f2e665e0f1f2a7027e683956d04b734"},
593 | {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59caf6a4ed0d164055ccff8fe31eddc0ebc07cf7326a2aaa0dbf7a4001cd823e"},
594 | {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0907e1a7119b337971a689153665764adc34e89175f9a34793307d9def08e6ca"},
595 | {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65553c9b6da8166e819a6aa90ad15288599b340f91d18f60b2061f402b9a4915"},
596 | {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7a946a8992941fea80ed4beae6bff74ffd7ee129a90b4dd5cf9c476a30e9708d"},
597 | {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4b51405e36e075193bc051315dbf29168d6141ae2500ba8cd80a522964e31434"},
598 | {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4c01941fd2ff87c2a934ee6055bda4ed353a7846b8d4f341c428109e9fcde8c"},
599 | {file = "msgpack-1.1.0-cp313-cp313-win32.whl", hash = "sha256:7c9a35ce2c2573bada929e0b7b3576de647b0defbd25f5139dcdaba0ae35a4cc"},
600 | {file = "msgpack-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:bce7d9e614a04d0883af0b3d4d501171fbfca038f12c77fa838d9f198147a23f"},
601 | {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c40ffa9a15d74e05ba1fe2681ea33b9caffd886675412612d93ab17b58ea2fec"},
602 | {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1ba6136e650898082d9d5a5217d5906d1e138024f836ff48691784bbe1adf96"},
603 | {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0856a2b7e8dcb874be44fea031d22e5b3a19121be92a1e098f46068a11b0870"},
604 | {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:471e27a5787a2e3f974ba023f9e265a8c7cfd373632247deb225617e3100a3c7"},
605 | {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:646afc8102935a388ffc3914b336d22d1c2d6209c773f3eb5dd4d6d3b6f8c1cb"},
606 | {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:13599f8829cfbe0158f6456374e9eea9f44eee08076291771d8ae93eda56607f"},
607 | {file = "msgpack-1.1.0-cp38-cp38-win32.whl", hash = "sha256:8a84efb768fb968381e525eeeb3d92857e4985aacc39f3c47ffd00eb4509315b"},
608 | {file = "msgpack-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:879a7b7b0ad82481c52d3c7eb99bf6f0645dbdec5134a4bddbd16f3506947feb"},
609 | {file = "msgpack-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:53258eeb7a80fc46f62fd59c876957a2d0e15e6449a9e71842b6d24419d88ca1"},
610 | {file = "msgpack-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e7b853bbc44fb03fbdba34feb4bd414322180135e2cb5164f20ce1c9795ee48"},
611 | {file = "msgpack-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3e9b4936df53b970513eac1758f3882c88658a220b58dcc1e39606dccaaf01c"},
612 | {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46c34e99110762a76e3911fc923222472c9d681f1094096ac4102c18319e6468"},
613 | {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a706d1e74dd3dea05cb54580d9bd8b2880e9264856ce5068027eed09680aa74"},
614 | {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:534480ee5690ab3cbed89d4c8971a5c631b69a8c0883ecfea96c19118510c846"},
615 | {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8cf9e8c3a2153934a23ac160cc4cba0ec035f6867c8013cc6077a79823370346"},
616 | {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3180065ec2abbe13a4ad37688b61b99d7f9e012a535b930e0e683ad6bc30155b"},
617 | {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c5a91481a3cc573ac8c0d9aace09345d989dc4a0202b7fcb312c88c26d4e71a8"},
618 | {file = "msgpack-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f80bc7d47f76089633763f952e67f8214cb7b3ee6bfa489b3cb6a84cfac114cd"},
619 | {file = "msgpack-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:4d1b7ff2d6146e16e8bd665ac726a89c74163ef8cd39fa8c1087d4e52d3a2325"},
620 | {file = "msgpack-1.1.0.tar.gz", hash = "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e"},
621 | ]
622 |
623 | [[package]]
624 | name = "multidict"
625 | version = "6.1.0"
626 | description = "multidict implementation"
627 | optional = false
628 | python-versions = ">=3.8"
629 | files = [
630 | {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60"},
631 | {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1"},
632 | {file = "multidict-6.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a114d03b938376557927ab23f1e950827c3b893ccb94b62fd95d430fd0e5cf53"},
633 | {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1c416351ee6271b2f49b56ad7f308072f6f44b37118d69c2cad94f3fa8a40d5"},
634 | {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b5d83030255983181005e6cfbac1617ce9746b219bc2aad52201ad121226581"},
635 | {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e97b5e938051226dc025ec80980c285b053ffb1e25a3db2a3aa3bc046bf7f56"},
636 | {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d618649d4e70ac6efcbba75be98b26ef5078faad23592f9b51ca492953012429"},
637 | {file = "multidict-6.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10524ebd769727ac77ef2278390fb0068d83f3acb7773792a5080f2b0abf7748"},
638 | {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ff3827aef427c89a25cc96ded1759271a93603aba9fb977a6d264648ebf989db"},
639 | {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056"},
640 | {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f179dee3b863ab1c59580ff60f9d99f632f34ccb38bf67a33ec6b3ecadd0fd76"},
641 | {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:aaed8b0562be4a0876ee3b6946f6869b7bcdb571a5d1496683505944e268b160"},
642 | {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c8b88a2ccf5493b6c8da9076fb151ba106960a2df90c2633f342f120751a9e7"},
643 | {file = "multidict-6.1.0-cp310-cp310-win32.whl", hash = "sha256:4a9cb68166a34117d6646c0023c7b759bf197bee5ad4272f420a0141d7eb03a0"},
644 | {file = "multidict-6.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:20b9b5fbe0b88d0bdef2012ef7dee867f874b72528cf1d08f1d59b0e3850129d"},
645 | {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6"},
646 | {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156"},
647 | {file = "multidict-6.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb"},
648 | {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b"},
649 | {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72"},
650 | {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304"},
651 | {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351"},
652 | {file = "multidict-6.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb"},
653 | {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3"},
654 | {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399"},
655 | {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423"},
656 | {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3"},
657 | {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753"},
658 | {file = "multidict-6.1.0-cp311-cp311-win32.whl", hash = "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80"},
659 | {file = "multidict-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926"},
660 | {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa"},
661 | {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436"},
662 | {file = "multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761"},
663 | {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e"},
664 | {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef"},
665 | {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95"},
666 | {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925"},
667 | {file = "multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966"},
668 | {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305"},
669 | {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2"},
670 | {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2"},
671 | {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6"},
672 | {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3"},
673 | {file = "multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133"},
674 | {file = "multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1"},
675 | {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008"},
676 | {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f"},
677 | {file = "multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28"},
678 | {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b"},
679 | {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c"},
680 | {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3"},
681 | {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44"},
682 | {file = "multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2"},
683 | {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3"},
684 | {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa"},
685 | {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa"},
686 | {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4"},
687 | {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6"},
688 | {file = "multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81"},
689 | {file = "multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774"},
690 | {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:db7457bac39421addd0c8449933ac32d8042aae84a14911a757ae6ca3eef1392"},
691 | {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d094ddec350a2fb899fec68d8353c78233debde9b7d8b4beeafa70825f1c281a"},
692 | {file = "multidict-6.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5845c1fd4866bb5dd3125d89b90e57ed3138241540897de748cdf19de8a2fca2"},
693 | {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9079dfc6a70abe341f521f78405b8949f96db48da98aeb43f9907f342f627cdc"},
694 | {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3914f5aaa0f36d5d60e8ece6a308ee1c9784cd75ec8151062614657a114c4478"},
695 | {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c08be4f460903e5a9d0f76818db3250f12e9c344e79314d1d570fc69d7f4eae4"},
696 | {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d093be959277cb7dee84b801eb1af388b6ad3ca6a6b6bf1ed7585895789d027d"},
697 | {file = "multidict-6.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3702ea6872c5a2a4eeefa6ffd36b042e9773f05b1f37ae3ef7264b1163c2dcf6"},
698 | {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2090f6a85cafc5b2db085124d752757c9d251548cedabe9bd31afe6363e0aff2"},
699 | {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:f67f217af4b1ff66c68a87318012de788dd95fcfeb24cc889011f4e1c7454dfd"},
700 | {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:189f652a87e876098bbc67b4da1049afb5f5dfbaa310dd67c594b01c10388db6"},
701 | {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:6bb5992037f7a9eff7991ebe4273ea7f51f1c1c511e6a2ce511d0e7bdb754492"},
702 | {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f4c2b9e770c4e393876e35a7046879d195cd123b4f116d299d442b335bcd"},
703 | {file = "multidict-6.1.0-cp38-cp38-win32.whl", hash = "sha256:e27bbb6d14416713a8bd7aaa1313c0fc8d44ee48d74497a0ff4c3a1b6ccb5167"},
704 | {file = "multidict-6.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:22f3105d4fb15c8f57ff3959a58fcab6ce36814486500cd7485651230ad4d4ef"},
705 | {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4e18b656c5e844539d506a0a06432274d7bd52a7487e6828c63a63d69185626c"},
706 | {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a185f876e69897a6f3325c3f19f26a297fa058c5e456bfcff8015e9a27e83ae1"},
707 | {file = "multidict-6.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab7c4ceb38d91570a650dba194e1ca87c2b543488fe9309b4212694174fd539c"},
708 | {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e617fb6b0b6953fffd762669610c1c4ffd05632c138d61ac7e14ad187870669c"},
709 | {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16e5f4bf4e603eb1fdd5d8180f1a25f30056f22e55ce51fb3d6ad4ab29f7d96f"},
710 | {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c035da3f544b1882bac24115f3e2e8760f10a0107614fc9839fd232200b875"},
711 | {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:957cf8e4b6e123a9eea554fa7ebc85674674b713551de587eb318a2df3e00255"},
712 | {file = "multidict-6.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:483a6aea59cb89904e1ceabd2b47368b5600fb7de78a6e4a2c2987b2d256cf30"},
713 | {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:87701f25a2352e5bf7454caa64757642734da9f6b11384c1f9d1a8e699758057"},
714 | {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:682b987361e5fd7a139ed565e30d81fd81e9629acc7d925a205366877d8c8657"},
715 | {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce2186a7df133a9c895dea3331ddc5ddad42cdd0d1ea2f0a51e5d161e4762f28"},
716 | {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9f636b730f7e8cb19feb87094949ba54ee5357440b9658b2a32a5ce4bce53972"},
717 | {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:73eae06aa53af2ea5270cc066dcaf02cc60d2994bbb2c4ef5764949257d10f43"},
718 | {file = "multidict-6.1.0-cp39-cp39-win32.whl", hash = "sha256:1ca0083e80e791cffc6efce7660ad24af66c8d4079d2a750b29001b53ff59ada"},
719 | {file = "multidict-6.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:aa466da5b15ccea564bdab9c89175c762bc12825f4659c11227f515cee76fa4a"},
720 | {file = "multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506"},
721 | {file = "multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a"},
722 | ]
723 |
724 | [package.dependencies]
725 | typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""}
726 |
727 | [[package]]
728 | name = "mypy"
729 | version = "1.15.0"
730 | description = "Optional static typing for Python"
731 | optional = false
732 | python-versions = ">=3.9"
733 | files = [
734 | {file = "mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13"},
735 | {file = "mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559"},
736 | {file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b"},
737 | {file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3"},
738 | {file = "mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b"},
739 | {file = "mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828"},
740 | {file = "mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f"},
741 | {file = "mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5"},
742 | {file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e"},
743 | {file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c"},
744 | {file = "mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f"},
745 | {file = "mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f"},
746 | {file = "mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd"},
747 | {file = "mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f"},
748 | {file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464"},
749 | {file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee"},
750 | {file = "mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e"},
751 | {file = "mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22"},
752 | {file = "mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445"},
753 | {file = "mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d"},
754 | {file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5"},
755 | {file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036"},
756 | {file = "mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357"},
757 | {file = "mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf"},
758 | {file = "mypy-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e601a7fa172c2131bff456bb3ee08a88360760d0d2f8cbd7a75a65497e2df078"},
759 | {file = "mypy-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:712e962a6357634fef20412699a3655c610110e01cdaa6180acec7fc9f8513ba"},
760 | {file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95579473af29ab73a10bada2f9722856792a36ec5af5399b653aa28360290a5"},
761 | {file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f8722560a14cde92fdb1e31597760dc35f9f5524cce17836c0d22841830fd5b"},
762 | {file = "mypy-1.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fbb8da62dc352133d7d7ca90ed2fb0e9d42bb1a32724c287d3c76c58cbaa9c2"},
763 | {file = "mypy-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:d10d994b41fb3497719bbf866f227b3489048ea4bbbb5015357db306249f7980"},
764 | {file = "mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e"},
765 | {file = "mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43"},
766 | ]
767 |
768 | [package.dependencies]
769 | mypy_extensions = ">=1.0.0"
770 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
771 | typing_extensions = ">=4.6.0"
772 |
773 | [package.extras]
774 | dmypy = ["psutil (>=4.0)"]
775 | faster-cache = ["orjson"]
776 | install-types = ["pip"]
777 | mypyc = ["setuptools (>=50)"]
778 | reports = ["lxml"]
779 |
780 | [[package]]
781 | name = "mypy-extensions"
782 | version = "1.0.0"
783 | description = "Type system extensions for programs checked with the mypy type checker."
784 | optional = false
785 | python-versions = ">=3.5"
786 | files = [
787 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
788 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
789 | ]
790 |
791 | [[package]]
792 | name = "packaging"
793 | version = "24.2"
794 | description = "Core utilities for Python packages"
795 | optional = false
796 | python-versions = ">=3.8"
797 | files = [
798 | {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
799 | {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
800 | ]
801 |
802 | [[package]]
803 | name = "pathspec"
804 | version = "0.12.1"
805 | description = "Utility library for gitignore style pattern matching of file paths."
806 | optional = false
807 | python-versions = ">=3.8"
808 | files = [
809 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
810 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
811 | ]
812 |
813 | [[package]]
814 | name = "platformdirs"
815 | version = "4.3.6"
816 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
817 | optional = false
818 | python-versions = ">=3.8"
819 | files = [
820 | {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"},
821 | {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"},
822 | ]
823 |
824 | [package.extras]
825 | docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"]
826 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"]
827 | type = ["mypy (>=1.11.2)"]
828 |
829 | [[package]]
830 | name = "pluggy"
831 | version = "1.5.0"
832 | description = "plugin and hook calling mechanisms for python"
833 | optional = false
834 | python-versions = ">=3.8"
835 | files = [
836 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
837 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
838 | ]
839 |
840 | [package.extras]
841 | dev = ["pre-commit", "tox"]
842 | testing = ["pytest", "pytest-benchmark"]
843 |
844 | [[package]]
845 | name = "propcache"
846 | version = "0.3.0"
847 | description = "Accelerated property cache"
848 | optional = false
849 | python-versions = ">=3.9"
850 | files = [
851 | {file = "propcache-0.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:efa44f64c37cc30c9f05932c740a8b40ce359f51882c70883cc95feac842da4d"},
852 | {file = "propcache-0.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2383a17385d9800b6eb5855c2f05ee550f803878f344f58b6e194de08b96352c"},
853 | {file = "propcache-0.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3e7420211f5a65a54675fd860ea04173cde60a7cc20ccfbafcccd155225f8bc"},
854 | {file = "propcache-0.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3302c5287e504d23bb0e64d2a921d1eb4a03fb93a0a0aa3b53de059f5a5d737d"},
855 | {file = "propcache-0.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7e2e068a83552ddf7a39a99488bcba05ac13454fb205c847674da0352602082f"},
856 | {file = "propcache-0.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d913d36bdaf368637b4f88d554fb9cb9d53d6920b9c5563846555938d5450bf"},
857 | {file = "propcache-0.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ee1983728964d6070ab443399c476de93d5d741f71e8f6e7880a065f878e0b9"},
858 | {file = "propcache-0.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:36ca5e9a21822cc1746023e88f5c0af6fce3af3b85d4520efb1ce4221bed75cc"},
859 | {file = "propcache-0.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9ecde3671e62eeb99e977f5221abcf40c208f69b5eb986b061ccec317c82ebd0"},
860 | {file = "propcache-0.3.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:d383bf5e045d7f9d239b38e6acadd7b7fdf6c0087259a84ae3475d18e9a2ae8b"},
861 | {file = "propcache-0.3.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:8cb625bcb5add899cb8ba7bf716ec1d3e8f7cdea9b0713fa99eadf73b6d4986f"},
862 | {file = "propcache-0.3.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5fa159dcee5dba00c1def3231c249cf261185189205073bde13797e57dd7540a"},
863 | {file = "propcache-0.3.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a7080b0159ce05f179cfac592cda1a82898ca9cd097dacf8ea20ae33474fbb25"},
864 | {file = "propcache-0.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ed7161bccab7696a473fe7ddb619c1d75963732b37da4618ba12e60899fefe4f"},
865 | {file = "propcache-0.3.0-cp310-cp310-win32.whl", hash = "sha256:bf0d9a171908f32d54f651648c7290397b8792f4303821c42a74e7805bfb813c"},
866 | {file = "propcache-0.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:42924dc0c9d73e49908e35bbdec87adedd651ea24c53c29cac103ede0ea1d340"},
867 | {file = "propcache-0.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9ddd49258610499aab83b4f5b61b32e11fce873586282a0e972e5ab3bcadee51"},
868 | {file = "propcache-0.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2578541776769b500bada3f8a4eeaf944530516b6e90c089aa368266ed70c49e"},
869 | {file = "propcache-0.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8074c5dd61c8a3e915fa8fc04754fa55cfa5978200d2daa1e2d4294c1f136aa"},
870 | {file = "propcache-0.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b58229a844931bca61b3a20efd2be2a2acb4ad1622fc026504309a6883686fbf"},
871 | {file = "propcache-0.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e45377d5d6fefe1677da2a2c07b024a6dac782088e37c0b1efea4cfe2b1be19b"},
872 | {file = "propcache-0.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ec5060592d83454e8063e487696ac3783cc48c9a329498bafae0d972bc7816c9"},
873 | {file = "propcache-0.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15010f29fbed80e711db272909a074dc79858c6d28e2915704cfc487a8ac89c6"},
874 | {file = "propcache-0.3.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a254537b9b696ede293bfdbc0a65200e8e4507bc9f37831e2a0318a9b333c85c"},
875 | {file = "propcache-0.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2b975528998de037dfbc10144b8aed9b8dd5a99ec547f14d1cb7c5665a43f075"},
876 | {file = "propcache-0.3.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:19d36bb351ad5554ff20f2ae75f88ce205b0748c38b146c75628577020351e3c"},
877 | {file = "propcache-0.3.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6032231d4a5abd67c7f71168fd64a47b6b451fbcb91c8397c2f7610e67683810"},
878 | {file = "propcache-0.3.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6985a593417cdbc94c7f9c3403747335e450c1599da1647a5af76539672464d3"},
879 | {file = "propcache-0.3.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6a1948df1bb1d56b5e7b0553c0fa04fd0e320997ae99689488201f19fa90d2e7"},
880 | {file = "propcache-0.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8319293e85feadbbfe2150a5659dbc2ebc4afdeaf7d98936fb9a2f2ba0d4c35c"},
881 | {file = "propcache-0.3.0-cp311-cp311-win32.whl", hash = "sha256:63f26258a163c34542c24808f03d734b338da66ba91f410a703e505c8485791d"},
882 | {file = "propcache-0.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:cacea77ef7a2195f04f9279297684955e3d1ae4241092ff0cfcef532bb7a1c32"},
883 | {file = "propcache-0.3.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e53d19c2bf7d0d1e6998a7e693c7e87300dd971808e6618964621ccd0e01fe4e"},
884 | {file = "propcache-0.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a61a68d630e812b67b5bf097ab84e2cd79b48c792857dc10ba8a223f5b06a2af"},
885 | {file = "propcache-0.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fb91d20fa2d3b13deea98a690534697742029f4fb83673a3501ae6e3746508b5"},
886 | {file = "propcache-0.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67054e47c01b7b349b94ed0840ccae075449503cf1fdd0a1fdd98ab5ddc2667b"},
887 | {file = "propcache-0.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:997e7b8f173a391987df40f3b52c423e5850be6f6df0dcfb5376365440b56667"},
888 | {file = "propcache-0.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d663fd71491dde7dfdfc899d13a067a94198e90695b4321084c6e450743b8c7"},
889 | {file = "propcache-0.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8884ba1a0fe7210b775106b25850f5e5a9dc3c840d1ae9924ee6ea2eb3acbfe7"},
890 | {file = "propcache-0.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa806bbc13eac1ab6291ed21ecd2dd426063ca5417dd507e6be58de20e58dfcf"},
891 | {file = "propcache-0.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6f4d7a7c0aff92e8354cceca6fe223973ddf08401047920df0fcb24be2bd5138"},
892 | {file = "propcache-0.3.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:9be90eebc9842a93ef8335291f57b3b7488ac24f70df96a6034a13cb58e6ff86"},
893 | {file = "propcache-0.3.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bf15fc0b45914d9d1b706f7c9c4f66f2b7b053e9517e40123e137e8ca8958b3d"},
894 | {file = "propcache-0.3.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5a16167118677d94bb48bfcd91e420088854eb0737b76ec374b91498fb77a70e"},
895 | {file = "propcache-0.3.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:41de3da5458edd5678b0f6ff66691507f9885f5fe6a0fb99a5d10d10c0fd2d64"},
896 | {file = "propcache-0.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:728af36011bb5d344c4fe4af79cfe186729efb649d2f8b395d1572fb088a996c"},
897 | {file = "propcache-0.3.0-cp312-cp312-win32.whl", hash = "sha256:6b5b7fd6ee7b54e01759f2044f936dcf7dea6e7585f35490f7ca0420fe723c0d"},
898 | {file = "propcache-0.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:2d15bc27163cd4df433e75f546b9ac31c1ba7b0b128bfb1b90df19082466ff57"},
899 | {file = "propcache-0.3.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a2b9bf8c79b660d0ca1ad95e587818c30ccdb11f787657458d6f26a1ea18c568"},
900 | {file = "propcache-0.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b0c1a133d42c6fc1f5fbcf5c91331657a1ff822e87989bf4a6e2e39b818d0ee9"},
901 | {file = "propcache-0.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bb2f144c6d98bb5cbc94adeb0447cfd4c0f991341baa68eee3f3b0c9c0e83767"},
902 | {file = "propcache-0.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1323cd04d6e92150bcc79d0174ce347ed4b349d748b9358fd2e497b121e03c8"},
903 | {file = "propcache-0.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b812b3cb6caacd072276ac0492d249f210006c57726b6484a1e1805b3cfeea0"},
904 | {file = "propcache-0.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:742840d1d0438eb7ea4280f3347598f507a199a35a08294afdcc560c3739989d"},
905 | {file = "propcache-0.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c6e7e4f9167fddc438cd653d826f2222222564daed4116a02a184b464d3ef05"},
906 | {file = "propcache-0.3.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a94ffc66738da99232ddffcf7910e0f69e2bbe3a0802e54426dbf0714e1c2ffe"},
907 | {file = "propcache-0.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c6ec957025bf32b15cbc6b67afe233c65b30005e4c55fe5768e4bb518d712f1"},
908 | {file = "propcache-0.3.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:549722908de62aa0b47a78b90531c022fa6e139f9166be634f667ff45632cc92"},
909 | {file = "propcache-0.3.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5d62c4f6706bff5d8a52fd51fec6069bef69e7202ed481486c0bc3874912c787"},
910 | {file = "propcache-0.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:24c04f8fbf60094c531667b8207acbae54146661657a1b1be6d3ca7773b7a545"},
911 | {file = "propcache-0.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7c5f5290799a3f6539cc5e6f474c3e5c5fbeba74a5e1e5be75587746a940d51e"},
912 | {file = "propcache-0.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4fa0e7c9c3cf7c276d4f6ab9af8adddc127d04e0fcabede315904d2ff76db626"},
913 | {file = "propcache-0.3.0-cp313-cp313-win32.whl", hash = "sha256:ee0bd3a7b2e184e88d25c9baa6a9dc609ba25b76daae942edfb14499ac7ec374"},
914 | {file = "propcache-0.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1c8f7d896a16da9455f882870a507567d4f58c53504dc2d4b1e1d386dfe4588a"},
915 | {file = "propcache-0.3.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e560fd75aaf3e5693b91bcaddd8b314f4d57e99aef8a6c6dc692f935cc1e6bbf"},
916 | {file = "propcache-0.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:65a37714b8ad9aba5780325228598a5b16c47ba0f8aeb3dc0514701e4413d7c0"},
917 | {file = "propcache-0.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:07700939b2cbd67bfb3b76a12e1412405d71019df00ca5697ce75e5ef789d829"},
918 | {file = "propcache-0.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c0fdbdf6983526e269e5a8d53b7ae3622dd6998468821d660d0daf72779aefa"},
919 | {file = "propcache-0.3.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:794c3dd744fad478b6232289c866c25406ecdfc47e294618bdf1697e69bd64a6"},
920 | {file = "propcache-0.3.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4544699674faf66fb6b4473a1518ae4999c1b614f0b8297b1cef96bac25381db"},
921 | {file = "propcache-0.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fddb8870bdb83456a489ab67c6b3040a8d5a55069aa6f72f9d872235fbc52f54"},
922 | {file = "propcache-0.3.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f857034dc68d5ceb30fb60afb6ff2103087aea10a01b613985610e007053a121"},
923 | {file = "propcache-0.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:02df07041e0820cacc8f739510078f2aadcfd3fc57eaeeb16d5ded85c872c89e"},
924 | {file = "propcache-0.3.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f47d52fd9b2ac418c4890aad2f6d21a6b96183c98021f0a48497a904199f006e"},
925 | {file = "propcache-0.3.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9ff4e9ecb6e4b363430edf2c6e50173a63e0820e549918adef70515f87ced19a"},
926 | {file = "propcache-0.3.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ecc2920630283e0783c22e2ac94427f8cca29a04cfdf331467d4f661f4072dac"},
927 | {file = "propcache-0.3.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:c441c841e82c5ba7a85ad25986014be8d7849c3cfbdb6004541873505929a74e"},
928 | {file = "propcache-0.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6c929916cbdb540d3407c66f19f73387f43e7c12fa318a66f64ac99da601bcdf"},
929 | {file = "propcache-0.3.0-cp313-cp313t-win32.whl", hash = "sha256:0c3e893c4464ebd751b44ae76c12c5f5c1e4f6cbd6fbf67e3783cd93ad221863"},
930 | {file = "propcache-0.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:75e872573220d1ee2305b35c9813626e620768248425f58798413e9c39741f46"},
931 | {file = "propcache-0.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:03c091bb752349402f23ee43bb2bff6bd80ccab7c9df6b88ad4322258d6960fc"},
932 | {file = "propcache-0.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:46ed02532cb66612d42ae5c3929b5e98ae330ea0f3900bc66ec5f4862069519b"},
933 | {file = "propcache-0.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11ae6a8a01b8a4dc79093b5d3ca2c8a4436f5ee251a9840d7790dccbd96cb649"},
934 | {file = "propcache-0.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df03cd88f95b1b99052b52b1bb92173229d7a674df0ab06d2b25765ee8404bce"},
935 | {file = "propcache-0.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03acd9ff19021bd0567582ac88f821b66883e158274183b9e5586f678984f8fe"},
936 | {file = "propcache-0.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd54895e4ae7d32f1e3dd91261df46ee7483a735017dc6f987904f194aa5fd14"},
937 | {file = "propcache-0.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26a67e5c04e3119594d8cfae517f4b9330c395df07ea65eab16f3d559b7068fe"},
938 | {file = "propcache-0.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee25f1ac091def37c4b59d192bbe3a206298feeb89132a470325bf76ad122a1e"},
939 | {file = "propcache-0.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:58e6d2a5a7cb3e5f166fd58e71e9a4ff504be9dc61b88167e75f835da5764d07"},
940 | {file = "propcache-0.3.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:be90c94570840939fecedf99fa72839aed70b0ced449b415c85e01ae67422c90"},
941 | {file = "propcache-0.3.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:49ea05212a529c2caffe411e25a59308b07d6e10bf2505d77da72891f9a05641"},
942 | {file = "propcache-0.3.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:119e244ab40f70a98c91906d4c1f4c5f2e68bd0b14e7ab0a06922038fae8a20f"},
943 | {file = "propcache-0.3.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:507c5357a8d8b4593b97fb669c50598f4e6cccbbf77e22fa9598aba78292b4d7"},
944 | {file = "propcache-0.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8526b0941ec5a40220fc4dfde76aed58808e2b309c03e9fa8e2260083ef7157f"},
945 | {file = "propcache-0.3.0-cp39-cp39-win32.whl", hash = "sha256:7cedd25e5f678f7738da38037435b340694ab34d424938041aa630d8bac42663"},
946 | {file = "propcache-0.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:bf4298f366ca7e1ad1d21bbb58300a6985015909964077afd37559084590c929"},
947 | {file = "propcache-0.3.0-py3-none-any.whl", hash = "sha256:67dda3c7325691c2081510e92c561f465ba61b975f481735aefdfc845d2cd043"},
948 | {file = "propcache-0.3.0.tar.gz", hash = "sha256:a8fd93de4e1d278046345f49e2238cdb298589325849b2645d4a94c53faeffc5"},
949 | ]
950 |
951 | [[package]]
952 | name = "pygments"
953 | version = "2.19.1"
954 | description = "Pygments is a syntax highlighting package written in Python."
955 | optional = false
956 | python-versions = ">=3.8"
957 | files = [
958 | {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"},
959 | {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"},
960 | ]
961 |
962 | [package.extras]
963 | windows-terminal = ["colorama (>=0.4.6)"]
964 |
965 | [[package]]
966 | name = "pytest"
967 | version = "8.3.5"
968 | description = "pytest: simple powerful testing with Python"
969 | optional = false
970 | python-versions = ">=3.8"
971 | files = [
972 | {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"},
973 | {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"},
974 | ]
975 |
976 | [package.dependencies]
977 | colorama = {version = "*", markers = "sys_platform == \"win32\""}
978 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
979 | iniconfig = "*"
980 | packaging = "*"
981 | pluggy = ">=1.5,<2"
982 | tomli = {version = ">=1", markers = "python_version < \"3.11\""}
983 |
984 | [package.extras]
985 | dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
986 |
987 | [[package]]
988 | name = "rich"
989 | version = "13.9.4"
990 | description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
991 | optional = false
992 | python-versions = ">=3.8.0"
993 | files = [
994 | {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"},
995 | {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"},
996 | ]
997 |
998 | [package.dependencies]
999 | markdown-it-py = ">=2.2.0"
1000 | pygments = ">=2.13.0,<3.0.0"
1001 | typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""}
1002 |
1003 | [package.extras]
1004 | jupyter = ["ipywidgets (>=7.5.1,<9)"]
1005 |
1006 | [[package]]
1007 | name = "shellingham"
1008 | version = "1.5.4"
1009 | description = "Tool to Detect Surrounding Shell"
1010 | optional = true
1011 | python-versions = ">=3.7"
1012 | files = [
1013 | {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"},
1014 | {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"},
1015 | ]
1016 |
1017 | [[package]]
1018 | name = "textual"
1019 | version = "2.1.2"
1020 | description = "Modern Text User Interface framework"
1021 | optional = false
1022 | python-versions = "<4.0.0,>=3.8.1"
1023 | files = [
1024 | {file = "textual-2.1.2-py3-none-any.whl", hash = "sha256:95f37f49e930838e721bba8612f62114d410a3019665b6142adabc14c2fb9611"},
1025 | {file = "textual-2.1.2.tar.gz", hash = "sha256:aae3f9fde00c7440be00e3c3ac189e02d014f5298afdc32132f93480f9e09146"},
1026 | ]
1027 |
1028 | [package.dependencies]
1029 | markdown-it-py = {version = ">=2.1.0", extras = ["linkify", "plugins"]}
1030 | platformdirs = ">=3.6.0,<5"
1031 | rich = ">=13.3.3"
1032 | typing-extensions = ">=4.4.0,<5.0.0"
1033 |
1034 | [package.extras]
1035 | syntax = ["tree-sitter (>=0.23.0)", "tree-sitter-bash (>=0.23.0)", "tree-sitter-css (>=0.23.0)", "tree-sitter-go (>=0.23.0)", "tree-sitter-html (>=0.23.0)", "tree-sitter-java (>=0.23.0)", "tree-sitter-javascript (>=0.23.0)", "tree-sitter-json (>=0.24.0)", "tree-sitter-markdown (>=0.3.0)", "tree-sitter-python (>=0.23.0)", "tree-sitter-regex (>=0.24.0)", "tree-sitter-rust (>=0.23.0)", "tree-sitter-sql (>=0.3.0,<0.3.8)", "tree-sitter-toml (>=0.6.0)", "tree-sitter-xml (>=0.7.0)", "tree-sitter-yaml (>=0.6.0)"]
1036 |
1037 | [[package]]
1038 | name = "textual-dev"
1039 | version = "1.7.0"
1040 | description = "Development tools for working with Textual"
1041 | optional = false
1042 | python-versions = "<4.0.0,>=3.8.1"
1043 | files = [
1044 | {file = "textual_dev-1.7.0-py3-none-any.whl", hash = "sha256:a93a846aeb6a06edb7808504d9c301565f7f4bf2e7046d56583ed755af356c8d"},
1045 | {file = "textual_dev-1.7.0.tar.gz", hash = "sha256:bf1a50eaaff4cd6a863535dd53f06dbbd62617c371604f66f56de3908220ccd5"},
1046 | ]
1047 |
1048 | [package.dependencies]
1049 | aiohttp = ">=3.8.1"
1050 | click = ">=8.1.2"
1051 | msgpack = ">=1.0.3"
1052 | textual = ">=0.86.2"
1053 | textual_serve = ">=1.0.3"
1054 | typing-extensions = ">=4.4.0,<5.0.0"
1055 |
1056 | [[package]]
1057 | name = "textual-serve"
1058 | version = "1.1.1"
1059 | description = "Turn your Textual TUIs in to web applications"
1060 | optional = false
1061 | python-versions = ">=3.8"
1062 | files = [
1063 | {file = "textual_serve-1.1.1-py3-none-any.whl", hash = "sha256:568782f1c0e60e3f7039d9121e1cb5c2f4ca1aaf6d6bd7aeb833d5763a534cb2"},
1064 | {file = "textual_serve-1.1.1.tar.gz", hash = "sha256:71c662472c462e5e368defc660ee6e8eae3bfda88ca40c050c55474686eb0c54"},
1065 | ]
1066 |
1067 | [package.dependencies]
1068 | aiohttp = ">=3.9.5"
1069 | aiohttp-jinja2 = ">=1.6"
1070 | jinja2 = ">=3.1.4"
1071 | rich = "*"
1072 | textual = ">=0.66.0"
1073 |
1074 | [[package]]
1075 | name = "tomli"
1076 | version = "2.2.1"
1077 | description = "A lil' TOML parser"
1078 | optional = false
1079 | python-versions = ">=3.8"
1080 | files = [
1081 | {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"},
1082 | {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"},
1083 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"},
1084 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"},
1085 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"},
1086 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"},
1087 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"},
1088 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"},
1089 | {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"},
1090 | {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"},
1091 | {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"},
1092 | {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"},
1093 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"},
1094 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"},
1095 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"},
1096 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"},
1097 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"},
1098 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"},
1099 | {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"},
1100 | {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"},
1101 | {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"},
1102 | {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"},
1103 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"},
1104 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"},
1105 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"},
1106 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"},
1107 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"},
1108 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"},
1109 | {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"},
1110 | {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"},
1111 | {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"},
1112 | {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"},
1113 | ]
1114 |
1115 | [[package]]
1116 | name = "typer"
1117 | version = "0.15.2"
1118 | description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
1119 | optional = true
1120 | python-versions = ">=3.7"
1121 | files = [
1122 | {file = "typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc"},
1123 | {file = "typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5"},
1124 | ]
1125 |
1126 | [package.dependencies]
1127 | click = ">=8.0.0"
1128 | rich = ">=10.11.0"
1129 | shellingham = ">=1.3.0"
1130 | typing-extensions = ">=3.7.4.3"
1131 |
1132 | [[package]]
1133 | name = "typing-extensions"
1134 | version = "4.12.2"
1135 | description = "Backported and Experimental Type Hints for Python 3.8+"
1136 | optional = false
1137 | python-versions = ">=3.8"
1138 | files = [
1139 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
1140 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
1141 | ]
1142 |
1143 | [[package]]
1144 | name = "uc-micro-py"
1145 | version = "1.0.3"
1146 | description = "Micro subset of unicode data files for linkify-it-py projects."
1147 | optional = false
1148 | python-versions = ">=3.7"
1149 | files = [
1150 | {file = "uc-micro-py-1.0.3.tar.gz", hash = "sha256:d321b92cff673ec58027c04015fcaa8bb1e005478643ff4a500882eaab88c48a"},
1151 | {file = "uc_micro_py-1.0.3-py3-none-any.whl", hash = "sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5"},
1152 | ]
1153 |
1154 | [package.extras]
1155 | test = ["coverage", "pytest", "pytest-cov"]
1156 |
1157 | [[package]]
1158 | name = "yarl"
1159 | version = "1.18.3"
1160 | description = "Yet another URL library"
1161 | optional = false
1162 | python-versions = ">=3.9"
1163 | files = [
1164 | {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7df647e8edd71f000a5208fe6ff8c382a1de8edfbccdbbfe649d263de07d8c34"},
1165 | {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c69697d3adff5aa4f874b19c0e4ed65180ceed6318ec856ebc423aa5850d84f7"},
1166 | {file = "yarl-1.18.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:602d98f2c2d929f8e697ed274fbadc09902c4025c5a9963bf4e9edfc3ab6f7ed"},
1167 | {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c654d5207c78e0bd6d749f6dae1dcbbfde3403ad3a4b11f3c5544d9906969dde"},
1168 | {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5094d9206c64181d0f6e76ebd8fb2f8fe274950a63890ee9e0ebfd58bf9d787b"},
1169 | {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35098b24e0327fc4ebdc8ffe336cee0a87a700c24ffed13161af80124b7dc8e5"},
1170 | {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3236da9272872443f81fedc389bace88408f64f89f75d1bdb2256069a8730ccc"},
1171 | {file = "yarl-1.18.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2c08cc9b16f4f4bc522771d96734c7901e7ebef70c6c5c35dd0f10845270bcd"},
1172 | {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80316a8bd5109320d38eef8833ccf5f89608c9107d02d2a7f985f98ed6876990"},
1173 | {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1e1cc06da1491e6734f0ea1e6294ce00792193c463350626571c287c9a704db"},
1174 | {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fea09ca13323376a2fdfb353a5fa2e59f90cd18d7ca4eaa1fd31f0a8b4f91e62"},
1175 | {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e3b9fd71836999aad54084906f8663dffcd2a7fb5cdafd6c37713b2e72be1760"},
1176 | {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:757e81cae69244257d125ff31663249b3013b5dc0a8520d73694aed497fb195b"},
1177 | {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b1771de9944d875f1b98a745bc547e684b863abf8f8287da8466cf470ef52690"},
1178 | {file = "yarl-1.18.3-cp310-cp310-win32.whl", hash = "sha256:8874027a53e3aea659a6d62751800cf6e63314c160fd607489ba5c2edd753cf6"},
1179 | {file = "yarl-1.18.3-cp310-cp310-win_amd64.whl", hash = "sha256:93b2e109287f93db79210f86deb6b9bbb81ac32fc97236b16f7433db7fc437d8"},
1180 | {file = "yarl-1.18.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8503ad47387b8ebd39cbbbdf0bf113e17330ffd339ba1144074da24c545f0069"},
1181 | {file = "yarl-1.18.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:02ddb6756f8f4517a2d5e99d8b2f272488e18dd0bfbc802f31c16c6c20f22193"},
1182 | {file = "yarl-1.18.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:67a283dd2882ac98cc6318384f565bffc751ab564605959df4752d42483ad889"},
1183 | {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d980e0325b6eddc81331d3f4551e2a333999fb176fd153e075c6d1c2530aa8a8"},
1184 | {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b643562c12680b01e17239be267bc306bbc6aac1f34f6444d1bded0c5ce438ca"},
1185 | {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c017a3b6df3a1bd45b9fa49a0f54005e53fbcad16633870104b66fa1a30a29d8"},
1186 | {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75674776d96d7b851b6498f17824ba17849d790a44d282929c42dbb77d4f17ae"},
1187 | {file = "yarl-1.18.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ccaa3a4b521b780a7e771cc336a2dba389a0861592bbce09a476190bb0c8b4b3"},
1188 | {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2d06d3005e668744e11ed80812e61efd77d70bb7f03e33c1598c301eea20efbb"},
1189 | {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:9d41beda9dc97ca9ab0b9888cb71f7539124bc05df02c0cff6e5acc5a19dcc6e"},
1190 | {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ba23302c0c61a9999784e73809427c9dbedd79f66a13d84ad1b1943802eaaf59"},
1191 | {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6748dbf9bfa5ba1afcc7556b71cda0d7ce5f24768043a02a58846e4a443d808d"},
1192 | {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0b0cad37311123211dc91eadcb322ef4d4a66008d3e1bdc404808992260e1a0e"},
1193 | {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fb2171a4486bb075316ee754c6d8382ea6eb8b399d4ec62fde2b591f879778a"},
1194 | {file = "yarl-1.18.3-cp311-cp311-win32.whl", hash = "sha256:61b1a825a13bef4a5f10b1885245377d3cd0bf87cba068e1d9a88c2ae36880e1"},
1195 | {file = "yarl-1.18.3-cp311-cp311-win_amd64.whl", hash = "sha256:b9d60031cf568c627d028239693fd718025719c02c9f55df0a53e587aab951b5"},
1196 | {file = "yarl-1.18.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50"},
1197 | {file = "yarl-1.18.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576"},
1198 | {file = "yarl-1.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640"},
1199 | {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2"},
1200 | {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75"},
1201 | {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512"},
1202 | {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba"},
1203 | {file = "yarl-1.18.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb"},
1204 | {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272"},
1205 | {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6"},
1206 | {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e"},
1207 | {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb"},
1208 | {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393"},
1209 | {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285"},
1210 | {file = "yarl-1.18.3-cp312-cp312-win32.whl", hash = "sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2"},
1211 | {file = "yarl-1.18.3-cp312-cp312-win_amd64.whl", hash = "sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477"},
1212 | {file = "yarl-1.18.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb"},
1213 | {file = "yarl-1.18.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa"},
1214 | {file = "yarl-1.18.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782"},
1215 | {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a19f62ff30117e706ebc9090b8ecc79aeb77d0b1f5ec10d2d27a12bc9f66d0"},
1216 | {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e17c9361d46a4d5addf777c6dd5eab0715a7684c2f11b88c67ac37edfba6c482"},
1217 | {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a74a13a4c857a84a845505fd2d68e54826a2cd01935a96efb1e9d86c728e186"},
1218 | {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41f7ce59d6ee7741af71d82020346af364949314ed3d87553763a2df1829cc58"},
1219 | {file = "yarl-1.18.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f52a265001d830bc425f82ca9eabda94a64a4d753b07d623a9f2863fde532b53"},
1220 | {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:82123d0c954dc58db301f5021a01854a85bf1f3bb7d12ae0c01afc414a882ca2"},
1221 | {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2ec9bbba33b2d00999af4631a3397d1fd78290c48e2a3e52d8dd72db3a067ac8"},
1222 | {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbd6748e8ab9b41171bb95c6142faf068f5ef1511935a0aa07025438dd9a9bc1"},
1223 | {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:877d209b6aebeb5b16c42cbb377f5f94d9e556626b1bfff66d7b0d115be88d0a"},
1224 | {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b464c4ab4bfcb41e3bfd3f1c26600d038376c2de3297760dfe064d2cb7ea8e10"},
1225 | {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d39d351e7faf01483cc7ff7c0213c412e38e5a340238826be7e0e4da450fdc8"},
1226 | {file = "yarl-1.18.3-cp313-cp313-win32.whl", hash = "sha256:61ee62ead9b68b9123ec24bc866cbef297dd266175d53296e2db5e7f797f902d"},
1227 | {file = "yarl-1.18.3-cp313-cp313-win_amd64.whl", hash = "sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c"},
1228 | {file = "yarl-1.18.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:61e5e68cb65ac8f547f6b5ef933f510134a6bf31bb178be428994b0cb46c2a04"},
1229 | {file = "yarl-1.18.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe57328fbc1bfd0bd0514470ac692630f3901c0ee39052ae47acd1d90a436719"},
1230 | {file = "yarl-1.18.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a440a2a624683108a1b454705ecd7afc1c3438a08e890a1513d468671d90a04e"},
1231 | {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09c7907c8548bcd6ab860e5f513e727c53b4a714f459b084f6580b49fa1b9cee"},
1232 | {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b4f6450109834af88cb4cc5ecddfc5380ebb9c228695afc11915a0bf82116789"},
1233 | {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9ca04806f3be0ac6d558fffc2fdf8fcef767e0489d2684a21912cc4ed0cd1b8"},
1234 | {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77a6e85b90a7641d2e07184df5557132a337f136250caafc9ccaa4a2a998ca2c"},
1235 | {file = "yarl-1.18.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6333c5a377c8e2f5fae35e7b8f145c617b02c939d04110c76f29ee3676b5f9a5"},
1236 | {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0b3c92fa08759dbf12b3a59579a4096ba9af8dd344d9a813fc7f5070d86bbab1"},
1237 | {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:4ac515b860c36becb81bb84b667466885096b5fc85596948548b667da3bf9f24"},
1238 | {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:045b8482ce9483ada4f3f23b3774f4e1bf4f23a2d5c912ed5170f68efb053318"},
1239 | {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:a4bb030cf46a434ec0225bddbebd4b89e6471814ca851abb8696170adb163985"},
1240 | {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:54d6921f07555713b9300bee9c50fb46e57e2e639027089b1d795ecd9f7fa910"},
1241 | {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1d407181cfa6e70077df3377938c08012d18893f9f20e92f7d2f314a437c30b1"},
1242 | {file = "yarl-1.18.3-cp39-cp39-win32.whl", hash = "sha256:ac36703a585e0929b032fbaab0707b75dc12703766d0b53486eabd5139ebadd5"},
1243 | {file = "yarl-1.18.3-cp39-cp39-win_amd64.whl", hash = "sha256:ba87babd629f8af77f557b61e49e7c7cac36f22f871156b91e10a6e9d4f829e9"},
1244 | {file = "yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b"},
1245 | {file = "yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1"},
1246 | ]
1247 |
1248 | [package.dependencies]
1249 | idna = ">=2.0"
1250 | multidict = ">=4.0"
1251 | propcache = ">=0.2.0"
1252 |
1253 | [extras]
1254 | typer = ["typer"]
1255 |
1256 | [metadata]
1257 | lock-version = "2.0"
1258 | python-versions = "^3.9.1"
1259 | content-hash = "eee2773017f18be2b18039d4b8e84b279eb846f4690452ff21fcad8565b26a5f"
1260 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.poetry]
2 | name = "trogon"
3 | version = "0.6.0"
4 | description = "Automatically generate a Textual TUI for your Click CLI"
5 | authors = ["Darren Burns "]
6 | readme = "README.md"
7 | packages = [{include = "trogon"}]
8 | license = "MIT"
9 | homepage = "https://github.com/Textualize/trogon"
10 | classifiers = [
11 | "Development Status :: 4 - Beta",
12 | "Environment :: Console",
13 | "Intended Audience :: Developers",
14 | "Intended Audience :: End Users/Desktop",
15 | "Intended Audience :: Information Technology",
16 | "Intended Audience :: Other Audience",
17 | "Operating System :: MacOS",
18 | "Operating System :: Microsoft :: Windows :: Windows 10",
19 | "Operating System :: Microsoft :: Windows :: Windows 11",
20 | "Operating System :: POSIX :: Linux",
21 | "Programming Language :: Python :: 3.9",
22 | "Programming Language :: Python :: 3.10",
23 | "Programming Language :: Python :: 3.11",
24 | "Programming Language :: Python :: 3.12",
25 | "Programming Language :: Python :: 3.13",
26 | "Topic :: Software Development :: Documentation",
27 | ]
28 |
29 | [tool.poetry.dependencies]
30 | python = "^3.9.1"
31 | textual = ">=2.1.2"
32 | click = ">=8.0.0"
33 | typer = {version = ">=0.9.0", optional = true}
34 |
35 | [tool.poetry.extras]
36 | typer = ["typer"]
37 |
38 | [tool.poetry.group.dev.dependencies]
39 | mypy = "^1.2.0"
40 | black = "^24.3.0"
41 | pytest = ">=8.0.0"
42 | textual-dev = ">=1.0"
43 |
44 | [build-system]
45 | requires = ["poetry-core"]
46 | build-backend = "poetry.core.masonry.api"
47 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Textualize/trogon/eaa9e68c403cae6aff0a80957d8876b284fd76b0/tests/__init__.py
--------------------------------------------------------------------------------
/tests/test_help.py:
--------------------------------------------------------------------------------
1 | import click
2 | from click.testing import CliRunner
3 | import re
4 | from trogon import tui
5 |
6 |
7 | @tui()
8 | @click.group()
9 | def default():
10 | pass
11 |
12 |
13 | @tui(command="custom")
14 | @click.group()
15 | def custom_command():
16 | pass
17 |
18 |
19 | @tui(help="Custom help")
20 | @click.group()
21 | def custom_help():
22 | pass
23 |
24 |
25 | def test_default_help():
26 | result = CliRunner().invoke(default, ["--help"])
27 | assert re.search(r"tui\s+Open Textual TUI", result.output) is not None
28 |
29 |
30 | def test_custom_command():
31 | result = CliRunner().invoke(custom_command, ["--help"])
32 | assert re.search(r"custom\s+Open Textual TUI", result.output) is not None
33 |
34 |
35 | def test_custom_help():
36 | result = CliRunner().invoke(custom_help, ["--help"])
37 | assert re.search(r"tui\s+Custom help", result.output) is not None
38 |
--------------------------------------------------------------------------------
/tests/test_run_command.py:
--------------------------------------------------------------------------------
1 | import click
2 | import pytest
3 |
4 | from trogon.introspect import (
5 | CommandSchema,
6 | OptionSchema,
7 | ArgumentSchema,
8 | CommandName, MultiValueParamData,
9 | )
10 | from trogon.run_command import UserCommandData, UserOptionData, UserArgumentData
11 |
12 |
13 | @pytest.fixture
14 | def command_schema():
15 | return CommandSchema(
16 | name=CommandName("test"),
17 | arguments=[
18 | ArgumentSchema(name="arg1", type=click.INT, required=False, default=MultiValueParamData([(123,)])),
19 | ],
20 | options=[
21 | OptionSchema(
22 | name=["--option1"], type=click.STRING, required=False, default=MultiValueParamData([("default1",)])
23 | ),
24 | OptionSchema(
25 | name=["--option2"], type=click.INT, required=False, default=MultiValueParamData([(42,)])
26 | ),
27 | ],
28 | subcommands={},
29 | function=lambda: 1,
30 | )
31 |
32 |
33 | @pytest.fixture
34 | def command_schema_with_subcommand(command_schema):
35 | command_schema.subcommands = {
36 | "sub": CommandSchema(
37 | name=CommandName("sub"),
38 | options=[
39 | OptionSchema(
40 | name=["--sub-option"], type=click.BOOL, required=False, default=MultiValueParamData([(False,)])
41 | )
42 | ],
43 | arguments=[],
44 | function=lambda: 2,
45 | )
46 | }
47 | return command_schema
48 |
49 |
50 | @pytest.fixture
51 | def user_command_data_no_subcommand():
52 | return UserCommandData(
53 | name=CommandName("test"),
54 | options=[
55 | UserOptionData(name="--option1", value=("value1",),
56 | option_schema=OptionSchema(name=["--option1", "-o1"], type=click.STRING)),
57 | UserOptionData(name="--option2", value=("42",),
58 | option_schema=OptionSchema(name=["--option2", "-o2"], type=click.STRING)),
59 | ],
60 | arguments=[
61 | UserArgumentData(name="arg1", value=("123",), argument_schema=ArgumentSchema("arg1", click.INT)),
62 | ],
63 | )
64 |
65 |
66 | @pytest.fixture
67 | def user_command_data_with_subcommand(user_command_data_no_subcommand):
68 | return UserCommandData(
69 | name=CommandName("test"),
70 | options=user_command_data_no_subcommand.options,
71 | arguments=user_command_data_no_subcommand.arguments,
72 | subcommand=UserCommandData(
73 | name=CommandName("sub"),
74 | options=[
75 | UserOptionData(name="--sub-option", value=("True",),
76 | option_schema=OptionSchema(name=["--sub-option"], type=click.BOOL))
77 | ],
78 | arguments=[],
79 | ),
80 | )
81 |
82 |
83 | def test_to_cli_args_no_subcommand(user_command_data_no_subcommand):
84 | cli_args = user_command_data_no_subcommand.to_cli_args(True)
85 | assert cli_args == ["test", "--option1", "value1", "--option2", "42", "123"]
86 |
87 |
88 | def test_to_cli_args_with_subcommand(user_command_data_with_subcommand):
89 | cli_args = user_command_data_with_subcommand.to_cli_args(True)
90 | assert cli_args == [
91 | "test",
92 | "--option1",
93 | "value1",
94 | "--option2",
95 | "42",
96 | "123",
97 | "sub",
98 | "--sub-option",
99 | "True",
100 | ]
101 |
102 |
103 | def test_to_cli_string_no_subcommand(user_command_data_no_subcommand):
104 | cli_string = user_command_data_no_subcommand.to_cli_string(True)
105 |
106 | assert cli_string.plain == "test --option1 value1 --option2 42 123"
107 |
108 |
109 | def test_to_cli_string_with_subcommand(user_command_data_with_subcommand):
110 | cli_string = user_command_data_with_subcommand.to_cli_string(True)
111 |
112 | assert cli_string.plain == "test --option1 value1 --option2 42 123 sub --sub-option True"
113 |
--------------------------------------------------------------------------------
/trogon/__init__.py:
--------------------------------------------------------------------------------
1 | from trogon.trogon import Trogon, tui
2 |
3 | __all__ = ["tui", "Trogon"]
4 |
--------------------------------------------------------------------------------
/trogon/constants.py:
--------------------------------------------------------------------------------
1 | APP_TITLE = "Trogon"
2 | PACKAGE_NAME = "trogon"
3 | TEXTUAL_URL = "https://github.com/textualize/textual"
4 | ORGANIZATION_NAME = "T"
5 |
--------------------------------------------------------------------------------
/trogon/detect_run_string.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import os
4 | import shlex
5 | import sys
6 | from types import ModuleType
7 |
8 |
9 | def get_orig_argv() -> list[str]:
10 | """Polyfil for orig_argv"""
11 | if hasattr(sys, "orig_argv"):
12 | return sys.orig_argv
13 | import ctypes
14 |
15 | _argv = ctypes.POINTER(ctypes.c_wchar_p)()
16 | _argc = ctypes.c_int()
17 |
18 | ctypes.pythonapi.Py_GetArgcArgv(ctypes.byref(_argc), ctypes.byref(_argv))
19 |
20 | argv = _argv[: _argc.value]
21 | return argv
22 |
23 |
24 | def detect_run_string(_main: ModuleType = sys.modules["__main__"]) -> str:
25 | """This is a slightly modified version of a function from Click."""
26 | path = sys.argv[0]
27 |
28 | # The value of __package__ indicates how Python was called. It may
29 | # not exist if a setuptools script is installed as an egg. It may be
30 | # set incorrectly for entry points created with pip on Windows.
31 | if getattr(_main, "__package__", None) is None or (
32 | os.name == "nt"
33 | and _main.__package__ == ""
34 | and not os.path.exists(path)
35 | and os.path.exists(f"{path}.exe")
36 | ):
37 | # Executed a file, like "python app.py".
38 | file_path = shlex.quote(os.path.basename(path))
39 | argv = get_orig_argv()
40 | if argv[0] == "python":
41 | prefix = f"{argv[0]} "
42 | else:
43 | prefix = ""
44 | return f"{prefix}{file_path}"
45 |
46 | # Executed a module, like "python -m example".
47 | # Rewritten by Python from "-m script" to "/path/to/script.py".
48 | # Need to look at main module to determine how it was executed.
49 | py_module = _main.__package__
50 | name = os.path.splitext(os.path.basename(path))[0]
51 |
52 | # A submodule like "example.cli".
53 | if name != "__main__":
54 | py_module = f"{py_module}.{name}"
55 |
56 | return f"python -m {py_module.lstrip('.')}"
57 |
--------------------------------------------------------------------------------
/trogon/introspect.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import uuid
4 | from dataclasses import dataclass, field
5 | from typing import Any, Callable, Sequence, NewType
6 |
7 | import click
8 | from click import BaseCommand, ParamType
9 |
10 |
11 | def generate_unique_id():
12 | return f"id_{str(uuid.uuid4())[:8]}"
13 |
14 |
15 | @dataclass
16 | class MultiValueParamData:
17 | values: list[tuple[int | float | str]]
18 |
19 | @staticmethod
20 | def process_cli_option(value) -> "MultiValueParamData":
21 | if value is None:
22 | value = MultiValueParamData([])
23 | elif isinstance(value, tuple):
24 | value = MultiValueParamData([value])
25 | elif isinstance(value, list):
26 | processed_list = [
27 | (item,) if not isinstance(item, tuple) else item for item in value
28 | ]
29 | value = MultiValueParamData(processed_list)
30 | else:
31 | value = MultiValueParamData([(value,)])
32 |
33 | return value
34 |
35 |
36 | @dataclass
37 | class OptionSchema:
38 | name: list[str]
39 | type: ParamType
40 | default: MultiValueParamData | None = None
41 | required: bool = False
42 | is_flag: bool = False
43 | is_boolean_flag: bool = False
44 | flag_value: Any = ""
45 | opts: list[str] = field(default_factory=list)
46 | counting: bool = False
47 | secondary_opts: list[str] = field(default_factory=list)
48 | key: str | tuple[str] = field(default_factory=generate_unique_id)
49 | help: str | None = None
50 | choices: Sequence[str] | None = None
51 | multiple: bool = False
52 | multi_value: bool = False
53 | nargs: int = 1
54 |
55 | def __post_init__(self):
56 | self.multi_value = isinstance(self.type, click.Tuple)
57 |
58 |
59 | @dataclass
60 | class ArgumentSchema:
61 | name: str
62 | type: str
63 | required: bool = False
64 | key: str = field(default_factory=generate_unique_id)
65 | default: MultiValueParamData | None = None
66 | choices: Sequence[str] | None = None
67 | multiple: bool = False
68 | nargs: int = 1
69 |
70 |
71 | @dataclass
72 | class CommandSchema:
73 | name: CommandName
74 | function: Callable[..., Any | None]
75 | key: str = field(default_factory=generate_unique_id)
76 | docstring: str | None = None
77 | options: list[OptionSchema] = field(default_factory=list)
78 | arguments: list[ArgumentSchema] = field(default_factory=list)
79 | subcommands: dict["CommandName", "CommandSchema"] = field(default_factory=dict)
80 | parent: "CommandSchema | None" = None
81 | is_group: bool = False
82 |
83 | @property
84 | def path_from_root(self) -> list["CommandSchema"]:
85 | node = self
86 | path: list[CommandSchema] = [self]
87 | while True:
88 | node = node.parent
89 | if node is None:
90 | break
91 | path.append(node)
92 | return list(reversed(path))
93 |
94 |
95 | def introspect_click_app(app: BaseCommand) -> dict[CommandName, CommandSchema]:
96 | """
97 | Introspect a Click application and build a data structure containing
98 | information about all commands, options, arguments, and subcommands,
99 | including the docstrings and command function references.
100 |
101 | This function recursively processes each command and its subcommands
102 | (if any), creating a nested dictionary that includes details about
103 | options, arguments, and subcommands, as well as the docstrings and
104 | command function references.
105 |
106 | Args:
107 | app (click.BaseCommand): The Click application's top-level group or command instance.
108 |
109 | Returns:
110 | Dict[str, CommandData]: A nested dictionary containing the Click application's
111 | structure. The structure is defined by the CommandData TypedDict and its related
112 | TypedDicts (OptionData and ArgumentData).
113 | """
114 |
115 | def process_command(
116 | cmd_name: CommandName, cmd_obj: click.Command, parent=None
117 | ) -> CommandSchema:
118 | cmd_data = CommandSchema(
119 | name=cmd_name,
120 | docstring=cmd_obj.help,
121 | function=cmd_obj.callback,
122 | options=[],
123 | arguments=[],
124 | subcommands={},
125 | parent=parent,
126 | is_group=isinstance(cmd_obj, click.Group),
127 | )
128 |
129 | for param in cmd_obj.params:
130 | default = MultiValueParamData.process_cli_option(param.default)
131 | if isinstance(param, (click.Option, click.core.Group)):
132 | option_data = OptionSchema(
133 | name=param.opts,
134 | type=param.type,
135 | is_flag=param.is_flag,
136 | is_boolean_flag=param.is_bool_flag,
137 | flag_value=param.flag_value,
138 | counting=param.count,
139 | opts=param.opts,
140 | secondary_opts=param.secondary_opts,
141 | required=param.required,
142 | default=default,
143 | help=param.help,
144 | multiple=param.multiple,
145 | nargs=param.nargs,
146 | )
147 | if isinstance(param.type, click.Choice):
148 | option_data.choices = param.type.choices
149 | cmd_data.options.append(option_data)
150 | elif isinstance(param, click.Argument):
151 | argument_data = ArgumentSchema(
152 | name=param.name,
153 | type=param.type,
154 | required=param.required,
155 | multiple=param.multiple,
156 | default=default,
157 | nargs=param.nargs,
158 | )
159 | if isinstance(param.type, click.Choice):
160 | argument_data.choices = param.type.choices
161 | cmd_data.arguments.append(argument_data)
162 |
163 | if isinstance(cmd_obj, click.core.Group):
164 | for subcmd_name, subcmd_obj in cmd_obj.commands.items():
165 | cmd_data.subcommands[CommandName(subcmd_name)] = process_command(
166 | CommandName(subcmd_name), subcmd_obj, parent=cmd_data
167 | )
168 |
169 | return cmd_data
170 |
171 | data: dict[CommandName, CommandSchema] = {}
172 |
173 | # Special case for the root group
174 | if isinstance(app, click.Group):
175 | root_cmd_name = CommandName("root")
176 | data[root_cmd_name] = process_command(root_cmd_name, app)
177 | app = data[root_cmd_name]
178 |
179 | if isinstance(app, click.Group):
180 | for cmd_name, cmd_obj in app.commands.items():
181 | data[CommandName(cmd_name)] = process_command(
182 | CommandName(cmd_name), cmd_obj
183 | )
184 | elif isinstance(app, click.Command):
185 | cmd_name = CommandName(app.name)
186 | data[cmd_name] = process_command(cmd_name, app)
187 |
188 | return data
189 |
190 |
191 | CommandName = NewType("CommandName", str)
192 |
--------------------------------------------------------------------------------
/trogon/run_command.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import itertools
4 | import shlex
5 | from collections import defaultdict
6 | from dataclasses import dataclass, field
7 | from typing import Any, List, Optional
8 |
9 | from rich.text import Text
10 |
11 | from trogon.introspect import (
12 | CommandSchema,
13 | CommandName,
14 | OptionSchema,
15 | ArgumentSchema,
16 | MultiValueParamData,
17 | )
18 | from trogon.widgets.parameter_controls import ValueNotSupplied
19 |
20 |
21 | @dataclass
22 | class UserOptionData:
23 | """
24 | A dataclass to store user input for a specific option.
25 |
26 | Attributes:
27 | name: The name of the option.
28 | value: The user-provided value for the option.
29 | option_schema: The schema corresponding to this option.
30 | """
31 |
32 | name: str | list[str]
33 | value: tuple[Any] # Multi-value options will be tuple length > 1
34 | option_schema: OptionSchema
35 |
36 | @property
37 | def string_name(self) -> str:
38 | if isinstance(self.name, str):
39 | return self.name
40 | else:
41 | return self.name[0]
42 |
43 |
44 | @dataclass
45 | class UserArgumentData:
46 | """
47 | A dataclass to store user input for a specific argument.
48 |
49 | Attributes:
50 | name: The name of the argument.
51 | value: The user-provided value for the argument.
52 | argument_schema: The schema corresponding to this argument.
53 | """
54 |
55 | name: str
56 | value: tuple[Any]
57 | argument_schema: ArgumentSchema
58 |
59 |
60 | @dataclass
61 | class UserCommandData:
62 | """
63 | A dataclass to store user input for a command, its options, and arguments.
64 |
65 | Attributes:
66 | name: The name of the command.
67 | options: A list of UserOptionData instances representing the user input for the command's options.
68 | arguments: A list of UserArgumentData instances representing the user input for the command's arguments.
69 | subcommand: An optional UserCommandData instance representing a subcommand of the current command.
70 | Since commands can be nested (i.e. subcommands), this may be processed recursively.
71 | """
72 |
73 | name: CommandName
74 | options: list[UserOptionData] = field(default_factory=list)
75 | arguments: list[UserArgumentData] = field(default_factory=list)
76 | subcommand: UserCommandData | None = None
77 | parent: UserCommandData | None = None
78 | command_schema: CommandSchema | None = None
79 |
80 | def to_cli_args(self, include_root_command: bool = False) -> list[str]:
81 | """
82 | Generates a list of strings representing the CLI invocation based on the user input data.
83 |
84 | Returns:
85 | A list of strings that can be passed to subprocess.run to execute the command.
86 | """
87 | cli_args = self._to_cli_args()
88 | if not include_root_command:
89 | cli_args = cli_args[1:]
90 |
91 | return cli_args
92 |
93 | def _to_cli_args(self) -> list[str]:
94 | args: list[str] = [self.name]
95 |
96 | multiples: dict[str, list[tuple[str]]] = defaultdict(list)
97 | multiples_schemas: dict[str, OptionSchema] = {}
98 |
99 | for option in self.options:
100 | if option.option_schema.multiple:
101 | # We need to gather the items for the same option,
102 | # compare them to the default, then display them all
103 | # if they aren't equivalent to the default.
104 | multiples[option.string_name].append(option.value)
105 | multiples_schemas[option.string_name] = option.option_schema
106 | else:
107 | value_data: list[tuple[Any]] = MultiValueParamData.process_cli_option(
108 | option.value
109 | ).values
110 |
111 | if option.option_schema.default is not None:
112 | default_data: list[tuple[Any]] = option.option_schema.default.values
113 | else:
114 | default_data = [tuple()]
115 |
116 | flattened_values = sorted(itertools.chain.from_iterable(value_data))
117 | flattened_defaults = sorted(itertools.chain.from_iterable(default_data))
118 |
119 | # If the user has supplied values (any values are not None), then
120 | # we don't display the value.
121 | values_supplied = any(
122 | value != ValueNotSupplied() for value in flattened_values
123 | )
124 | values_are_defaults = list(map(str, flattened_values)) == list(
125 | map(str, flattened_defaults)
126 | )
127 |
128 | # If the user has supplied values, and they're not the default values,
129 | # then we want to display them in the command string...
130 | if values_supplied and not values_are_defaults:
131 | if isinstance(option.name, str):
132 | option_name = option.name
133 | else:
134 | if option.option_schema.counting:
135 | # For count options, we use the shortest name, e.g. use
136 | # -v instead of --verbose.
137 | option_name = min(option.name, key=len)
138 | else:
139 | # Use the option with the longest name, since
140 | # it's probably the most descriptive (use --verbose over -v)
141 | option_name = max(option.name, key=len)
142 |
143 | is_true_bool = value_data == [(True,)]
144 |
145 | is_flag = option.option_schema.is_flag
146 | secondary_opts = option.option_schema.secondary_opts
147 |
148 | if is_flag:
149 | # If the option is specified like `--thing/--not-thing`,
150 | # then secondary_opts will contain `--not-thing`, and if the
151 | # value is False, we should use that.
152 | if is_true_bool:
153 | args.append(option_name)
154 | else:
155 | if secondary_opts:
156 | longest_secondary_name = max(secondary_opts, key=len)
157 | args.append(longest_secondary_name)
158 | else:
159 | if not option.option_schema.counting:
160 | # Although buried away a little, this branch here is
161 | # actually the nominal case... single value options e.g.
162 | # `--foo bar`.
163 | args.append(option_name)
164 | for subvalue_tuple in value_data:
165 | args.extend(subvalue_tuple)
166 | else:
167 | # Get the value of the counting option
168 | count = next(itertools.chain.from_iterable(value_data), 1)
169 | try:
170 | count = int(count)
171 | except ValueError:
172 | # TODO: Not sure if this is the right thing to do
173 | count = 1
174 | count = max(1, min(count, 5))
175 | if option_name.startswith("--"):
176 | args.extend([option_name] * count)
177 | else:
178 | clean_option_name = option_name.lstrip("-")
179 | args.append(f"-{clean_option_name * count}")
180 |
181 | for option_name, values in multiples.items():
182 | # Check if the values given for this option differ from the default
183 | defaults = multiples_schemas[option_name].default or []
184 | default_values = list(itertools.chain.from_iterable(defaults.values))
185 | supplied_defaults = [
186 | value for value in default_values if value != ValueNotSupplied()
187 | ]
188 | supplied_defaults = list(map(str, supplied_defaults))
189 | supplied_defaults = sorted(supplied_defaults)
190 |
191 | supplied_values = list(itertools.chain.from_iterable(values))
192 | supplied_values = [
193 | value for value in supplied_values if value != ValueNotSupplied()
194 | ]
195 | supplied_values = list(map(str, supplied_values))
196 | supplied_values = sorted(supplied_values)
197 |
198 | values_are_defaults = supplied_values == supplied_defaults
199 | values_supplied = any(
200 | value != ValueNotSupplied() for value in supplied_values
201 | )
202 |
203 | # If the user has supplied any non-default values, include them...
204 | if values_supplied and not values_are_defaults:
205 | for value_data in values:
206 | if not all(value == ValueNotSupplied() for value in value_data):
207 | args.append(option_name)
208 | args.extend(v for v in value_data)
209 |
210 | for argument in self.arguments:
211 | this_arg_values = argument.value
212 | for argument_value in this_arg_values:
213 | if argument_value != ValueNotSupplied():
214 | args.append(argument_value)
215 |
216 | if self.subcommand:
217 | args.extend(self.subcommand._to_cli_args())
218 |
219 | return args
220 |
221 | def to_cli_string(self, include_root_command: bool = False) -> Text:
222 | """
223 | Generates a string representing the CLI invocation as if typed directly into the
224 | command line.
225 |
226 | Returns:
227 | A string representing the command invocation.
228 | """
229 | args = self.to_cli_args(include_root_command=include_root_command)
230 |
231 | text_renderables: list[Text] = []
232 | for arg in args:
233 | text_renderables.append(
234 | Text(shlex.quote(str(arg)))
235 | if arg != ValueNotSupplied()
236 | else Text("???", style="bold black on red")
237 | )
238 | return Text(" ").join(text_renderables)
239 |
--------------------------------------------------------------------------------
/trogon/trogon.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import os
4 | import shlex
5 | from importlib import metadata # type: ignore
6 | from pathlib import Path
7 | from typing import Any
8 | from webbrowser import open as open_url
9 |
10 | import click
11 | from rich.console import Console
12 | from rich.highlighter import ReprHighlighter
13 | from rich.text import Text
14 | from textual import log, events, on
15 | from textual.app import ComposeResult, App, AutopilotCallbackType
16 | from textual.binding import Binding
17 | from textual.containers import Vertical, Horizontal, VerticalScroll
18 | from textual.css.query import NoMatches
19 | from textual.screen import Screen
20 | from textual.widgets import (
21 | Tree,
22 | Label,
23 | Static,
24 | Button,
25 | Footer,
26 | )
27 | from textual.widgets.tree import TreeNode
28 |
29 | from trogon.detect_run_string import detect_run_string
30 | from trogon.introspect import (
31 | introspect_click_app,
32 | CommandSchema,
33 | CommandName,
34 | )
35 | from trogon.run_command import UserCommandData
36 | from trogon.widgets.command_info import CommandInfo
37 | from trogon.widgets.command_tree import CommandTree
38 | from trogon.widgets.form import CommandForm
39 | from trogon.widgets.multiple_choice import NonFocusableVerticalScroll
40 |
41 |
42 | class CommandBuilder(Screen[None]):
43 | COMPONENT_CLASSES = {"version-string", "prompt", "command-name-syntax"}
44 |
45 | BINDINGS = [
46 | Binding(key="ctrl+r", action="close_and_run", description="Close & Run"),
47 | Binding(
48 | key="ctrl+t",
49 | action="app.focus_command_tree",
50 | description="Focus Command Tree",
51 | ),
52 | Binding(
53 | key="ctrl+o", action="app.show_command_info", description="Command Info"
54 | ),
55 | Binding(key="ctrl+s", action="app.focus('search')", description="Search"),
56 | Binding(key="f1", action="about", description="About"),
57 | ]
58 |
59 | def __init__(
60 | self,
61 | cli: click.BaseCommand,
62 | click_app_name: str,
63 | command_name: str,
64 | name: str | None = None,
65 | id: str | None = None,
66 | classes: str | None = None,
67 | ):
68 | super().__init__(name, id, classes)
69 | self.command_data: UserCommandData = UserCommandData(CommandName("_default"))
70 | self.cli = cli
71 | self.is_grouped_cli = isinstance(cli, click.Group)
72 | self.command_schemas = introspect_click_app(cli)
73 | self.click_app_name = click_app_name
74 | self.command_name = command_name
75 |
76 | try:
77 | self.version = metadata.version(self.click_app_name)
78 | except Exception:
79 | self.version = None
80 |
81 | self.highlighter = ReprHighlighter()
82 |
83 | def compose(self) -> ComposeResult:
84 | tree = CommandTree("Commands", self.command_schemas, self.command_name)
85 |
86 | title_parts = [Text(self.click_app_name, style="b")]
87 | if self.version:
88 | version_style = self.get_component_rich_style("version-string")
89 | title_parts.extend(["\n", (f"v{self.version}", version_style)])
90 |
91 | title = Text.assemble(*title_parts)
92 |
93 | sidebar = Vertical(
94 | Label(title, id="home-commands-label"),
95 | tree,
96 | id="home-sidebar",
97 | )
98 | if self.is_grouped_cli:
99 | # If the root of the click app is a Group instance, then
100 | # we display the command tree to users and focus it.
101 | tree.focus()
102 | else:
103 | # If the click app is structured using a single command,
104 | # there's no need for us to display the command tree.
105 | sidebar.display = False
106 |
107 | yield sidebar
108 |
109 | with Vertical(id="home-body"):
110 | with Horizontal(id="home-command-description-container") as vs:
111 | vs.can_focus = False
112 | yield Static(self.click_app_name or "", id="home-command-description")
113 |
114 | scrollable_body = VerticalScroll(
115 | Static(""),
116 | id="home-body-scroll",
117 | )
118 | scrollable_body.can_focus = False
119 | yield scrollable_body
120 | yield Horizontal(
121 | NonFocusableVerticalScroll(
122 | Static("", id="home-exec-preview-static"),
123 | id="home-exec-preview-container",
124 | ),
125 | # Vertical(
126 | # Button.success("Close & Run", id="home-exec-button"),
127 | # id="home-exec-preview-buttons",
128 | # ),
129 | id="home-exec-preview",
130 | )
131 |
132 | yield Footer()
133 |
134 | def action_close_and_run(self) -> None:
135 | self.app.execute_on_exit = True
136 | self.app.exit()
137 |
138 | def action_about(self) -> None:
139 | from .widgets.about import AboutDialog
140 |
141 | self.app.push_screen(AboutDialog())
142 |
143 | async def _refresh_command_form(self, node: TreeNode[CommandSchema]) -> None:
144 | selected_command = node.data
145 | if selected_command is None:
146 | return
147 |
148 | self.selected_command_schema = selected_command
149 | self._update_command_description(selected_command)
150 | self._update_execution_string_preview()
151 | await self._update_form_body(node)
152 |
153 | @on(Tree.NodeHighlighted)
154 | async def selected_command_changed(
155 | self, event: Tree.NodeHighlighted[CommandSchema]
156 | ) -> None:
157 | """When we highlight a node in the CommandTree, the main body of the home page updates
158 | to display a form specific to the highlighted command."""
159 | await self._refresh_command_form(event.node)
160 |
161 | @on(CommandForm.Changed)
162 | def update_command_data(self, event: CommandForm.Changed) -> None:
163 | self.command_data = event.command_data
164 | self._update_execution_string_preview()
165 |
166 | def _update_command_description(self, command: CommandSchema) -> None:
167 | """Update the description of the command at the bottom of the sidebar
168 | based on the currently selected node in the command tree."""
169 | description_box = self.query_one("#home-command-description", Static)
170 | description_text = command.docstring or ""
171 | description_text = description_text.lstrip()
172 | description_text = f"[b]{command.name}[/]\n{description_text}"
173 | description_box.update(description_text)
174 |
175 | def _update_execution_string_preview(self) -> None:
176 | """Update the preview box showing the command string to be executed"""
177 | command_name_syntax_style = self.get_component_rich_style("command-name-syntax")
178 | prefix = Text(f"{self.click_app_name} ", command_name_syntax_style)
179 | new_value = self.command_data.to_cli_string(include_root_command=False)
180 | highlighted_new_value = Text.assemble(prefix, self.highlighter(new_value))
181 | prompt_style = self.get_component_rich_style("prompt")
182 | preview_string = Text.assemble(("$ ", prompt_style), highlighted_new_value)
183 | self.query_one("#home-exec-preview-static", Static).update(preview_string)
184 |
185 | async def _update_form_body(self, node: TreeNode[CommandSchema]) -> None:
186 | # self.query_one(Pretty).update(node.data)
187 | parent = self.query_one("#home-body-scroll", VerticalScroll)
188 | for child in parent.children:
189 | await child.remove()
190 |
191 | # Process the metadata for this command and mount corresponding widgets
192 | command_schema = node.data
193 | command_form = CommandForm(
194 | command_schema=command_schema, command_schemas=self.command_schemas
195 | )
196 | await parent.mount(command_form)
197 | if not self.is_grouped_cli:
198 | command_form.focus()
199 |
200 |
201 | class Trogon(App[None]):
202 | CSS_PATH = Path(__file__).parent / "trogon.scss"
203 |
204 | def __init__(
205 | self,
206 | cli: click.Group | click.Command,
207 | app_name: str | None = None,
208 | command_name: str = "tui",
209 | click_context: click.Context | None = None,
210 | ) -> None:
211 | super().__init__()
212 | self.cli = cli
213 | self.post_run_command: list[str] = []
214 | self.is_grouped_cli = isinstance(cli, click.Group)
215 | self.execute_on_exit = False
216 | if app_name is None and click_context is not None:
217 | self.app_name = detect_run_string()
218 | else:
219 | self.app_name = app_name or "cli"
220 | self.command_name = command_name
221 |
222 | def get_default_screen(self) -> CommandBuilder:
223 | return CommandBuilder(self.cli, self.app_name, self.command_name)
224 |
225 | @on(Button.Pressed, "#home-exec-button")
226 | def on_button_pressed(self):
227 | self.execute_on_exit = True
228 | self.exit()
229 |
230 | def run(
231 | self,
232 | *args: Any,
233 | headless: bool = False,
234 | size: tuple[int, int] | None = None,
235 | auto_pilot: AutopilotCallbackType | None = None,
236 | **kwargs: Any,
237 | ) -> None:
238 | try:
239 | super().run(
240 | *args,
241 | headless=headless,
242 | size=size,
243 | auto_pilot=auto_pilot,
244 | **kwargs,
245 | )
246 | finally:
247 | if self.post_run_command:
248 | console = Console()
249 | if self.post_run_command and self.execute_on_exit:
250 | console.print(
251 | f"Running [b cyan]{self.app_name} {' '.join(shlex.quote(s) for s in self.post_run_command)}[/]"
252 | )
253 |
254 | split_app_name = shlex.split(self.app_name)
255 | program_name = shlex.split(self.app_name)[0]
256 | arguments = [*split_app_name, *self.post_run_command]
257 | os.execvp(program_name, arguments)
258 |
259 | @on(CommandForm.Changed)
260 | def update_command_to_run(self, event: CommandForm.Changed):
261 | include_root_command = not self.is_grouped_cli
262 | self.post_run_command = event.command_data.to_cli_args(include_root_command)
263 |
264 | def action_focus_command_tree(self) -> None:
265 | try:
266 | command_tree = self.query_one(CommandTree)
267 | except NoMatches:
268 | return
269 |
270 | command_tree.focus()
271 |
272 | def action_show_command_info(self) -> None:
273 | command_builder = self.query_one(CommandBuilder)
274 | self.push_screen(CommandInfo(command_builder.selected_command_schema))
275 |
276 | def action_visit(self, url: str) -> None:
277 | """Visit the given URL, via the operating system.
278 |
279 | Args:
280 | url: The URL to visit.
281 | """
282 | open_url(url)
283 |
284 |
285 | def tui(name: str | None = None, command: str = "tui", help: str = "Open Textual TUI."):
286 | def decorator(app: click.Group | click.Command):
287 | @click.pass_context
288 | def wrapped_tui(ctx, *args, **kwargs):
289 | Trogon(app, app_name=name, command_name=command, click_context=ctx).run()
290 |
291 | if isinstance(app, click.Group):
292 | app.command(name=command, help=help)(wrapped_tui)
293 | else:
294 | new_group = click.Group()
295 | new_group.add_command(app)
296 | new_group.command(name=command, help=help)(wrapped_tui)
297 | return new_group
298 |
299 | return app
300 |
301 | return decorator
302 |
--------------------------------------------------------------------------------
/trogon/trogon.scss:
--------------------------------------------------------------------------------
1 | * {
2 | scrollbar-color: $accent-lighten-2 20%;
3 | }
4 |
5 | #home-body {
6 | height: 100%;
7 | width: 1fr;
8 | }
9 |
10 | #home-body-scroll {
11 | width: 1fr;
12 | height: 1fr;
13 |
14 | }
15 |
16 | #home-exec-preview {
17 | dock: bottom;
18 | background: $panel-darken-2;
19 | height: auto;
20 | }
21 |
22 | #home-exec-preview-container {
23 | min-height: 3;
24 | max-height: 8;
25 | height: auto;
26 | scrollbar-size-vertical: 1;
27 | }
28 |
29 | #home-exec-button {
30 | height: 1fr;
31 | width: auto;
32 | }
33 |
34 | #home-exec-preview-static {
35 | width: 1fr;
36 | padding: 1 2;
37 | height: auto;
38 | }
39 |
40 | #home-exec-preview-buttons {
41 | dock: right;
42 | height: auto;
43 | width: auto;
44 | }
45 |
46 | #home-commands-label {
47 | color: $text;
48 | width: auto;
49 | height: 4;
50 | padding: 1 2;
51 | }
52 |
53 | #home-sidebar {
54 | background: $background 50%;
55 | dock: left;
56 | width: auto;
57 | }
58 |
59 | #home-command-description {
60 | width: 1fr;
61 | height: 2;
62 | }
63 |
64 | #home-command-description-container {
65 | dock: top;
66 | width: 1fr;
67 | height: 4;
68 | padding: 1 2;
69 | color: $text;
70 | background: $accent-lighten-2 5%;
71 | }
72 |
73 | CommandBuilder .version-string {
74 | color: $text-muted;
75 | background: $background 50%;
76 | text-style: italic;
77 | }
78 |
79 | CommandBuilder .prompt {
80 | color: $success;
81 | background: $background-darken-2;
82 | text-style: bold;
83 | }
84 |
85 | CommandBuilder .command-name-syntax {
86 | color: $accent-lighten-2;
87 | background: $panel-darken-2;
88 | text-style: italic bold;
89 | }
90 |
91 | CommandTree {
92 | background: $background-lighten-1;
93 | scrollbar-gutter: stable;
94 | width: auto;
95 | height: 1fr;
96 | padding: 0 1;
97 | margin-bottom: 1; /* why is this needed? */
98 | border: blank;
99 | }
100 |
101 |
102 | CommandTree:focus {
103 | border: tall $success;
104 | }
105 |
106 | CommandTree > .tree--cursor {
107 | background: $primary-darken-1;
108 | }
109 |
110 | CommandTree:focus > .tree--cursor {
111 | background: $primary-lighten-1;
112 | }
113 |
114 | CommandTree > .tree--guides {
115 | color: slategray;
116 | }
117 |
118 | CommandTree .group {
119 | color: $accent-lighten-2;
120 | text-style: bold;
121 | }
122 |
123 | CommandTree > .tree--guides-selected {
124 | color: $primary-lighten-2;
125 | }
126 |
127 | CommandTree > .tree--guides-hover {
128 | color: $primary-lighten-2;
129 | }
130 |
131 | ParameterControls {
132 | height: auto;
133 | }
134 |
135 | ControlGroup {
136 | height: auto;
137 | border: solid $panel-lighten-2;
138 |
139 | }
140 |
141 | ControlGroup.single-item {
142 | margin: 0;
143 | border: none;
144 | }
145 |
146 | ControlGroup.single-item:focus-within {
147 | margin: 0;
148 | border: none;
149 | }
150 |
151 | ControlGroup:focus-within {
152 | border: solid $primary;
153 | }
154 |
155 | ControlGroupsContainer {
156 | height: auto;
157 | }
158 |
159 | Pretty {
160 | height: auto;
161 | }
162 |
163 | .add-another-button {
164 | margin-right: 1;
165 | background: transparent;
166 | color: $success;
167 | border: none;
168 | background: $boost;
169 | height: 1;
170 | }
171 |
172 | .add-another-button:hover {
173 | background: transparent;
174 | }
175 |
176 | .add-another-button-container {
177 | width: 1fr;
178 | height: auto;
179 | align: right top;
180 | }
181 |
182 | CommandInfo {
183 | align: center middle;
184 | }
185 |
186 | .command-info-header-text {
187 | padding: 1 1 0 2;
188 | }
189 |
190 | CommandForm .command-form-filter-input {
191 | margin: 1 2;
192 | }
193 |
194 | $command-info-header-bg: $primary-darken-1;
195 |
196 | .command-info-header {
197 | dock: top;
198 | background: $command-info-header-bg;
199 | color: $text;
200 | height: auto;
201 | }
202 |
203 | .command-info-container {
204 | width: 80%;
205 | height: 60%;
206 | background: $panel;
207 | }
208 |
209 | #command-info-switcher {
210 | height: auto;
211 | }
212 |
213 | .command-info-text {
214 | padding: 2 4;
215 | height: auto;
216 | }
217 |
218 | .command-info-metadata {
219 | padding: 2 4;
220 | height: auto;
221 | }
222 |
223 | CommandInfo .title {
224 | background: $command-info-header-bg;
225 | text-style: bold;
226 | }
227 |
228 | CommandInfo .subtitle {
229 | background: $command-info-header-bg;
230 | color: $text-muted;
231 | }
232 |
233 | CommandInfo Tab {
234 | width: 1fr;
235 | }
236 |
237 | .command-info-tabs {
238 | width: 100%;
239 | }
240 |
241 | Tabs:focus .underline--bar {
242 | color: $accent-lighten-1;
243 | }
244 |
245 | Select.command-form-select {
246 |
247 | }
248 |
249 | Select.command-form-select SelectCurrent {
250 | border: tall transparent;
251 | }
252 |
253 | Select.command-form-select:focus SelectCurrent {
254 | border: tall $accent;
255 | }
256 |
257 | .command-form-multiple-choice {
258 | margin-left: 0;
259 | border: tall transparent;
260 | background: $boost;
261 | padding: 0 1;
262 | margin-top: 0;
263 | }
264 |
265 | .command-form-multiple-choice:focus-within {
266 |
267 | border: tall $accent;
268 | }
269 |
--------------------------------------------------------------------------------
/trogon/typer.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import click
4 |
5 | try:
6 | import typer
7 | except ImportError:
8 | raise ImportError(
9 | "The extra `trogon[typer]` is required to enable tui generation from Typer apps."
10 | )
11 |
12 | from trogon.trogon import Trogon
13 |
14 |
15 | def init_tui(app: typer.Typer, name: str | None = None):
16 | def wrapped_tui():
17 | Trogon(
18 | typer.main.get_group(app),
19 | app_name=name,
20 | click_context=click.get_current_context(),
21 | ).run()
22 |
23 | app.command("tui", help="Open Textual TUI.")(wrapped_tui)
24 |
25 | return app
26 |
--------------------------------------------------------------------------------
/trogon/widgets/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Textualize/trogon/eaa9e68c403cae6aff0a80957d8876b284fd76b0/trogon/widgets/__init__.py
--------------------------------------------------------------------------------
/trogon/widgets/about.py:
--------------------------------------------------------------------------------
1 | """Provides a base modal dialog for showing text to the user."""
2 |
3 | from rich.text import Text, TextType
4 | from textual.app import ComposeResult
5 | from textual.binding import Binding
6 | from textual.containers import Center, Vertical
7 | from textual.screen import ModalScreen
8 | from textual.widgets import Button, Static
9 | from textual.widgets._button import ButtonVariant
10 |
11 |
12 | from .. import constants
13 |
14 |
15 | class TextDialog(ModalScreen[None]):
16 | """Base modal dialog for showing information."""
17 |
18 | DEFAULT_CSS = """
19 | TextDialog {
20 | align: center middle;
21 | }
22 |
23 | TextDialog Center {
24 | width: 100%;
25 | }
26 |
27 | TextDialog > Vertical {
28 | background: $boost;
29 | min-width: 30%;
30 | width: auto;
31 | height: auto;
32 | border: round $primary;
33 | }
34 |
35 | TextDialog Static {
36 | width: auto;
37 | }
38 |
39 | TextDialog .spaced {
40 | padding: 1 4;
41 | }
42 |
43 | TextDialog #message {
44 | min-width: 100%;
45 | }
46 | """
47 | """Default CSS for the base text modal dialog."""
48 |
49 | BINDINGS = [
50 | Binding("escape", "dismiss(None)", "", show=False),
51 | ]
52 | """Bindings for the base text modal dialog."""
53 |
54 | def __init__(self, title: TextType, message: TextType) -> None:
55 | """Initialise the dialog.
56 |
57 | Args:
58 | title: The title for the dialog.
59 | message: The message to show.
60 | """
61 | super().__init__()
62 | self._title = title
63 | self._message = message
64 |
65 | @property
66 | def button_style(self) -> ButtonVariant:
67 | """The style for the dialog's button."""
68 | return "primary"
69 |
70 | def compose(self) -> ComposeResult:
71 | """Compose the content of the modal dialog."""
72 | with Vertical():
73 | with Center():
74 | yield Static(self._title, classes="spaced")
75 | yield Static(self._message, id="message", classes="spaced")
76 | with Center(classes="spaced"):
77 | yield Button("OK", variant=self.button_style)
78 |
79 | def on_mount(self) -> None:
80 | """Configure the dialog once the DOM has loaded."""
81 | self.query_one(Button).focus()
82 |
83 | def on_button_pressed(self) -> None:
84 | """Handle the OK button being pressed."""
85 | self.dismiss(None)
86 |
87 |
88 | class AboutDialog(TextDialog):
89 | DEFAULT_CSS = """
90 | TextDialog > Vertical {
91 | border: thick $primary 50%;
92 | }
93 | """
94 |
95 | def __init__(self) -> None:
96 | title = f"About Trogon"
97 | message = Text.from_markup(
98 | f"Built with [@click=app.visit('https://github.com/textualize/textual')]Textual[/] "
99 | f"by [@click=app.visit('https://textualize.io')]Textualize[/].\n\n"
100 | f"[@click=app.visit('https://github.com/textualize/{constants.PACKAGE_NAME}')]"
101 | f"https://github.com/textualize/{constants.PACKAGE_NAME}[/]",
102 | )
103 | super().__init__(title, message)
104 |
--------------------------------------------------------------------------------
/trogon/widgets/command_info.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from rich.text import Text
4 | from textual import on
5 | from textual.app import ComposeResult
6 | from textual.binding import Binding
7 | from textual.containers import Vertical
8 | from textual.screen import ModalScreen
9 | from textual.widgets import Static, Tabs, Tab, ContentSwitcher, DataTable
10 |
11 | from trogon.introspect import CommandSchema
12 | from trogon.widgets.multiple_choice import NonFocusableVerticalScroll
13 |
14 |
15 | class CommandMetadata(DataTable):
16 | def __init__(
17 | self,
18 | command_schema: CommandSchema,
19 | name: str | None = None,
20 | id: str | None = None,
21 | classes: str | None = None,
22 | disabled: bool = False,
23 | ) -> None:
24 | super().__init__(
25 | name=name,
26 | id=id,
27 | classes=classes,
28 | disabled=disabled,
29 | )
30 | self.show_header = False
31 | self.zebra_stripes = True
32 | self.cursor_type = "none"
33 | self.command_schema = command_schema
34 |
35 | def on_mount(self) -> None:
36 | self.add_columns("Key", "Value")
37 | schema = self.command_schema
38 | self.add_rows(
39 | [
40 | (Text("Name", style="b"), schema.name),
41 | (
42 | Text("Parent", style="b"),
43 | getattr(schema.parent, "name", "No parent"),
44 | ),
45 | (Text("Subcommands", style="b"), list(schema.subcommands.keys())),
46 | (Text("Group", style="b"), schema.is_group),
47 | (Text("Arguments", style="b"), len(schema.arguments)),
48 | (Text("Options", style="b"), len(schema.options)),
49 | ]
50 | )
51 |
52 |
53 | class CommandInfo(ModalScreen):
54 | COMPONENT_CLASSES = {"title", "subtitle"}
55 |
56 | BINDINGS = [Binding("q,escape", "close_modal", "Close Modal")]
57 |
58 | def __init__(
59 | self,
60 | command_schema: CommandSchema,
61 | name: str | None = None,
62 | id: str | None = None,
63 | classes: str | None = None,
64 | ) -> None:
65 | super().__init__(
66 | name=name,
67 | id=id,
68 | classes=classes,
69 | )
70 | self.command_schema = command_schema
71 |
72 | def compose(self) -> ComposeResult:
73 | schema = self.command_schema
74 | path = schema.path_from_root
75 | path_string = " ➜ ".join(command.name for command in path)
76 |
77 | title_style = self.get_component_rich_style("title")
78 | subtitle_style = self.get_component_rich_style("subtitle")
79 | modal_header = Text.assemble(
80 | (path_string, title_style), "\n", ("command info", subtitle_style)
81 | )
82 | with NonFocusableVerticalScroll(classes="command-info-container"):
83 | with Vertical(classes="command-info-header"):
84 | yield Static(modal_header, classes="command-info-header-text")
85 | tabs = Tabs(
86 | Tab("Description", id="command-info-text"),
87 | Tab("Metadata", id="command-info-metadata"),
88 | classes="command-info-tabs",
89 | )
90 | tabs.focus()
91 | yield tabs
92 |
93 | command_info = (
94 | self.command_schema.docstring.strip()
95 | if self.command_schema.docstring
96 | else "No description available"
97 | )
98 |
99 | with ContentSwitcher(
100 | initial="command-info-text", id="command-info-switcher"
101 | ):
102 | yield Static(
103 | command_info, id="command-info-text", classes="command-info-text"
104 | )
105 | yield CommandMetadata(
106 | command_schema=self.command_schema,
107 | id="command-info-metadata",
108 | classes="command-info-metadata",
109 | )
110 |
111 | @on(Tabs.TabActivated)
112 | def switch_content(self, event: Tabs.TabActivated) -> None:
113 | self.query_one(ContentSwitcher).current = event.tab.id
114 |
115 | def action_close_modal(self):
116 | self.app.pop_screen()
117 |
--------------------------------------------------------------------------------
/trogon/widgets/command_tree.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from rich.style import Style
4 | from rich.text import TextType, Text
5 | from textual.widgets import Tree
6 | from textual.widgets._tree import TreeNode
7 |
8 | from trogon.introspect import CommandSchema, CommandName
9 |
10 |
11 | class CommandTree(Tree[CommandSchema]):
12 | COMPONENT_CLASSES = {"group"}
13 |
14 | def __init__(
15 | self,
16 | label: TextType,
17 | cli_metadata: dict[CommandName, CommandSchema],
18 | command_name: str,
19 | ):
20 | super().__init__(label)
21 | self.show_root = False
22 | self.guide_depth = 2
23 | self.show_guides = False
24 | self.cli_metadata = cli_metadata
25 | self.command_name = command_name
26 |
27 | def render_label(
28 | self, node: TreeNode[CommandSchema], base_style: Style, style: Style
29 | ) -> Text:
30 | label = node._label.copy()
31 | label.stylize(style)
32 | return label
33 |
34 | def on_mount(self):
35 | def build_tree(
36 | data: dict[CommandName, CommandSchema], node: TreeNode[CommandSchema]
37 | ) -> TreeNode[CommandSchema]:
38 | data = {key: data[key] for key in sorted(data)}
39 | for cmd_name, cmd_data in data.items():
40 | if cmd_name == self.command_name:
41 | continue
42 | if cmd_data.subcommands:
43 | label = Text(cmd_name)
44 | if cmd_data.is_group:
45 | group_style = self.get_component_rich_style("group")
46 | label.stylize(group_style)
47 | label.append(" ")
48 | label.append("group", "dim i")
49 | child = node.add(label, allow_expand=False, data=cmd_data)
50 | build_tree(cmd_data.subcommands, child)
51 | else:
52 | node.add_leaf(cmd_name, data=cmd_data)
53 | return node
54 |
55 | build_tree(self.cli_metadata, self.root)
56 | self.root.expand_all()
57 | self.select_node(self.root)
58 |
--------------------------------------------------------------------------------
/trogon/widgets/form.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import dataclasses
4 |
5 | from textual import on
6 | from textual.app import ComposeResult
7 | from textual.containers import VerticalScroll, Vertical
8 | from textual.message import Message
9 | from textual.widget import Widget
10 | from textual.widgets import Label, Input
11 |
12 | from trogon.introspect import (
13 | CommandSchema,
14 | CommandName,
15 | ArgumentSchema,
16 | OptionSchema,
17 | )
18 | from trogon.run_command import UserCommandData, UserOptionData, UserArgumentData
19 | from trogon.widgets.parameter_controls import ParameterControls
20 |
21 |
22 | @dataclasses.dataclass
23 | class FormControlMeta:
24 | widget: Widget
25 | meta: OptionSchema | ArgumentSchema
26 |
27 |
28 | class CommandForm(Widget):
29 | """Form which is constructed from an introspected Click app. Users
30 | make use of this form in order to construct CLI invocation strings."""
31 |
32 | DEFAULT_CSS = """
33 | .command-form-heading {
34 | padding: 1 0 0 1;
35 | text-style: u;
36 | color: $text;
37 | }
38 | .command-form-input {
39 | border: tall transparent;
40 | }
41 | .command-form-label {
42 | padding: 1 0 0 1;
43 | }
44 | .command-form-checkbox {
45 | background: $boost;
46 | margin: 1 0 0 0;
47 | padding-left: 1;
48 | border: tall transparent;
49 | }
50 | .command-form-checkbox:focus {
51 | border: tall $accent;
52 | }
53 | .command-form-checkbox:focus > .toggle--label {
54 | text-style: none;
55 | }
56 | .command-form-command-group {
57 |
58 | margin: 1 2;
59 | padding: 0 1;
60 | height: auto;
61 | background: $foreground 3%;
62 | border: panel $background;
63 | border-title-color: $text 80%;
64 | border-title-style: bold;
65 | border-subtitle-color: $text 30%;
66 | padding-bottom: 1;
67 | }
68 | .command-form-command-group:focus-within {
69 | border: panel $primary;
70 | }
71 | .command-form-control-help-text {
72 | height: auto;
73 | color: $text 40%;
74 | padding-top: 0;
75 | padding-left: 1;
76 | }
77 | """
78 |
79 | class Changed(Message):
80 | def __init__(self, command_data: UserCommandData):
81 | super().__init__()
82 | self.command_data = command_data
83 | """The new data taken from the form to be converted into a CLI invocation."""
84 |
85 | def __init__(
86 | self,
87 | command_schema: CommandSchema | None = None,
88 | command_schemas: dict[CommandName, CommandSchema] | None = None,
89 | name: str | None = None,
90 | id: str | None = None,
91 | classes: str | None = None,
92 | disabled: bool = False,
93 | ):
94 | super().__init__(name=name, id=id, classes=classes, disabled=disabled)
95 | self.command_schema = command_schema
96 | self.command_schemas = command_schemas
97 | self.first_control: ParameterControls | None = None
98 |
99 | def compose(self) -> ComposeResult:
100 | path_from_root = iter(reversed(self.command_schema.path_from_root))
101 | command_node = next(path_from_root)
102 | with VerticalScroll() as vs:
103 | vs.can_focus = False
104 |
105 | yield Input(
106 | placeholder="Search...",
107 | classes="command-form-filter-input",
108 | id="search",
109 | )
110 |
111 | while command_node is not None:
112 | options = command_node.options
113 | arguments = command_node.arguments
114 | if options or arguments:
115 | with Vertical(
116 | classes="command-form-command-group", id=command_node.key
117 | ) as v:
118 | is_inherited = command_node is not self.command_schema
119 | v.border_title = (
120 | f"{'↪ ' if is_inherited else ''}{command_node.name}"
121 | )
122 | if is_inherited:
123 | v.border_title += " [dim not bold](inherited)"
124 | if arguments:
125 | yield Label(f"Arguments", classes="command-form-heading")
126 | for argument in arguments:
127 | controls = ParameterControls(argument, id=argument.key)
128 | if self.first_control is None:
129 | self.first_control = controls
130 | yield controls
131 |
132 | if options:
133 | yield Label(f"Options", classes="command-form-heading")
134 | for option in options:
135 | controls = ParameterControls(option, id=option.key)
136 | if self.first_control is None:
137 | self.first_control = controls
138 | yield controls
139 |
140 | command_node = next(path_from_root, None)
141 |
142 | def on_mount(self) -> None:
143 | self._form_changed()
144 |
145 | def on_input_changed(self) -> None:
146 | self._form_changed()
147 |
148 | def on_select_changed(self) -> None:
149 | self._form_changed()
150 |
151 | def on_checkbox_changed(self) -> None:
152 | self._form_changed()
153 |
154 | def on_multiple_choice_changed(self) -> None:
155 | self._form_changed()
156 |
157 | def _form_changed(self) -> None:
158 | """Take the current state of the form and build a UserCommandData from it,
159 | then post a FormChanged message"""
160 |
161 | command_schema = self.command_schema
162 | path_from_root = command_schema.path_from_root
163 |
164 | # Sentinel root value to make constructing the tree a little easier.
165 | parent_command_data = UserCommandData(
166 | name=CommandName("_"), options=[], arguments=[]
167 | )
168 |
169 | root_command_data = parent_command_data
170 | for command in path_from_root:
171 | option_datas = []
172 | # For each of the options in the schema for this command,
173 | # lets grab the values the user has supplied for them in the form.
174 | for option in command.options:
175 | parameter_control = self.query_one(f"#{option.key}", ParameterControls)
176 | value = parameter_control.get_values()
177 | for v in value.values:
178 | assert isinstance(v, tuple)
179 | option_data = UserOptionData(option.name, v, option)
180 | option_datas.append(option_data)
181 |
182 | # Now do the same for the arguments
183 | argument_datas = []
184 | for argument in command.arguments:
185 | form_control_widget = self.query_one(
186 | f"#{argument.key}", ParameterControls
187 | )
188 | value = form_control_widget.get_values()
189 | # This should only ever loop once since arguments can be multi-value but not multiple=True.
190 | for v in value.values:
191 | assert isinstance(v, tuple)
192 | argument_data = UserArgumentData(argument.name, v, argument)
193 | argument_datas.append(argument_data)
194 |
195 | assert all(isinstance(option.value, tuple) for option in option_datas)
196 | assert all(isinstance(argument.value, tuple) for argument in argument_datas)
197 | command_data = UserCommandData(
198 | name=command.name,
199 | options=option_datas,
200 | arguments=argument_datas,
201 | parent=parent_command_data,
202 | command_schema=command,
203 | )
204 | parent_command_data.subcommand = command_data
205 | parent_command_data = command_data
206 |
207 | # Trim the sentinel
208 | root_command_data = root_command_data.subcommand
209 | root_command_data.parent = None
210 | self.post_message(self.Changed(root_command_data))
211 |
212 | def focus(self, scroll_visible: bool = True):
213 | if self.first_control is not None:
214 | return self.first_control.focus()
215 |
216 | @on(Input.Changed, ".command-form-filter-input")
217 | def apply_filter(self, event: Input.Changed) -> None:
218 | filter_query = event.value
219 | all_controls = self.query(ParameterControls)
220 | for control in all_controls:
221 | filter_query = filter_query.casefold()
222 | control.apply_filter(filter_query)
223 |
--------------------------------------------------------------------------------
/trogon/widgets/multiple_choice.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import Any, ClassVar
4 |
5 | from rich.text import TextType
6 | from textual import on
7 | from textual.app import ComposeResult
8 | from textual.binding import BindingType, Binding
9 | from textual.containers import VerticalScroll
10 | from textual.message import Message
11 | from textual.widget import Widget
12 | from textual.widgets import Checkbox
13 |
14 |
15 | class NonFocusableVerticalScroll(VerticalScroll, can_focus=False):
16 | pass
17 |
18 |
19 | class MultipleChoice(Widget):
20 | DEFAULT_CSS = """
21 | MultipleChoice {
22 | border: round #666;
23 | width: auto;
24 | height: auto;
25 | }
26 | MultipleChoice > VerticalScroll {
27 | height: auto;
28 | width: auto;
29 | }
30 |
31 | MultipleChoice :focus-within {
32 | border: round $primary-lighten-2;
33 | }
34 | """
35 |
36 | BINDINGS: ClassVar[list[BindingType]] = [
37 | Binding("down,right", "next_button", "", show=False),
38 | Binding("up,left", "previous_button", "", show=False),
39 | ]
40 |
41 | def __init__(
42 | self,
43 | options: list[TextType],
44 | defaults: list[tuple[Any]] | None = None,
45 | name: str | None = None,
46 | id: str | None = None,
47 | classes: str | None = None,
48 | disabled: bool = False,
49 | ):
50 | super().__init__(name=name, id=id, classes=classes, disabled=disabled)
51 | if defaults is None:
52 | defaults = [()]
53 | self.options = options
54 | self.defaults = defaults
55 | self.selected = defaults
56 |
57 | class Changed(Message):
58 | def __init__(self, selected: list[Checkbox]):
59 | super().__init__()
60 | self.selected = selected
61 |
62 | def compose(self) -> ComposeResult:
63 | with NonFocusableVerticalScroll():
64 | for option in self.options:
65 | yield Checkbox(option, value=(option,) in self.defaults)
66 |
67 | @on(Checkbox.Changed)
68 | def checkbox_toggled(self) -> None:
69 | checkboxes = self.query(Checkbox)
70 | selected = []
71 | for checkbox in checkboxes:
72 | if checkbox.value is True:
73 | selected.append(checkbox)
74 | self.selected = [(checkbox.label.plain,) for checkbox in selected]
75 | self.post_message(self.Changed(selected))
76 |
77 | def select_by_label(self, label: str) -> None:
78 | checkboxes = self.query(Checkbox)
79 | for checkbox in checkboxes:
80 | checkbox.value = checkbox.label == label
81 |
82 | def action_next_button(self) -> None:
83 | focused = self.app.focused
84 | checkboxes = list(self.query(Checkbox))
85 | if focused is checkboxes[-1]:
86 | checkboxes[0].focus()
87 | else:
88 | self.app.action_focus_next()
89 |
90 | def action_previous_button(self) -> None:
91 | focused = self.app.focused
92 | checkboxes = list(self.query(Checkbox))
93 | if focused is checkboxes[0]:
94 | checkboxes[-1].focus()
95 | else:
96 | self.app.action_focus_previous()
97 |
--------------------------------------------------------------------------------
/trogon/widgets/parameter_controls.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import functools
4 | from functools import partial
5 | from typing import Any, Callable, Iterable, Union, cast
6 |
7 | import click
8 | from rich.text import Text
9 | from textual import on
10 | from textual.app import ComposeResult
11 | from textual.containers import Vertical, Horizontal
12 | from textual.css.query import NoMatches
13 | from textual.widget import Widget
14 | from textual.widgets import (
15 | Label,
16 | Checkbox,
17 | Input,
18 | Select,
19 | Static,
20 | Button,
21 | )
22 |
23 | from trogon.introspect import ArgumentSchema, OptionSchema, MultiValueParamData
24 | from trogon.widgets.multiple_choice import MultipleChoice
25 |
26 | ControlWidgetType = Union[Input, Checkbox, MultipleChoice, Select[str]]
27 |
28 |
29 | class ControlGroup(Vertical):
30 | pass
31 |
32 |
33 | class ControlGroupsContainer(Vertical):
34 | pass
35 |
36 |
37 | @functools.total_ordering
38 | class ValueNotSupplied:
39 | def __eq__(self, other):
40 | return isinstance(other, ValueNotSupplied)
41 |
42 | def __lt__(self, other):
43 | return False
44 |
45 | def __bool__(self):
46 | return False
47 |
48 |
49 | class ParameterControls(Widget):
50 | def __init__(
51 | self,
52 | schema: ArgumentSchema | OptionSchema,
53 | name: str | None = None,
54 | id: str | None = None,
55 | classes: str | None = None,
56 | disabled: bool = False,
57 | ) -> None:
58 | super().__init__(name=name, id=id, classes=classes, disabled=disabled)
59 | self.schema = schema
60 | self.first_control: Widget | None = None
61 |
62 | def apply_filter(self, filter_query: str) -> bool:
63 | """Show or hide this ParameterControls depending on whether it matches the filter query or not.
64 |
65 | Args:
66 | filter_query: The string to filter on.
67 |
68 | Returns:
69 | True if the filter matched (and the widget is visible).
70 | """
71 | help_text = getattr(self.schema, "help", "") or ""
72 | if not filter_query:
73 | should_be_visible = True
74 | self.display = should_be_visible
75 | else:
76 | name = self.schema.name
77 | if isinstance(name, str):
78 | # Argument names are strings, there's only one name
79 | name_contains_query = filter_query in name.casefold()
80 | should_be_visible = name_contains_query
81 | else:
82 | # Option names are lists since they can have multiple names (e.g. -v and --verbose)
83 | name_contains_query = any(
84 | filter_query in name.casefold() for name in self.schema.name
85 | )
86 | help_contains_query = filter_query in help_text.casefold()
87 | should_be_visible = name_contains_query or help_contains_query
88 |
89 | self.display = should_be_visible
90 |
91 | # Update the highlighting of the help text
92 | if help_text:
93 | try:
94 | help_label = self.query_one(".command-form-control-help-text", Static)
95 | new_help_text = Text(help_text)
96 | new_help_text.highlight_words(
97 | filter_query.split(), "black on yellow", case_sensitive=False
98 | )
99 | help_label.update(new_help_text)
100 | except NoMatches:
101 | pass
102 |
103 | return should_be_visible
104 |
105 | def compose(self) -> ComposeResult:
106 | """Takes the schemas for each parameter of the current command, and converts it into a
107 | form consisting of Textual widgets."""
108 | schema = self.schema
109 | name = schema.name
110 | argument_type = schema.type
111 | default = schema.default
112 | help_text = getattr(schema, "help", "") or ""
113 | multiple = schema.multiple
114 | is_option = isinstance(schema, OptionSchema)
115 | nargs = schema.nargs
116 |
117 | label = self._make_command_form_control_label(
118 | name, argument_type, is_option, schema.required, multiple=multiple
119 | )
120 | first_focus_control: Widget | None = (
121 | None # The widget that will be focused when the form is focused.
122 | )
123 |
124 | # If there are N defaults, we render the "group" N times.
125 | # Each group will contain `nargs` widgets.
126 | with ControlGroupsContainer():
127 | if not argument_type == click.BOOL:
128 | yield Label(label, classes="command-form-label")
129 |
130 | if isinstance(argument_type, click.Choice) and multiple:
131 | # Display a MultipleChoice widget
132 | # There's a special case where we have a Choice with multiple=True,
133 | # in this case, we can just render a single MultipleChoice widget
134 | # instead of multiple radio-sets.
135 | control_method = self.get_control_method(argument_type)
136 | multiple_choice_widget = control_method(
137 | default=default,
138 | label=label,
139 | multiple=multiple,
140 | schema=schema,
141 | control_id=schema.key,
142 | )
143 | yield from multiple_choice_widget
144 | else:
145 | # For other widgets, we'll render as normal...
146 | # If required, we'll generate widgets containing the defaults
147 | for default_value_tuple in default.values:
148 | widget_group = list(self.make_widget_group())
149 | with ControlGroup() as control_group:
150 | if len(widget_group) == 1:
151 | control_group.add_class("single-item")
152 |
153 | # Parameter types can be of length 1, but there could still
154 | # be multiple defaults. We need to render a widget for each
155 | # of those defaults. Extend the widget group such that
156 | # there's a slot available for each default...
157 | for default_value, control_widget in zip(
158 | default_value_tuple, widget_group
159 | ):
160 | self._apply_default_value(control_widget, default_value)
161 | yield control_widget
162 | # Keep track of the first control we render, for easy focus
163 | if first_focus_control is None:
164 | first_focus_control = control_widget
165 |
166 | # We always need to display the original group of controls,
167 | # regardless of whether there are defaults
168 | if multiple or not default.values:
169 | widget_group = list(self.make_widget_group())
170 | with ControlGroup() as control_group:
171 | if len(widget_group) == 1:
172 | control_group.add_class("single-item")
173 |
174 | # No need to apply defaults to this group
175 | for control_widget in widget_group:
176 | yield control_widget
177 | if first_focus_control is None:
178 | first_focus_control = control_widget
179 |
180 | # Take note of the first form control, so we can easily focus it
181 | if self.first_control is None:
182 | self.first_control = first_focus_control
183 |
184 | # If it's a multiple, and it's a Choice parameter, then we display
185 | # our special case MultiChoice widget, and so there's no need for this
186 | # button.
187 | if (multiple or nargs == -1) and not isinstance(argument_type, click.Choice):
188 | with Horizontal(classes="add-another-button-container"):
189 | yield Button("+ value", variant="success", classes="add-another-button")
190 |
191 | # Render the dim help text below the form controls
192 | if help_text:
193 | yield Static(help_text, classes="command-form-control-help-text")
194 |
195 | def make_widget_group(self) -> Iterable[ControlWidgetType]:
196 | """For this option, yield a single set of widgets required to receive user input for it."""
197 | schema = self.schema
198 | default = schema.default
199 | parameter_type = schema.type
200 | name = schema.name
201 | multiple = schema.multiple
202 | required = schema.required
203 | is_option = isinstance(schema, OptionSchema)
204 | label = self._make_command_form_control_label(
205 | name, parameter_type, is_option, required, multiple
206 | )
207 |
208 | # Get the types of the parameter. We can map these types on to widgets that will be rendered.
209 | parameter_types = (
210 | parameter_type.types
211 | if isinstance(parameter_type, click.Tuple)
212 | else [parameter_type]
213 | )
214 |
215 | # For each of the these parameters, render the corresponding widget for it.
216 | # At this point we don't care about filling in the default values.
217 | for _type in parameter_types:
218 | control_method = self.get_control_method(_type)
219 | control_widgets = control_method(
220 | default, label, multiple, schema, schema.key
221 | )
222 | yield from control_widgets
223 |
224 | @on(Button.Pressed, ".add-another-button")
225 | def add_another_widget_group(self, event: Button.Pressed) -> None:
226 | widget_group = list(self.make_widget_group())
227 | widget_group[0].focus()
228 | control_group = ControlGroup(*widget_group)
229 | if len(widget_group) <= 1:
230 | control_group.add_class("single-item")
231 | control_groups_container = self.query_one(ControlGroupsContainer)
232 | control_groups_container.mount(control_group)
233 | event.button.scroll_visible(animate=False)
234 |
235 | @staticmethod
236 | def _apply_default_value(
237 | control_widget: ControlWidgetType, default_value: Any
238 | ) -> None:
239 | """Set the default value of a parameter-handling widget."""
240 | if isinstance(control_widget, Input):
241 | control_widget.value = str(default_value)
242 | control_widget.placeholder = f"{default_value} (default)"
243 | elif isinstance(control_widget, Select):
244 | control_widget.value = str(default_value)
245 | control_widget.prompt = f"{default_value} (default)"
246 |
247 | @staticmethod
248 | def _get_form_control_value(control: ControlWidgetType) -> Any:
249 | if isinstance(control, MultipleChoice):
250 | return control.selected
251 | elif isinstance(control, Select):
252 | if control.value is None or control.value is Select.BLANK:
253 | return ValueNotSupplied()
254 | return control.value
255 | elif isinstance(control, Input):
256 | return (
257 | ValueNotSupplied() if control.value == "" else control.value
258 | ) # TODO: We should only return "" when user selects a checkbox - needs custom widget.
259 | elif isinstance(control, Checkbox):
260 | return control.value
261 |
262 | def get_values(self) -> MultiValueParamData:
263 | # We can find all relevant control widgets by querying the parameter schema
264 | # key as a class.
265 |
266 | def list_to_tuples(
267 | lst: list[int | float | str], tuple_size: int
268 | ) -> list[tuple[int | float | str, ...]]:
269 | if tuple_size == 0:
270 | return [tuple()]
271 | elif tuple_size == -1:
272 | # Unspecified number of arguments as per Click docs.
273 | tuple_size = 1
274 | return [
275 | tuple(lst[i : i + tuple_size]) for i in range(0, len(lst), tuple_size)
276 | ]
277 |
278 | controls = list(self.query(f".{self.schema.key}"))
279 |
280 | if len(controls) == 1 and isinstance(controls[0], MultipleChoice):
281 | # Since MultipleChoice widgets are a special case that appear in
282 | # isolation, our logic to fetch the values out of them is slightly
283 | # modified from the nominal case presented in the other branch.
284 | # MultiChoice never appears for multi-value options, only for options
285 | # where multiple=True.
286 | control = cast(MultipleChoice, controls[0])
287 | control_values = self._get_form_control_value(control)
288 | return MultiValueParamData.process_cli_option(control_values)
289 | else:
290 | # For each control widget for this parameter, capture the value(s) from them
291 | collected_values = []
292 | for control in list(controls):
293 | control_values = self._get_form_control_value(control)
294 | collected_values.append(control_values)
295 |
296 | # Since we fetched a flat list of widgets (and thus a flat list of values
297 | # from those widgets), we now need to group them into tuples based on nargs.
298 | # We can safely do this since widgets are added to the DOM in the same order
299 | # as the types specified in the click Option `type`. We convert a flat list
300 | # of widget values into a list of tuples, each tuple of length nargs.
301 | collected_values = list_to_tuples(collected_values, self.schema.nargs)
302 | return MultiValueParamData.process_cli_option(collected_values)
303 |
304 | def get_control_method(
305 | self, argument_type: Any
306 | ) -> Callable[
307 | [Any, Text, bool, OptionSchema | ArgumentSchema, str], ControlWidgetType
308 | ]:
309 | text_click_types = {
310 | click.STRING,
311 | click.FLOAT,
312 | click.INT,
313 | click.UUID,
314 | }
315 | text_types = (
316 | click.Path,
317 | click.File,
318 | click.IntRange,
319 | click.FloatRange,
320 | click.types.FuncParamType,
321 | )
322 |
323 | is_text_type = argument_type in text_click_types or isinstance(
324 | argument_type, text_types
325 | )
326 | if is_text_type:
327 | return self.make_text_control
328 | elif argument_type == click.BOOL:
329 | return self.make_checkbox_control
330 | elif isinstance(argument_type, click.types.Choice):
331 | return partial(self.make_choice_control, choices=argument_type.choices)
332 | else:
333 | return self.make_text_control
334 |
335 | @staticmethod
336 | def make_text_control(
337 | default: Any,
338 | label: Text | None,
339 | multiple: bool,
340 | schema: OptionSchema | ArgumentSchema,
341 | control_id: str,
342 | ) -> Iterable[ControlWidgetType]:
343 | control = Input(
344 | classes=f"command-form-input {control_id}",
345 | )
346 | yield control
347 | return control
348 |
349 | @staticmethod
350 | def make_checkbox_control(
351 | default: MultiValueParamData,
352 | label: Text | None,
353 | multiple: bool,
354 | schema: OptionSchema | ArgumentSchema,
355 | control_id: str,
356 | ) -> Iterable[ControlWidgetType]:
357 | if default.values:
358 | default = default.values[0][0]
359 | else:
360 | default = ValueNotSupplied()
361 |
362 | control = Checkbox(
363 | label,
364 | button_first=True,
365 | classes=f"command-form-checkbox {control_id}",
366 | value=default,
367 | )
368 | yield control
369 | return control
370 |
371 | @staticmethod
372 | def make_choice_control(
373 | default: MultiValueParamData,
374 | label: Text | None,
375 | multiple: bool,
376 | schema: OptionSchema | ArgumentSchema,
377 | control_id: str,
378 | choices: list[str],
379 | ) -> Iterable[ControlWidgetType]:
380 | # The MultipleChoice widget is only for single-valued parameters.
381 | if isinstance(schema.type, click.Tuple):
382 | multiple = False
383 |
384 | if multiple:
385 | multi_choice = MultipleChoice(
386 | choices,
387 | classes=f"command-form-multiple-choice {control_id}",
388 | defaults=default.values,
389 | )
390 | yield multi_choice
391 | return multi_choice
392 | else:
393 | select = Select[str](
394 | [(choice, choice) for choice in choices],
395 | classes=f"{control_id} command-form-select",
396 | )
397 | yield select
398 | return select
399 |
400 | @staticmethod
401 | def _make_command_form_control_label(
402 | name: str | list[str],
403 | type: click.ParamType,
404 | is_option: bool,
405 | is_required: bool,
406 | multiple: bool,
407 | ) -> Text:
408 | if isinstance(name, str):
409 | text = Text.from_markup(
410 | f"{name}[dim]{' multiple' if multiple else ''} {type.name}[/] {' [b red]*[/]required' if is_required else ''}"
411 | )
412 | else:
413 | names = Text(" / ", style="dim").join([Text(n) for n in name])
414 | text = Text.from_markup(
415 | f"{names}[dim]{' multiple' if multiple else ''} {type.name}[/] {' [b red]*[/]required' if is_required else ''}"
416 | )
417 |
418 | if isinstance(type, (click.IntRange, click.FloatRange)):
419 | if type.min is not None:
420 | text = Text.assemble(text, Text(f"min={type.min} ", "dim"))
421 | if type.max is not None:
422 | text = Text.assemble(text, Text(f"max={type.max}", "dim"))
423 |
424 | return text
425 |
426 | def focus(self, scroll_visible: bool = True):
427 | if self.first_control is not None:
428 | self.first_control.focus()
429 |
--------------------------------------------------------------------------------