diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 8a0a64cc..7ba2f9ed 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -9,6 +9,9 @@ on: pull_request: branches: [ "develop" ] +permissions: + contents: read + jobs: build: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 22eb649a..ba144f94 100644 --- a/.gitignore +++ b/.gitignore @@ -79,3 +79,6 @@ target/ docs/auto_examples/sg_execution_times.* docs/auto_examples/*.pickle docs/sg_execution_times.rst + +# Temporary files +tmp/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ecbebb33..e70b8469 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,6 +31,16 @@ repos: types: [python] language: system pass_filenames: false + - id: generate-images + name: Generate README images + entry: >- + uv run python -m statemachine.contrib.diagram + tests.examples.traffic_light_machine.TrafficLightMachine + docs/images/readme_trafficlightmachine.png + --events cycle cycle cycle + language: system + pass_filenames: false + files: (statemachine/contrib/diagram/|tests/examples/traffic_light_machine\.py) - id: pytest name: Pytest entry: uv run pytest -n auto --cov-fail-under=100 diff --git a/AGENTS.md b/AGENTS.md index 03c18edb..3ddbe3f4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -77,6 +77,16 @@ current event. - `on_error_execution()` works via naming convention but **only** when a transition for `error.execution` is declared — it is NOT a generic callback. +### Thread safety + +- The sync engine is **thread-safe**: multiple threads can send events to the same SM instance + concurrently. The processing loop uses a `threading.Lock` so at most one thread executes + transitions at a time. Event queues use `PriorityQueue` (stdlib, thread-safe). +- **Do not replace `PriorityQueue`** with non-thread-safe alternatives (e.g., `collections.deque`, + plain `list`) — this would break concurrent access guarantees. +- Stress tests in `tests/test_threading.py::TestThreadSafety` exercise real contention with + barriers and multiple sender threads. Any change to queue or locking internals must pass these. + ### Invoke (``) - `invoke.py` — `InvokeManager` on the engine manages the lifecycle: `mark_for_invoke()`, @@ -127,6 +137,16 @@ timeout 120 uv run pytest -n 4 Testes normally run under 60s (~40s on average), so take a closer look if they take longer, it can be a regression. +### Debug logging + +`log_cli_level` defaults to `WARNING` in `pyproject.toml`. The engine caches a no-op +for `logger.debug` at init time — running tests with `DEBUG` would bypass this +optimization and inflate benchmark numbers. To enable debug logs for a specific run: + +```bash +uv run pytest -o log_cli_level=DEBUG tests/test_something.py +``` + When analyzing warnings or extensive output, run the tests **once** saving the output to a file (`> /tmp/pytest-output.txt 2>&1`), then analyze the file — instead of running the suite repeatedly with different greps. @@ -160,6 +180,26 @@ async def test_something(self, sm_runner): Do **not** manually add async no-op listeners or duplicate test classes — prefer `sm_runner`. +### TDD and coverage requirements + +Follow a **test-driven development** approach: tests are not an afterthought — they are a +first-class requirement that must be part of every implementation plan. + +- **Planning phase:** every plan must include test tasks as explicit steps, not a final + "add tests" bullet. Identify what needs to be tested (new branches, edge cases, error + paths) while designing the implementation. +- **100% branch coverage is mandatory.** The pre-commit hook enforces `--cov-fail-under=100` + with branch coverage enabled. Code that drops coverage will not pass CI. +- **Verify coverage before committing:** after writing tests, run coverage on the affected + modules and check for missing lines/branches: + ```bash + timeout 120 uv run pytest tests/.py --cov=statemachine. --cov-report=term-missing --cov-branch + ``` +- **Use pytest fixtures** (`tmp_path`, `monkeypatch`, etc.) — never hardcode paths or + use mutable global state when a fixture exists. +- **Unreachable defensive branches** (e.g., `if` guards that can never be True given the + type system) may be marked with `pragma: no cover`, but prefer writing a test first. + ## Linting and formatting ```bash diff --git a/README.md b/README.md index 28e0c673..b7393b09 100644 --- a/README.md +++ b/README.md @@ -77,16 +77,23 @@ True ``` -Generate a diagram: +Generate a diagram or get a text representation with f-strings: ```py ->>> # This example will only run on automated tests if dot is present ->>> getfixture("requires_dot_installed") ->>> img_path = "docs/images/readme_trafficlightmachine.png" ->>> sm._graph().write_png(img_path) +>>> print(f"{sm:md}") +| State | Event | Guard | Target | +| ------ | ----- | ----- | ------ | +| Green | Cycle | | Yellow | +| Yellow | Cycle | | Red | +| Red | Cycle | | Green | + ``` +```python +sm._graph().write_png("traffic_light.png") +``` + ![](https://raw.githubusercontent.com/fgmacedo/python-statemachine/develop/docs/images/readme_trafficlightmachine.png) Parameters are injected into callbacks automatically — the library inspects the @@ -345,7 +352,7 @@ There's a lot more to explore: - **`prepare_event`** callback — inject custom data into all callbacks - **Observer pattern** — register external listeners to watch events and state changes - **Django integration** — auto-discover state machines in Django apps with `MachineMixin` -- **Diagram generation** — from the CLI, at runtime, or in Jupyter notebooks +- **Diagram generation** — via f-strings (`f"{sm:mermaid}"`), CLI, Sphinx directive, or Jupyter - **Dictionary-based definitions** — create state machines from data structures - **Internationalization** — error messages in multiple languages diff --git a/docs/async.md b/docs/async.md index 3680d7ac..313d03f8 100644 --- a/docs/async.md +++ b/docs/async.md @@ -75,7 +75,7 @@ the initial state: ... print(list(sm.configuration_values)) >>> asyncio.run(show_problem()) -[None] +[] ``` diff --git a/docs/conf.py b/docs/conf.py index af845c4f..59e518b5 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -51,6 +51,8 @@ "sphinx.ext.autosectionlabel", "sphinx_gallery.gen_gallery", "sphinx_copybutton", + "statemachine.contrib.diagram.sphinx_ext", + "sphinxcontrib.mermaid", ] autosectionlabel_prefix_document = True diff --git a/docs/diagram.md b/docs/diagram.md index 9caeb39e..d48443d3 100644 --- a/docs/diagram.md +++ b/docs/diagram.md @@ -6,6 +6,10 @@ You can generate visual diagrams from any {class}`~statemachine.statemachine.StateChart` — useful for documentation, debugging, or sharing your machine's structure with teammates. +```{statemachine-diagram} tests.examples.order_control_machine.OrderControl +:target: +``` + ## Installation Diagram generation requires [pydot](https://github.com/pydot/pydot) and @@ -23,84 +27,204 @@ sudo apt install graphviz For other systems, see the [Graphviz downloads page](https://graphviz.org/download/). - ## Generating diagrams -Use `DotGraphMachine` to create a diagram from a class or an instance: +Every state machine instance exposes a `_graph()` method that returns a +[pydot.Dot](https://github.com/pydot/pydot) graph object: -```py ->>> from statemachine.contrib.diagram import DotGraphMachine +```python +from tests.examples.order_control_machine import OrderControl ->>> from tests.examples.order_control_machine import OrderControl +sm = OrderControl() +graph = sm._graph() # returns a pydot.Dot object +``` ->>> graph = DotGraphMachine(OrderControl) # also accepts instances +### Highlighting the current state ->>> dot = graph() +The diagram automatically highlights the current state of the instance. +Send events to advance the machine and see the active state change: ->>> dot.to_string() # doctest: +ELLIPSIS -'digraph OrderControl {... +```python +from tests.examples.traffic_light_machine import TrafficLightMachine +sm = TrafficLightMachine() +sm.send("cycle") +sm._graph().write_png("traffic_light_yellow.png") ``` -Export to an image file: +```{statemachine-diagram} tests.examples.traffic_light_machine.TrafficLightMachine +:events: cycle +:caption: TrafficLightMachine after one cycle +``` -```py ->>> dot.write_png("docs/images/order_control_machine_initial.png") -``` +### Exporting to a file -![OrderControl](images/order_control_machine_initial.png) +The `pydot.Dot` object supports writing to many formats — use +`write_png()`, `write_svg()`, `write_pdf()`, etc.: -For higher resolution, set the DPI before exporting: +```python +sm = OrderControl() +sm._graph().write_png("order_control.png") +``` -```py ->>> dot.set_dpi(300) +```{statemachine-diagram} tests.examples.order_control_machine.OrderControl +:caption: OrderControl +``` + +For higher resolution PNGs, set the DPI before exporting: ->>> dot.write_png("docs/images/order_control_machine_initial_300dpi.png") +```python +graph = sm._graph() +graph.set_dpi(300).write_png("order_control_300dpi.png") +``` +```{note} +Supported formats include `dia`, `dot`, `fig`, `gif`, `jpg`, `pdf`, +`png`, `ps`, `svg`, and many others. See +[Graphviz output formats](https://graphviz.org/docs/outputs/) for the +complete list. ``` -![OrderControl (300 DPI)](images/order_control_machine_initial_300dpi.png) -### Highlighting the current state +## Text representations -When you pass a machine **instance** (not a class), the diagram highlights -the current state: +State machines support multiple text-based output formats, all accessible +through Python's built-in `format()` protocol, the `formatter` API, or +the command line. -``` py ->>> # This example will only run on automated tests if dot is present ->>> getfixture("requires_dot_installed") +| Format | Aliases | Description | Dependencies | +|--------|---------|-------------|--------------| +| `mermaid` | | [Mermaid stateDiagram-v2](https://mermaid.js.org/syntax/stateDiagram.html) source | None [^mermaid] | +| `md` | `markdown` | Transition table (pipe-delimited Markdown) | None | +| `rst` | | Transition table (RST grid table) | None | +| `dot` | | [Graphviz DOT](https://graphviz.org/doc/info/lang.html) language source | pydot | +| `svg` | | SVG markup (generated via DOT) | pydot, Graphviz | ->>> from statemachine.contrib.diagram import DotGraphMachine +[^mermaid]: Mermaid has a known rendering bug + ([mermaid-js/mermaid#4052](https://github.com/mermaid-js/mermaid/issues/4052)) + where transitions targeting or originating from a compound state inside a + parallel region crash the renderer. As a workaround, the `MermaidRenderer` + redirects such transitions to the compound's initial child state. The + visual result is equivalent — Mermaid draws the arrow crossing into the + compound boundary — but the arrow points to the child rather than the + compound border. This workaround will be revisited when the upstream bug + is resolved. ->>> from tests.examples.order_control_machine import OrderControl ->>> machine = OrderControl() +### Using `format()` ->>> graph = DotGraphMachine(machine) # also accepts instances +Use f-strings or the built-in `format()` function — no diagram imports needed: ->>> machine.receive_payment(10) -[10] +```py +>>> from tests.examples.traffic_light_machine import TrafficLightMachine +>>> sm = TrafficLightMachine() +>>> print(f"{sm:mermaid}") +stateDiagram-v2 + direction LR + state "Green" as green + state "Yellow" as yellow + state "Red" as red + [*] --> green + green --> yellow : Cycle + yellow --> red : Cycle + red --> green : Cycle + + classDef active fill:#40E0D0,stroke:#333 + green:::active + + +>>> print(f"{sm:md}") +| State | Event | Guard | Target | +| ------ | ----- | ----- | ------ | +| Green | Cycle | | Yellow | +| Yellow | Cycle | | Red | +| Red | Cycle | | Green | + ->>> graph().write_png("docs/images/order_control_machine_processing.png") +``` + +Works on **classes** too (no active-state highlighting): + +```py +>>> print(f"{TrafficLightMachine:mermaid}") +stateDiagram-v2 + direction LR + state "Green" as green + state "Yellow" as yellow + state "Red" as red + [*] --> green + green --> yellow : Cycle + yellow --> red : Cycle + red --> green : Cycle + ``` -![OrderControl](images/order_control_machine_processing.png) +The `dot` format returns the Graphviz DOT language source: + +```py +>>> print(f"{sm:dot}") # doctest: +ELLIPSIS +digraph TrafficLightMachine { +... +} -```{tip} -Every state machine instance exposes a `_graph()` shortcut that returns -the `pydot.Dot` object directly. ``` +An empty format spec (e.g., `f"{sm:}"`) falls back to `repr()`. + + +(formatter-api)= +### Using the `formatter` API + +The `formatter` object is the programmatic entry point for rendering +state machines in any registered text format: + ```py ->>> machine._graph() # doctest: +ELLIPSIS ->> from statemachine.contrib.diagram import formatter +>>> from tests.examples.traffic_light_machine import TrafficLightMachine + +>>> print(formatter.render(TrafficLightMachine, "mermaid")) +stateDiagram-v2 + direction LR + state "Green" as green + state "Yellow" as yellow + state "Red" as red + [*] --> green + green --> yellow : Cycle + yellow --> red : Cycle + red --> green : Cycle + + +>>> formatter.supported_formats() +['dot', 'markdown', 'md', 'mermaid', 'rst', 'svg'] + +``` + +Both `format()` and the Sphinx directive delegate to this same `formatter` +under the hood. + +#### Registering custom formats + +The `formatter` is extensible — register your own format with a +decorator and it becomes available everywhere (`format()`, CLI, +Sphinx directive): + +```python +from statemachine.contrib.diagram import formatter + +@formatter.register_format("plantuml", "puml") +def _render_plantuml(machine_or_class): + # your PlantUML renderer here + ... ``` +After registration, `f"{sm:plantuml}"` and `--format plantuml` work +immediately. + -## Command line +### Command line You can generate diagrams without writing Python code: @@ -114,11 +238,242 @@ The output format is inferred from the file extension: python -m statemachine.contrib.diagram tests.examples.traffic_light_machine.TrafficLightMachine diagram.png ``` +To highlight the current state, use `--events` to instantiate the machine and +send events before rendering: + +```bash +python -m statemachine.contrib.diagram tests.examples.traffic_light_machine.TrafficLightMachine diagram.png --events cycle cycle cycle +``` + +Use `--format` to produce a text format instead of a Graphviz image: + +```bash +# Mermaid stateDiagram-v2 +python -m statemachine.contrib.diagram tests.examples.traffic_light_machine.TrafficLightMachine output.mmd --format mermaid + +# DOT source +python -m statemachine.contrib.diagram tests.examples.traffic_light_machine.TrafficLightMachine output.dot --format dot + +# Markdown transition table +python -m statemachine.contrib.diagram tests.examples.traffic_light_machine.TrafficLightMachine output.md --format md + +# RST transition table +python -m statemachine.contrib.diagram tests.examples.traffic_light_machine.TrafficLightMachine output.rst --format rst +``` + +Use `-` as the output file to write to stdout (handy for piping): + +```bash +python -m statemachine.contrib.diagram tests.examples.traffic_light_machine.TrafficLightMachine - --format mermaid +``` + + +## Auto-expanding docstrings + +Use `{statechart:FORMAT}` placeholders in your class docstring to embed +a live representation of the state machine. The placeholder is replaced +at class definition time, so the docstring always reflects the actual +states and transitions: + +```py +>>> from statemachine.statemachine import StateChart +>>> from statemachine.state import State + +>>> class TrafficLight(StateChart): +... """A traffic light. +... +... {statechart:md} +... """ +... green = State(initial=True) +... yellow = State() +... red = State() +... cycle = green.to(yellow) | yellow.to(red) | red.to(green) + +>>> print(TrafficLight.__doc__) +A traffic light. + +| State | Event | Guard | Target | +| ------ | ----- | ----- | ------ | +| Green | Cycle | | Yellow | +| Yellow | Cycle | | Red | +| Red | Cycle | | Green | + + + +``` + +Any registered format works: `{statechart:rst}`, `{statechart:mermaid}`, +`{statechart:dot}`, etc. + +### Choosing the right format + +| Context | Recommended format | +|---------|-------------------| +| Sphinx with RST (autodoc default) | `{statechart:rst}` | +| Sphinx with MyST Markdown | `{statechart:md}` | +| `help()` in terminal / IDE | Either works; `md` reads more cleanly | + +### Sphinx autodoc integration + +Since the placeholder is expanded at class definition time, Sphinx `autodoc` +sees the final rendered text — no extra configuration needed. + +For example, this class uses `{statechart:rst}` in its docstring: + +```{literalinclude} ../tests/machines/showcase_simple.py +:pyobject: SimpleSC +:language: python +``` + +And here is the rendered autodoc output: + +```{eval-rst} +.. autoclass:: tests.machines.showcase_simple.SimpleSC + :noindex: +``` + + +## Sphinx directive + +If you use [Sphinx](https://www.sphinx-doc.org/) to build your documentation, the +`statemachine-diagram` directive renders diagrams inline — no need to generate +image files manually. + +### Setup + +Add the extension to your `conf.py`: + +```python +extensions = [ + ... + "statemachine.contrib.diagram.sphinx_ext", +] +``` + +### Basic usage + +Reference any importable {class}`~statemachine.statemachine.StateChart` class by +its fully qualified path: + +````markdown +```{statemachine-diagram} myproject.machines.OrderControl +``` +```` + +```{statemachine-diagram} tests.examples.order_control_machine.OrderControl +:alt: OrderControl state machine +:align: center +``` + +### Highlighting a specific state + +Pass `:events:` to instantiate the machine and send events before rendering. +This highlights the current state after processing: + +````markdown +```{statemachine-diagram} myproject.machines.TrafficLight +:events: cycle +:caption: Traffic light after one cycle +``` +```` + +```{statemachine-diagram} tests.examples.traffic_light_machine.TrafficLightMachine +:events: cycle +:caption: Traffic light after one cycle +:align: center +``` + +### Enabling zoom + +For complex diagrams, add `:target:` (without a value) to make the diagram +clickable — it opens the full SVG in a new browser tab where users can +zoom and pan freely: + +````markdown +```{statemachine-diagram} myproject.machines.OrderControl +:target: +``` +```` + +```{statemachine-diagram} tests.examples.order_control_machine.OrderControl +:caption: Click to open full-size SVG +:target: +:align: center +``` + +### Mermaid format + +Use `:format: mermaid` to render via +[sphinxcontrib-mermaid](https://github.com/mgaitan/sphinxcontrib-mermaid) +instead of Graphviz SVG — useful when you don't want to install Graphviz +in your docs build environment: + +````markdown +```{statemachine-diagram} myproject.machines.TrafficLight +:format: mermaid +:caption: Rendered as Mermaid +``` +```` + +```{statemachine-diagram} tests.examples.traffic_light_machine.TrafficLightMachine +:format: mermaid +:caption: TrafficLightMachine (Mermaid) +:align: center +``` + +### Directive options + +The directive supports the same layout options as the standard `image` and +`figure` directives, plus state-machine-specific ones. + +**State-machine options:** + +`:events:` *(comma-separated string)* +: Events to send in sequence. When present, the machine is instantiated and + each event is sent before rendering. + +`:format:` *(string)* +: Output format. Use `mermaid` to render via sphinxcontrib-mermaid + instead of Graphviz SVG. Default: DOT/SVG. + +**Image/figure options:** + +`:caption:` *(string)* +: Caption text; wraps the image in a `figure` node. + +`:alt:` *(string)* +: Alt text for the image. Defaults to the class name. + +`:width:` *(CSS length, e.g. `400px`, `80%`)* +: Explicit width for the diagram. + +`:height:` *(CSS length)* +: Explicit height for the diagram. + +`:scale:` *(integer percentage, e.g. `50%`)* +: Uniform scaling relative to the intrinsic size. + +`:align:` *(left | center | right)* +: Image alignment. Defaults to `center`. + +`:target:` *(URL or empty)* +: Makes the diagram clickable. When set without a value, the raw SVG is + saved as a file and linked so users can open it in a new tab for + full-resolution zooming — useful for large or complex diagrams. + +`:class:` *(space-separated strings)* +: Extra CSS classes for the wrapper element. + +`:figclass:` *(space-separated strings)* +: Extra CSS classes for the `figure` element (only when `:caption:` is set). + +`:name:` *(string)* +: Reference target name for cross-referencing with `{ref}`. + ```{note} -Supported formats include `dia`, `dot`, `fig`, `gif`, `jpg`, `pdf`, -`png`, `ps`, `svg`, and many others. See -[Graphviz output formats](https://graphviz.org/docs/outputs/) for the -complete list. +The directive imports the state machine class at Sphinx parse time. Machines +defined inline in doctest blocks cannot be referenced — use the +`_graph()` method for those cases. ``` @@ -139,4 +494,363 @@ using the [QuickChart](https://quickchart.io/) online service: .. autofunction:: statemachine.contrib.diagram.quickchart_write_svg ``` -![OrderControl](images/oc_machine_processing.svg) + +## Customizing the output + +The `DotGraphMachine` class gives you control over the diagram's visual +properties. Subclass it and override the class attributes to customize +fonts, colors, and layout: + +```python +from statemachine.contrib.diagram import DotGraphMachine +from tests.examples.order_control_machine import OrderControl +``` + +Available attributes: + +| Attribute | Default | Description | +|-----------|---------|-------------| +| `graph_rankdir` | `"LR"` | Graph direction (`"LR"` left-to-right, `"TB"` top-to-bottom) | +| `font_name` | `"Helvetica"` | Font face for labels | +| `state_font_size` | `"10"` | State label font size | +| `state_active_penwidth` | `2` | Border width of the active state | +| `state_active_fillcolor` | `"turquoise"` | Fill color of the active state | +| `transition_font_size` | `"9"` | Transition label font size | + +For example, to generate a top-to-bottom diagram with a custom active +state color: + +```python +class CustomDiagram(DotGraphMachine): + graph_rankdir = "TB" + state_active_fillcolor = "lightyellow" + +sm = OrderControl() +sm.receive_payment(10) + +graph = CustomDiagram(sm) +dot = graph() +dot.write_svg("order_control_custom.svg") +``` + +`DotGraphMachine` also works with **classes** (not just instances) to +generate diagrams without an active state: + +```python +dot = DotGraphMachine(OrderControl)() +dot.write_png("order_control_class.png") +``` + + +## Visual showcase + +This section shows how each state machine feature is rendered in diagrams. +Each example includes the class definition, diagrams in both **Graphviz** +and **Mermaid** formats, and **instance** diagrams with the current state +highlighted after sending events. + + +### Simple states + +A minimal state machine with three atomic states and linear transitions. + +```{literalinclude} ../tests/machines/showcase_simple.py +:pyobject: SimpleSC +:language: python +``` + +```{statemachine-diagram} tests.machines.showcase_simple.SimpleSC +:caption: Class (Graphviz) +``` + +```{statemachine-diagram} tests.machines.showcase_simple.SimpleSC +:format: mermaid +:caption: Class (Mermaid) +``` + +```{statemachine-diagram} tests.machines.showcase_simple.SimpleSC +:events: +:caption: Initial +``` + +```{statemachine-diagram} tests.machines.showcase_simple.SimpleSC +:events: start +:caption: Running +``` + +```{statemachine-diagram} tests.machines.showcase_simple.SimpleSC +:events: start, finish +:caption: Done (final) +``` + + +### Entry and exit actions + +States can declare `entry` / `exit` callbacks, shown in the state label. + +```{literalinclude} ../tests/machines/showcase_actions.py +:pyobject: ActionsSC +:language: python +``` + +```{statemachine-diagram} tests.machines.showcase_actions.ActionsSC +:caption: Class (Graphviz) +``` + +```{statemachine-diagram} tests.machines.showcase_actions.ActionsSC +:format: mermaid +:caption: Class (Mermaid) +``` + +```{statemachine-diagram} tests.machines.showcase_actions.ActionsSC +:events: power_on +:caption: Active: On +``` + + +### Guard conditions + +Transitions can have `cond` guards, shown in brackets on the edge label. + +```{literalinclude} ../tests/machines/showcase_guards.py +:pyobject: GuardSC +:language: python +``` + +```{statemachine-diagram} tests.machines.showcase_guards.GuardSC +:caption: Class (Graphviz) +``` + +```{statemachine-diagram} tests.machines.showcase_guards.GuardSC +:format: mermaid +:caption: Class (Mermaid) +``` + +```{statemachine-diagram} tests.machines.showcase_guards.GuardSC +:events: +:caption: Active: Pending +``` + + +### Self-transitions + +A transition from a state back to itself. + +```{literalinclude} ../tests/machines/showcase_self_transition.py +:pyobject: SelfTransitionSC +:language: python +``` + +```{statemachine-diagram} tests.machines.showcase_self_transition.SelfTransitionSC +:caption: Class (Graphviz) +``` + +```{statemachine-diagram} tests.machines.showcase_self_transition.SelfTransitionSC +:format: mermaid +:caption: Class (Mermaid) +``` + +```{statemachine-diagram} tests.machines.showcase_self_transition.SelfTransitionSC +:events: +:caption: Active: Counting +``` + + +### Internal transitions + +Internal transitions execute actions without exiting/entering the state. + +```{literalinclude} ../tests/machines/showcase_internal.py +:pyobject: InternalSC +:language: python +``` + +```{statemachine-diagram} tests.machines.showcase_internal.InternalSC +:caption: Class (Graphviz) +``` + +```{statemachine-diagram} tests.machines.showcase_internal.InternalSC +:format: mermaid +:caption: Class (Mermaid) +``` + +```{statemachine-diagram} tests.machines.showcase_internal.InternalSC +:events: +:caption: Active: Monitoring +``` + + +### Compound states + +A compound state contains child states. Entering the compound activates +its initial child. + +```{literalinclude} ../tests/machines/showcase_compound.py +:pyobject: CompoundSC +:language: python +``` + +```{statemachine-diagram} tests.machines.showcase_compound.CompoundSC +:caption: Class (Graphviz) +:target: +``` + +```{statemachine-diagram} tests.machines.showcase_compound.CompoundSC +:format: mermaid +:caption: Class (Mermaid) +``` + +```{statemachine-diagram} tests.machines.showcase_compound.CompoundSC +:events: +:caption: Off +:target: +``` + +```{statemachine-diagram} tests.machines.showcase_compound.CompoundSC +:events: turn_on +:caption: Active/Idle +:target: +``` + +```{statemachine-diagram} tests.machines.showcase_compound.CompoundSC +:events: turn_on, begin +:caption: Active/Working +:target: +``` + + +### Parallel states + +A parallel state activates all its regions simultaneously. + +```{literalinclude} ../tests/machines/showcase_parallel.py +:pyobject: ParallelSC +:language: python +``` + +```{statemachine-diagram} tests.machines.showcase_parallel.ParallelSC +:caption: Class (Graphviz) +:target: +``` + +```{statemachine-diagram} tests.machines.showcase_parallel.ParallelSC +:format: mermaid +:caption: Class (Mermaid) +``` + +```{statemachine-diagram} tests.machines.showcase_parallel.ParallelSC +:events: enter +:caption: Both active +:target: +``` + +```{statemachine-diagram} tests.machines.showcase_parallel.ParallelSC +:events: enter, go_l +:caption: Left done +:target: +``` + + +### Parallel with cross-boundary transitions + +A transition targeting a compound state **inside** a parallel region triggers a +rendering bug in Mermaid (`mermaid-js/mermaid#4052`). The Mermaid renderer works +around this by redirecting the arrow to the compound's initial child — compare the +``rebuild`` arrow in both diagrams below. + +```{literalinclude} ../tests/machines/showcase_parallel_compound.py +:pyobject: ParallelCompoundSC +:language: python +``` + +```{statemachine-diagram} tests.machines.showcase_parallel_compound.ParallelCompoundSC +:caption: Class (Graphviz) — ``rebuild`` points to the Build compound border +:target: +``` + +```{statemachine-diagram} tests.machines.showcase_parallel_compound.ParallelCompoundSC +:format: mermaid +:caption: Class (Mermaid) — ``rebuild`` is redirected to Compile (initial child of Build) +``` + +```{statemachine-diagram} tests.machines.showcase_parallel_compound.ParallelCompoundSC +:events: start, do_build +:caption: Build done +:target: +``` + +```{statemachine-diagram} tests.machines.showcase_parallel_compound.ParallelCompoundSC +:events: start, do_build, do_test +:caption: Pipeline done → Review +:target: +``` + + +### History states (shallow) + +A history pseudo-state remembers the last active child of a compound state. + +```{literalinclude} ../tests/machines/showcase_history.py +:pyobject: HistorySC +:language: python +``` + +```{statemachine-diagram} tests.machines.showcase_history.HistorySC +:caption: Class (Graphviz) +:target: +``` + +```{statemachine-diagram} tests.machines.showcase_history.HistorySC +:format: mermaid +:caption: Class (Mermaid) +``` + +```{statemachine-diagram} tests.machines.showcase_history.HistorySC +:events: begin, advance +:caption: Step2 +:target: +``` + +```{statemachine-diagram} tests.machines.showcase_history.HistorySC +:events: begin, advance, pause +:caption: Paused +:target: +``` + +```{statemachine-diagram} tests.machines.showcase_history.HistorySC +:events: begin, advance, pause, resume +:caption: Resumed (→Step2) +:target: +``` + + +### Deep history + +Deep history remembers the exact leaf state across nested compounds. + +```{literalinclude} ../tests/machines/showcase_deep_history.py +:pyobject: DeepHistorySC +:language: python +``` + +```{statemachine-diagram} tests.machines.showcase_deep_history.DeepHistorySC +:caption: Class (Graphviz) +:target: +``` + +```{statemachine-diagram} tests.machines.showcase_deep_history.DeepHistorySC +:format: mermaid +:caption: Class (Mermaid) +``` + +```{statemachine-diagram} tests.machines.showcase_deep_history.DeepHistorySC +:events: dive, enter_inner, go +:caption: Inner/B +:target: +``` + +```{statemachine-diagram} tests.machines.showcase_deep_history.DeepHistorySC +:events: dive, enter_inner, go, leave, restore +:caption: Restored (→Inner/B) +:target: +``` diff --git a/docs/events.md b/docs/events.md index 6db4c195..57351820 100644 --- a/docs/events.md +++ b/docs/events.md @@ -80,21 +80,31 @@ Every event has two string properties: - **`id`** — the programmatic identifier, derived from the class attribute name. Use this in `send()`, guards, and comparisons. -- **`name`** — a human-readable label for display purposes. Defaults to the `id` - when not explicitly set. +- **`name`** — a human-readable label for display purposes. Auto-generated from + the `id` by replacing `_` and `.` with spaces and capitalizing the first word. + You can override the automatic name by passing `name=` explicitly when + declaring the event: ```py >>> TrafficLight.cycle.id 'cycle' >>> TrafficLight.cycle.name -'cycle' +'Cycle' + +>>> class Example(StateChart): +... on = State(initial=True) +... off = State(final=True) +... shut_down = Event(on.to(off), name="Shut the system down") + +>>> Example.shut_down.name +'Shut the system down' ``` ```{tip} Always use `event.id` for programmatic checks. The `name` property is intended -for UI display and may change format in future versions. +for UI display and may differ from the `id`. ``` diff --git a/docs/images/order_control_machine_initial.png b/docs/images/order_control_machine_initial.png deleted file mode 100644 index e843ddf0..00000000 Binary files a/docs/images/order_control_machine_initial.png and /dev/null differ diff --git a/docs/images/order_control_machine_initial_300dpi.png b/docs/images/order_control_machine_initial_300dpi.png deleted file mode 100644 index c4c3bcb3..00000000 Binary files a/docs/images/order_control_machine_initial_300dpi.png and /dev/null differ diff --git a/docs/images/order_control_machine_processing.png b/docs/images/order_control_machine_processing.png deleted file mode 100644 index 747d5f78..00000000 Binary files a/docs/images/order_control_machine_processing.png and /dev/null differ diff --git a/docs/images/readme_orderworkflow.png b/docs/images/readme_orderworkflow.png deleted file mode 100644 index 47f3c631..00000000 Binary files a/docs/images/readme_orderworkflow.png and /dev/null differ diff --git a/docs/images/readme_trafficlightmachine.png b/docs/images/readme_trafficlightmachine.png index 2defa820..f5179c05 100644 Binary files a/docs/images/readme_trafficlightmachine.png and b/docs/images/readme_trafficlightmachine.png differ diff --git a/docs/images/transition_compound_cancel.png b/docs/images/transition_compound_cancel.png deleted file mode 100644 index 86ca6278..00000000 Binary files a/docs/images/transition_compound_cancel.png and /dev/null differ diff --git a/docs/images/transition_from_any.png b/docs/images/transition_from_any.png deleted file mode 100644 index a5d57039..00000000 Binary files a/docs/images/transition_from_any.png and /dev/null differ diff --git a/docs/images/tutorial_coffeeorder.png b/docs/images/tutorial_coffeeorder.png deleted file mode 100644 index 52659d6d..00000000 Binary files a/docs/images/tutorial_coffeeorder.png and /dev/null differ diff --git a/docs/integrations.md b/docs/integrations.md index bec7434b..fd362cee 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -39,6 +39,11 @@ You can attach it to a model by inheriting from `MachineMixin` and setting `state_machine_name` to the fully qualified class name: ``` py +>>> from statemachine import registry +>>> registry.register(CampaignMachine) # register for lookup by qualname + +>>> registry._initialized = True # skip Django autodiscovery in doctest + >>> class Workflow(MachineMixin): ... state_machine_name = '__main__.CampaignMachine' ... state_machine_attr = 'sm' @@ -47,13 +52,6 @@ You can attach it to a model by inheriting from `MachineMixin` and setting ... ... workflow_step = 1 -``` - -When a `Workflow` instance is created, it automatically receives a `CampaignMachine` -instance at the `state_machine_attr` attribute. The state value is read from and -written to the `state_field_name` field: - -``` py >>> model = Workflow() >>> isinstance(model.sm, CampaignMachine) @@ -70,6 +68,7 @@ True With `bind_events_as_methods = True`, events become methods on the model itself: ``` py +>>> model = Workflow() >>> model.produce() >>> model.workflow_step 2 diff --git a/docs/processing_model.md b/docs/processing_model.md index 50b6a998..d8181316 100644 --- a/docs/processing_model.md +++ b/docs/processing_model.md @@ -315,3 +315,50 @@ The machine starts, enters `trying` (attempt 1), and the eventless self-transition keeps firing as long as `can_retry()` returns `True`. Once the limit is reached, the second eventless transition fires — all within a single macrostep triggered by initialization. + + +(thread-safety)= + +## Thread safety + +State machines are **thread-safe** for concurrent event sending. Multiple threads +can call `send()` or trigger events on the **same state machine instance** +simultaneously — the engine guarantees correct behavior through its internal +locking mechanism. + +### How it works + +The processing loop uses a non-blocking lock (`threading.Lock`). When a thread +sends an event: + +1. The event is placed on the **external queue** (backed by a thread-safe + `PriorityQueue` from the standard library). +2. If no other thread is currently running the processing loop, the sending + thread acquires the lock and processes all queued events. +3. If another thread is already processing, the event is simply enqueued and + will be processed by the thread that holds the lock — no event is lost. + +This means that **at most one thread executes transitions at any time**, preserving +the run-to-completion (RTC) guarantee while allowing safe concurrent access. + +### What is safe + +- **Multiple threads sending events** to the same state machine instance. +- **Reading state** (`current_state_value`, `configuration`) from any thread + while events are being processed. Note that transient `None` values may be + observed for `current_state_value` during configuration updates when using + [`atomic_configuration_update`](behaviour.md#atomic_configuration_update) `= False` + (the default on `StateChart`, SCXML-compliant). With `atomic_configuration_update = True` + (the default on `StateMachine`), the configuration is updated atomically at + the end of the microstep, so `None` is not observed. +- **Invoke handlers** running in background threads or thread executors + communicate with the parent machine via the thread-safe event queue. + +### What to avoid + +- **Do not share a state machine instance across threads with the async engine** + unless you ensure only one event loop drives the machine. The async engine is + designed for `asyncio` concurrency, not thread-based concurrency. +- **Callbacks execute in the processing thread**, not in the thread that sent + the event. Design callbacks accordingly (e.g., use locks if they access + shared external state). diff --git a/docs/releases/1.0.1.md b/docs/releases/1.0.1.md index cccd852f..d32b6fcb 100644 --- a/docs/releases/1.0.1.md +++ b/docs/releases/1.0.1.md @@ -47,7 +47,9 @@ You can generate diagrams from your state machine. Example: -![OrderControl](../images/order_control_machine_initial.png) +```{statemachine-diagram} tests.examples.order_control_machine.OrderControl +:caption: OrderControl +``` ```{seealso} diff --git a/docs/releases/3.1.0.md b/docs/releases/3.1.0.md new file mode 100644 index 00000000..ca9814ab --- /dev/null +++ b/docs/releases/3.1.0.md @@ -0,0 +1,168 @@ +# StateChart 3.1.0 + +*Not released yet* + +## What's new in 3.1.0 + +### Text representations with `format()` + +State machines now support Python's built-in `format()` protocol. Use f-strings +or `format()` to get text representations — on both classes and instances: + +```python +f"{TrafficLightMachine:md}" +f"{sm:mermaid}" +format(sm, "rst") +``` + +Supported formats: + +| Format | Output | Requires | +|-----------|---------------------------|-----------------------| +| `dot` | Graphviz DOT source | `pydot` | +| `svg` | SVG markup (via Graphviz) | `pydot` + `graphviz` | +| `mermaid` | Mermaid stateDiagram-v2 | — | +| `md` | Markdown transition table | — | +| `rst` | RST transition table | — | + +See {ref}`diagram:Text representations` for details. + + +### Formatter facade + +A new `Formatter` facade with decorator-based registration unifies all text +format rendering behind a single API. Adding a new format requires only +registering a render function — no changes to `__format__`, the CLI, or the +Sphinx directive: + +```python +from statemachine.contrib.diagram import formatter + +formatter.render(sm, "mermaid") +formatter.supported_formats() + +@formatter.register_format("custom") +def _render_custom(machine_or_class): + ... +``` + +See {ref}`formatter-api` for details. + + +### Mermaid diagram support + +State machines can now be rendered as +[Mermaid `stateDiagram-v2`](https://mermaid.js.org/syntax/stateDiagram.html) +source text — no Graphviz installation required. Supports compound states, +parallel regions, history states, guards, and active-state highlighting. + +Three ways to use it: + +- **f-strings:** `f"{sm:mermaid}"` +- **CLI:** `python -m statemachine.contrib.diagram MyMachine - --format mermaid` +- **Sphinx directive:** `:format: mermaid` renders via `sphinxcontrib-mermaid`. + +See {ref}`diagram:Mermaid format` for details. + + +### Auto-expanding docstrings + +Use `{statechart:FORMAT}` placeholders in your class docstring to embed a +live representation of the state machine. The placeholder is replaced at +class definition time, so the docstring always stays in sync with the code: + +```python +class TrafficLight(StateChart): + """A traffic light. + + {statechart:md} + """ + green = State(initial=True) + yellow = State() + red = State() + cycle = green.to(yellow) | yellow.to(red) | red.to(green) +``` + +Any registered format works: `md`, `rst`, `mermaid`, `dot`, etc. +Works with Sphinx autodoc — the expanded docstring is what gets rendered. +See {ref}`diagram:Auto-expanding docstrings` for details. + + +### Sphinx directive for inline diagrams + +A new Sphinx extension renders state machine diagrams directly in your +documentation from an importable class path — no manual image generation +needed. + +Add `"statemachine.contrib.diagram.sphinx_ext"` to your `conf.py` +extensions, then use the directive in any MyST Markdown page: + +````markdown +```{statemachine-diagram} myproject.machines.OrderControl +:events: receive_payment +:caption: After payment +:target: +``` +```` + +The directive supports the same options as the standard `image`/`figure` +directives (`:width:`, `:height:`, `:scale:`, `:align:`, `:target:`, +`:class:`, `:name:`), plus `:events:` to instantiate the machine and send +events before rendering (highlighting the current state). + +Using `:target:` without a value makes the diagram clickable, opening the +full SVG in a new browser tab for zooming — useful for large statecharts. + +The `:format: mermaid` option renders via `sphinxcontrib-mermaid` instead of +Graphviz. + +See {ref}`diagram:Sphinx directive` for full documentation. +[#589](https://github.com/fgmacedo/python-statemachine/pull/589). + + +### Diagram CLI `--events` and `--format` options + +The `python -m statemachine.contrib.diagram` command now accepts: + +- `--events` to instantiate the machine and send events before rendering, + highlighting the current active state. +- `--format` to choose the output format (`mermaid`, `md`, `rst`, `dot`, `svg`, + or image formats via Graphviz). Use `-` as the output path to write text + formats to stdout. + +See {ref}`diagram:Command line` for details. +[#593](https://github.com/fgmacedo/python-statemachine/pull/593). + + +### Performance: 5x–7x faster event processing + +The engine's hot paths have been systematically profiled and optimized, resulting in +**4.7x–7.7x faster event throughput** and **1.9x–2.6x faster setup** across all +machine types. All optimizations are internal — no public API changes. +See [#592](https://github.com/fgmacedo/python-statemachine/pull/592) for details. + + +### Thread safety documentation + +The sync engine is thread-safe: multiple threads can send events to the same state +machine instance concurrently. This is now documented in the +{ref}`processing model ` and verified by stress tests. +[#592](https://github.com/fgmacedo/python-statemachine/pull/592). + + +### Bugfixes in 3.1.0 + +- Fixes silent misuse of `Event()` with multiple positional arguments. Passing more than one + transition to `Event()` (e.g., `Event(t1, t2)`) now raises `InvalidDefinition` with a + clear message suggesting the `|` operator. Previously, the second argument was silently + interpreted as the event `id`, leaving the extra transitions eventless (auto-firing). + [#588](https://github.com/fgmacedo/python-statemachine/pull/588). + +- `Event.name` is now auto-humanized from the `id` (e.g., `cycle` → `Cycle`, + `pick_up` → `Pick up`). Diagrams, Mermaid output, and text tables all display + the human-readable name. Explicit `name=` values are preserved. The same + `humanize_id()` helper is now shared by `Event` and `State`. + [#601](https://github.com/fgmacedo/python-statemachine/pull/601), + fixes [#600](https://github.com/fgmacedo/python-statemachine/issues/600). + +## Misc in 3.1.0 diff --git a/docs/releases/index.md b/docs/releases/index.md index 868f394e..3eeef892 100644 --- a/docs/releases/index.md +++ b/docs/releases/index.md @@ -16,6 +16,7 @@ Requires Python 3.9+. ```{toctree} :maxdepth: 2 +3.1.0 3.0.0 ``` diff --git a/docs/transitions.md b/docs/transitions.md index d5322a70..aa1809b5 100644 --- a/docs/transitions.md +++ b/docs/transitions.md @@ -171,16 +171,14 @@ True Compare the diagrams — both model the same behavior, but the compound version makes the "cancellable" grouping explicit in the hierarchy: -```py ->>> getfixture("requires_dot_installed") ->>> OrderWorkflow()._graph().write_png("docs/images/transition_from_any.png") ->>> OrderWorkflowCompound()._graph().write_png("docs/images/transition_compound_cancel.png") - +```{statemachine-diagram} tests.machines.transition_from_any.OrderWorkflow +:caption: from_.any() ``` -| `from_.any()` | Compound | -|---|---| -| ![from_.any()](images/transition_from_any.png) | ![Compound](images/transition_compound_cancel.png) | +```{statemachine-diagram} tests.machines.transition_from_any.OrderWorkflowCompound +:caption: Compound +:target: +``` The compound approach scales better as you add more states — no need to remember to include each new state in a `from_()` list. diff --git a/docs/tutorial.md b/docs/tutorial.md index 3e150e71..3bfe1489 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -345,46 +345,75 @@ factories, and the full list of listener callbacks. ## Generating diagrams -Visualize any state machine as a diagram. Install the `diagrams` extra -first: +Visualize any state machine as a diagram: +```{statemachine-diagram} tests.machines.tutorial_coffee_order.CoffeeOrder +:alt: CoffeeOrder diagram ``` -pip install python-statemachine[diagrams] + +Generate diagrams programmatically with `_graph()`: + +```python +order = CoffeeOrder() +order._graph().write_png("order.png") +``` + +Or from the command line: + +```bash +python -m statemachine.contrib.diagram my_module.CoffeeOrder order.png ``` -Then generate an image at runtime: +### Text representations with `format()` + +You can also get text representations of any state machine using Python's built-in +`format()` or f-strings — no Graphviz needed: ```py ->>> getfixture("requires_dot_installed") +>>> from tests.machines.tutorial_coffee_order import CoffeeOrder ->>> from statemachine import StateChart, State +>>> print(f"{CoffeeOrder:md}") +| State | Event | Guard | Target | +| --------- | ------- | ----- | --------- | +| Pending | Start | | Preparing | +| Preparing | Finish | | Ready | +| Ready | Pick up | | Picked up | ->>> class CoffeeOrder(StateChart): -... pending = State(initial=True) -... preparing = State() -... ready = State() -... picked_up = State(final=True) -... -... start = pending.to(preparing) -... finish = preparing.to(ready) -... pick_up = ready.to(picked_up) +``` ->>> order = CoffeeOrder() ->>> order._graph().write_png("docs/images/tutorial_coffeeorder.png") +Supported formats include `mermaid`, `md` (markdown table), `rst`, `dot`, and `svg`. +Works on both classes and instances: + +```py +>>> print(f"{CoffeeOrder:mermaid}") +stateDiagram-v2 + direction LR + state "Pending" as pending + state "Preparing" as preparing + state "Ready" as ready + state "Picked up" as picked_up + [*] --> pending + picked_up --> [*] + pending --> preparing : Start + preparing --> ready : Finish + ready --> picked_up : Pick up + ``` -![CoffeeOrder](images/tutorial_coffeeorder.png) +```{tip} +Graphviz diagram generation requires [Graphviz](https://graphviz.org/) (`dot` command) +and the `diagrams` extra: -Or from the command line: + pip install python-statemachine[diagrams] -```bash -python -m statemachine.contrib.diagram my_module.CoffeeOrder order.png +Text formats (`md`, `rst`, `mermaid`) work without any extra dependencies. ``` ```{seealso} -See [](diagram.md) for Jupyter integration, SVG output, DPI settings, and -the `quickchart_write_svg` alternative that doesn't require Graphviz. +See [](diagram.md) for all formats, highlighting active states, auto-expanding +docstrings, Jupyter integration, Sphinx directive, and the `quickchart_write_svg` +alternative that doesn't require Graphviz. ``` diff --git a/pyproject.toml b/pyproject.toml index 8e126525..40b3b9c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "python-statemachine" -version = "3.0.0" +version = "3.1.0" description = "Python Finite State Machines made easy." authors = [{ name = "Fernando Macedo", email = "fgmacedo@gmail.com" }] maintainers = [{ name = "Fernando Macedo", email = "fgmacedo@gmail.com" }] @@ -52,6 +52,7 @@ dev = [ "sphinx-autobuild; python_version >'3.8'", "furo >=2024.5.6; python_version >'3.8'", "sphinx-copybutton >=0.5.2; python_version >'3.8'", + "sphinxcontrib-mermaid; python_version >'3.8'", "pdbr>=0.8.9; python_version >'3.8'", "babel >=2.16.0; python_version >='3.8'", "pytest-xdist>=3.6.1", @@ -88,14 +89,15 @@ markers = [ ] python_files = ["tests.py", "test_*.py", "*_tests.py"] xfail_strict = true -log_cli = true -log_cli_level = "DEBUG" +# Log level WARNING by default; the engine caches a no-op for logger.debug at +# init time, so DEBUG here would bypass that optimization and slow benchmarks. +# To enable DEBUG logging for a specific test run: +# uv run pytest -o log_cli_level=DEBUG +log_cli_level = "WARNING" log_cli_format = "%(relativeCreated)6.0fms %(threadName)-18s %(name)-35s %(message)s" log_cli_date_format = "%H:%M:%S" asyncio_default_fixture_loop_scope = "module" -filterwarnings = [ - "ignore::pytest_benchmark.logger.PytestBenchmarkWarning", -] +filterwarnings = ["ignore::pytest_benchmark.logger.PytestBenchmarkWarning"] [tool.coverage.run] branch = true @@ -119,6 +121,7 @@ exclude_lines = [ "raise AssertionError", "raise NotImplementedError", "if TYPE_CHECKING", + 'if __name__ == "__main__"', ] [tool.coverage.html] @@ -133,7 +136,14 @@ disable_error_code = "annotation-unchecked" mypy_path = "$MYPY_CONFIG_FILE_DIR/tests/django_project" [[tool.mypy.overrides]] -module = ['django.*', 'pytest.*', 'pydot.*', 'sphinx_gallery.*'] +module = [ + 'django.*', + 'pytest.*', + 'pydot.*', + 'sphinx_gallery.*', + 'docutils.*', + 'sphinx.*', +] ignore_missing_imports = true [tool.ruff] diff --git a/statemachine/__init__.py b/statemachine/__init__.py index 6993e7cf..7e0deac6 100644 --- a/statemachine/__init__.py +++ b/statemachine/__init__.py @@ -8,7 +8,7 @@ __author__ = """Fernando Macedo""" __email__ = "fgmacedo@gmail.com" -__version__ = "3.0.0" +__version__ = "3.1.0" __all__ = [ "StateChart", diff --git a/statemachine/configuration.py b/statemachine/configuration.py new file mode 100644 index 00000000..c17123aa --- /dev/null +++ b/statemachine/configuration.py @@ -0,0 +1,162 @@ +from typing import TYPE_CHECKING +from typing import Any +from typing import Dict +from typing import Mapping +from typing import MutableSet + +from .exceptions import InvalidStateValue +from .i18n import _ +from .orderedset import OrderedSet + +_SENTINEL = object() + +if TYPE_CHECKING: + from .state import State + + +class Configuration: + """Encapsulates the dual representation of the active state configuration. + + Internally, ``current_state_value`` is either a scalar (single active state) + or an ``OrderedSet`` (parallel regions). This class hides that detail behind + a uniform interface for reading, mutating, and caching the resolved + ``OrderedSet[State]``. + """ + + __slots__ = ( + "_instance_states", + "_model", + "_state_field", + "_states_map", + "_cached", + "_cached_value", + ) + + def __init__( + self, + instance_states: "Mapping[str, State]", + model: Any, + state_field: str, + states_map: "Dict[Any, State]", + ): + self._instance_states = instance_states + self._model = model + self._state_field = state_field + self._states_map = states_map + self._cached: "OrderedSet[State] | None" = None + self._cached_value: Any = _SENTINEL + + # -- Raw value (persisted on the model) ------------------------------------ + + @property + def value(self) -> Any: + """The raw state value stored on the model (scalar or ``OrderedSet``).""" + return getattr(self._model, self._state_field, None) + + @value.setter + def value(self, val: Any): + if val is None: + self._write_to_model(OrderedSet()) + elif isinstance(val, MutableSet): + self._write_to_model(OrderedSet(val) if not isinstance(val, OrderedSet) else val) + else: + self._write_to_model(OrderedSet([val])) + + @property + def values(self) -> OrderedSet[Any]: + """The set of raw state values currently active.""" + return self._read_from_model() + + # -- Resolved states ------------------------------------------------------- + + @property + def states(self) -> "OrderedSet[State]": + """The set of currently active :class:`State` instances (cached).""" + raw = self.value + if self._cached is not None and self._cached_value is raw: + return self._cached + if raw is None: + return OrderedSet() + + # Normalize inline (avoid second getattr via _read_from_model) + values = raw if isinstance(raw, MutableSet) else (raw,) + result = OrderedSet(self._instance_states[self._states_map[v].id] for v in values) + self._cached = result + self._cached_value = raw + return result + + @states.setter + def states(self, new_configuration: "OrderedSet[State]"): + self._write_to_model(OrderedSet(s.value for s in new_configuration)) + + # -- Incremental mutation (used by the engine) ----------------------------- + + def add(self, state: "State"): + """Add *state* to the configuration.""" + values = self._read_from_model() + values.add(state.value) + self._write_to_model(values) + + def discard(self, state: "State"): + """Remove *state* from the configuration.""" + values = self._read_from_model() + values.discard(state.value) + self._write_to_model(values) + + # -- Deprecated v2 compat -------------------------------------------------- + + @property + def current_state(self) -> "State | OrderedSet[State]": + """Resolve the current state with validation. + + Unlike ``states`` (which returns an empty set for ``None``), this + raises ``InvalidStateValue`` when the value is ``None`` or not + found in ``states_map`` — matching the v2 ``current_state`` contract. + """ + csv = self.value + if csv is None: + raise InvalidStateValue( + csv, + _( + "There's no current state set. In async code, " + "did you activate the initial state? " + "(e.g., `await sm.activate_initial_state()`)" + ), + ) + try: + config = self.states + if len(config) == 1: + return next(iter(config)) + return config + except KeyError as err: + raise InvalidStateValue(csv) from err + + # -- Internal: model boundary ---------------------------------------------- + + def _read_from_model(self) -> OrderedSet: + """Normalize: model value → always ``OrderedSet``.""" + raw = self.value + if raw is None: + return OrderedSet() + if isinstance(raw, OrderedSet): + return raw + if isinstance(raw, MutableSet): + return OrderedSet(raw) + return OrderedSet([raw]) + + def _write_to_model(self, values: OrderedSet): + """Denormalize: ``OrderedSet`` → ``None | scalar | OrderedSet`` for model.""" + self._invalidate() + if len(values) == 0: + raw = None + elif len(values) == 1: + raw = next(iter(values)) + else: + raw = values + if raw is not None and not isinstance(raw, MutableSet) and raw not in self._states_map: + raise InvalidStateValue(raw) + setattr(self._model, self._state_field, raw) + + def _invalidate(self): + self._cached = None + self._cached_value = _SENTINEL diff --git a/statemachine/contrib/diagram.py b/statemachine/contrib/diagram.py deleted file mode 100644 index 1ec59804..00000000 --- a/statemachine/contrib/diagram.py +++ /dev/null @@ -1,370 +0,0 @@ -import importlib -import sys -from urllib.parse import quote -from urllib.request import urlopen - -import pydot - -from ..statemachine import StateChart - - -class DotGraphMachine: - graph_rankdir = "LR" - """ - Direction of the graph. Defaults to "LR" (option "TB" for top bottom) - http://www.graphviz.org/doc/info/attrs.html#d:rankdir - """ - - font_name = "Arial" - """Graph font face name""" - - state_font_size = "10pt" - """State font size""" - - state_active_penwidth = 2 - """Active state external line width""" - - state_active_fillcolor = "turquoise" - - transition_font_size = "9pt" - """Transition font size""" - - def __init__(self, machine): - self.machine = machine - - def _get_graph(self, machine): - return pydot.Dot( - machine.name, - graph_type="digraph", - label=machine.name, - fontname=self.font_name, - fontsize=self.state_font_size, - rankdir=self.graph_rankdir, - compound="true", - ) - - def _get_subgraph(self, state): - style = ", solid" - if state.parent and state.parent.parallel: - style = ", dashed" - label = state.name - if state.parallel: - label = f"<{state.name} ☷>" - subgraph = pydot.Subgraph( - label=label, - graph_name=f"cluster_{state.id}", - style=f"rounded{style}", - cluster="true", - ) - return subgraph - - def _initial_node(self, state): - node = pydot.Node( - self._state_id(state), - label="", - shape="point", - style="filled", - fontsize="1pt", - fixedsize="true", - width=0.2, - height=0.2, - ) - node.set_fillcolor("black") # type: ignore[attr-defined] - return node - - def _initial_edge(self, initial_node, state): - extra_params = {} - if state.states: - extra_params["lhead"] = f"cluster_{state.id}" - return pydot.Edge( - initial_node.get_name(), - self._state_id(state), - label="", - color="blue", - fontname=self.font_name, - fontsize=self.transition_font_size, - **extra_params, - ) - - def _actions_getter(self): - if isinstance(self.machine, StateChart): - - def getter(grouper): # pyright: ignore[reportRedeclaration] - return self.machine._callbacks.str(grouper.key) - else: - - def getter(grouper): - all_names = set(dir(self.machine)) - return ", ".join( - str(c) for c in grouper if not c.is_convention or c.func in all_names - ) - - return getter - - def _state_actions(self, state): - getter = self._actions_getter() - - entry = str(getter(state.enter)) - exit_ = str(getter(state.exit)) - internal = ", ".join( - f"{transition.event} / {str(getter(transition.on))}" - for transition in state.transitions - if transition.internal - ) - - if entry: - entry = f"entry / {entry}" - if exit_: - exit_ = f"exit / {exit_}" - - actions = "\n".join(x for x in [entry, exit_, internal] if x) - - if actions: - actions = f"\n{actions}" - - return actions - - @staticmethod - def _state_id(state): - if state.states: - return f"{state.id}_anchor" - else: - return state.id - - def _history_node(self, state): - label = "H*" if state.type.is_deep else "H" - return pydot.Node( - self._state_id(state), - label=label, - shape="circle", - style="filled", - fillcolor="white", - fontname=self.font_name, - fontsize="8pt", - fixedsize="true", - width=0.3, - height=0.3, - ) - - def _state_as_node(self, state): - actions = self._state_actions(state) - - node = pydot.Node( - self._state_id(state), - label=f"{state.name}{actions}", - shape="rectangle", - style="rounded, filled", - fontname=self.font_name, - fontsize=self.state_font_size, - peripheries=2 if state.final else 1, - ) - if ( - isinstance(self.machine, StateChart) - and state.value in self.machine.configuration_values - ): - node.set_penwidth(self.state_active_penwidth) # type: ignore[attr-defined] - node.set_fillcolor(self.state_active_fillcolor) # type: ignore[attr-defined] - else: - node.set_fillcolor("white") # type: ignore[attr-defined] - return node - - def _transition_as_edges(self, transition): - targets = transition.targets if transition.targets else [None] - cond = ", ".join([str(c) for c in transition.cond]) - if cond: - cond = f"\n[{cond}]" - - edges = [] - for i, target in enumerate(targets): - extra_params = {} - has_substates = transition.source.states or (target and target.states) - if transition.source.states: - extra_params["ltail"] = f"cluster_{transition.source.id}" - if target and target.states: - extra_params["lhead"] = f"cluster_{target.id}" - - targetless = target is None - label = f"{transition.event}{cond}" if i == 0 else "" - dst = self._state_id(target) if not targetless else self._state_id(transition.source) - edges.append( - pydot.Edge( - self._state_id(transition.source), - dst, - label=label, - color="blue", - fontname=self.font_name, - fontsize=self.transition_font_size, - minlen=2 if has_substates else 1, - **extra_params, - ) - ) - return edges - - def get_graph(self): - graph = self._get_graph(self.machine) - self._graph_states(self.machine, graph) - return graph - - def _add_transitions(self, graph, state): - for transition in state.transitions: - if transition.internal: - continue - for edge in self._transition_as_edges(transition): - graph.add_edge(edge) - - def _graph_states(self, state, graph): - initial_node = self._initial_node(state) - initial_subgraph = pydot.Subgraph( - graph_name=f"{initial_node.get_name()}_initial", - label="", - peripheries=0, - margin=0, - ) - atomic_states_subgraph = pydot.Subgraph( - graph_name=f"cluster_{initial_node.get_name()}_atomic", - label="", - peripheries=0, - cluster="true", - ) - initial_subgraph.add_node(initial_node) - graph.add_subgraph(initial_subgraph) - graph.add_subgraph(atomic_states_subgraph) - - if state.states and not getattr(state, "parallel", False): - initial = next((s for s in state.states if s.initial), None) - if initial: # pragma: no branch - graph.add_edge(self._initial_edge(initial_node, initial)) - - for substate in state.states: - if substate.states: - subgraph = self._get_subgraph(substate) - self._graph_states(substate, subgraph) - graph.add_subgraph(subgraph) - else: - atomic_states_subgraph.add_node(self._state_as_node(substate)) - self._add_transitions(graph, substate) - - for history_state in getattr(state, "history", []): - atomic_states_subgraph.add_node(self._history_node(history_state)) - self._add_transitions(graph, history_state) - - def __call__(self): - return self.get_graph() - - -def quickchart_write_svg(sm: StateChart, path: str): - """ - If the default dependency of GraphViz installed locally doesn't work for you. As an option, - you can generate the image online from the output of the `dot` language, - using one of the many services available. - - To get the **dot** representation of your state machine is as easy as follows: - - >>> from tests.examples.order_control_machine import OrderControl - >>> sm = OrderControl() - >>> print(sm._graph().to_string()) - digraph OrderControl { - compound=true; - fontname=Arial; - fontsize="10pt"; - label=OrderControl; - rankdir=LR; - ... - - To give you an example, we included this method that will serialize the dot, request the graph - to https://quickchart.io, and persist the result locally as an ``.svg`` file. - - - .. warning:: - Quickchart is an external graph service that supports many formats to generate diagrams. - - By using this method, you should trust http://quickchart.io. - - Please read https://quickchart.io/documentation/faq/ for more information. - - >>> quickchart_write_svg(sm, "docs/images/oc_machine_processing.svg") # doctest: +SKIP - - """ - dot_representation = sm._graph().to_string() - - url = f"https://quickchart.io/graphviz?graph={quote(dot_representation)}" - - response = urlopen(url) - data = response.read() - - with open(path, "wb") as f: - f.write(data) - - -def _find_sm_class(module): - """Find the first StateChart subclass defined in a module.""" - import inspect - - for _name, obj in inspect.getmembers(module, inspect.isclass): - if ( - issubclass(obj, StateChart) - and obj is not StateChart - and obj.__module__ == module.__name__ - ): - return obj - return None - - -def import_sm(qualname): - module_name, class_name = qualname.rsplit(".", 1) - module = importlib.import_module(module_name) - smclass = getattr(module, class_name, None) - if smclass is not None and isinstance(smclass, type) and issubclass(smclass, StateChart): - return smclass - - # qualname may be a module path without a class name — try importing - # the whole path as a module and find the first StateChart subclass. - try: - module = importlib.import_module(qualname) - except ImportError as err: - raise ValueError(f"{class_name} is not a subclass of StateMachine") from err - - smclass = _find_sm_class(module) - if smclass is None: - raise ValueError(f"No StateMachine subclass found in module {qualname!r}") - - return smclass - - -def write_image(qualname, out): - """ - Given a `qualname`, that is the fully qualified dotted path to a StateMachine - classes, imports the class and generates a dot graph using the `pydot` lib. - Writes the graph representation to the filename 'out' that will - open/create and truncate such file and write on it a representation of - the graph defined by the statemachine, in the format specified by - the extension contained in the out path (out.ext). - """ - smclass = import_sm(qualname) - - graph = DotGraphMachine(smclass).get_graph() - out_extension = out.rsplit(".", 1)[1] - graph.write(out, format=out_extension) - - -def main(argv=None): - import argparse - - parser = argparse.ArgumentParser( - usage="%(prog)s [OPTION] ", - description="Generate diagrams for StateMachine classes.", - ) - parser.add_argument( - "class_path", help="A fully-qualified dotted path to the StateMachine class." - ) - parser.add_argument( - "out", - help="File to generate the image using extension as the output format.", - ) - - args = parser.parse_args(argv) - write_image(qualname=args.class_path, out=args.out) - - -if __name__ == "__main__": # pragma: no cover - sys.exit(main()) diff --git a/statemachine/contrib/diagram/__init__.py b/statemachine/contrib/diagram/__init__.py new file mode 100644 index 00000000..c6317304 --- /dev/null +++ b/statemachine/contrib/diagram/__init__.py @@ -0,0 +1,237 @@ +import importlib +from urllib.parse import quote +from urllib.request import urlopen + +from .extract import extract +from .formatter import formatter as formatter +from .renderers.dot import DotRenderer +from .renderers.dot import DotRendererConfig +from .renderers.mermaid import MermaidRenderer +from .renderers.mermaid import MermaidRendererConfig + + +class DotGraphMachine: + """Backwards-compatible facade that uses the extract + render pipeline. + + Maintains the same public API and class-level customization attributes + as the original monolithic DotGraphMachine. + """ + + graph_rankdir = "LR" + """ + Direction of the graph. Defaults to "LR" (option "TB" for top bottom) + http://www.graphviz.org/doc/info/attrs.html#d:rankdir + """ + + font_name = "Helvetica" + """Graph font face name""" + + state_font_size = "10" + """State font size""" + + state_active_penwidth = 2 + """Active state external line width""" + + state_active_fillcolor = "turquoise" + + transition_font_size = "9" + """Transition font size""" + + def __init__(self, machine): + self.machine = machine + + def _build_config(self) -> DotRendererConfig: + return DotRendererConfig( + graph_rankdir=self.graph_rankdir, + font_name=self.font_name, + state_font_size=self.state_font_size, + state_active_penwidth=self.state_active_penwidth, + state_active_fillcolor=self.state_active_fillcolor, + transition_font_size=self.transition_font_size, + ) + + def get_graph(self): + ir = extract(self.machine) + renderer = DotRenderer(config=self._build_config()) + return renderer.render(ir) + + def __call__(self): + return self.get_graph() + + +class MermaidGraphMachine: + """Facade for generating Mermaid stateDiagram-v2 source from a state machine.""" + + direction = "LR" + active_fill = "#40E0D0" + active_stroke = "#333" + + def __init__(self, machine): + self.machine = machine + + def _build_config(self) -> MermaidRendererConfig: + return MermaidRendererConfig( + direction=self.direction, + active_fill=self.active_fill, + active_stroke=self.active_stroke, + ) + + def get_mermaid(self) -> str: + ir = extract(self.machine) + renderer = MermaidRenderer(config=self._build_config()) + return renderer.render(ir) + + def __call__(self) -> str: + return self.get_mermaid() + + +def quickchart_write_svg(sm, path: str): + """ + If the default dependency of GraphViz installed locally doesn't work for you. As an option, + you can generate the image online from the output of the `dot` language, + using one of the many services available. + + To get the **dot** representation of your state machine is as easy as follows: + + >>> from tests.examples.order_control_machine import OrderControl + >>> sm = OrderControl() + >>> print(sm._graph().to_string()) # doctest: +ELLIPSIS + digraph OrderControl { + ... + } + + To give you an example, we included this method that will serialize the dot, request the graph + to https://quickchart.io, and persist the result locally as an ``.svg`` file. + + + .. warning:: + Quickchart is an external graph service that supports many formats to generate diagrams. + + By using this method, you should trust http://quickchart.io. + + Please read https://quickchart.io/documentation/faq/ for more information. + + >>> quickchart_write_svg(sm, "docs/images/oc_machine_processing.svg") # doctest: +SKIP + + """ + dot_representation = sm._graph().to_string() + + url = f"https://quickchart.io/graphviz?graph={quote(dot_representation)}" + + response = urlopen(url) + data = response.read() + + with open(path, "wb") as f: + f.write(data) + + +def _find_sm_class(module): + """Find the first StateChart subclass defined in a module.""" + import inspect + + from statemachine.statemachine import StateChart + + for _name, obj in inspect.getmembers(module, inspect.isclass): + if ( + issubclass(obj, StateChart) + and obj is not StateChart + and obj.__module__ == module.__name__ + ): + return obj + return None + + +def import_sm(qualname): + from statemachine.statemachine import StateChart + + module_name, class_name = qualname.rsplit(".", 1) + module = importlib.import_module(module_name) + smclass = getattr(module, class_name, None) + if smclass is not None and isinstance(smclass, type) and issubclass(smclass, StateChart): + return smclass + + # qualname may be a module path without a class name — try importing + # the whole path as a module and find the first StateChart subclass. + try: + module = importlib.import_module(qualname) + except ImportError as err: + raise ValueError(f"{class_name} is not a subclass of StateMachine") from err + + smclass = _find_sm_class(module) + if smclass is None: + raise ValueError(f"No StateMachine subclass found in module {qualname!r}") + + return smclass + + +def write_image(qualname, out, events=None, fmt=None): + """ + Given a `qualname`, that is the fully qualified dotted path to a StateMachine + classes, imports the class and generates a dot graph using the `pydot` lib. + Writes the graph representation to the filename 'out' that will + open/create and truncate such file and write on it a representation of + the graph defined by the statemachine, in the format specified by + the extension contained in the out path (out.ext). + + If `events` is provided, the machine is instantiated and each event is sent + before rendering, so the diagram highlights the current active state. + + If `fmt` is provided, it overrides the output format (any registered text + format such as ``"mermaid"``, ``"dot"``, ``"md"``, ``"rst"``). + Use ``out="-"`` to write to stdout. + """ + import sys + + smclass = import_sm(qualname) + + if events: + machine = smclass() + for event_name in events: + machine.send(event_name) + else: + machine = smclass + + if fmt is not None: + text = formatter.render(machine, fmt) + if out == "-": + sys.stdout.write(text) + else: + with open(out, "w") as f: + f.write(text) + else: + graph = DotGraphMachine(machine).get_graph() + if out == "-": + sys.stdout.buffer.write(graph.create_svg()) # type: ignore[attr-defined] + else: + out_extension = out.rsplit(".", 1)[1] + graph.write(out, format=out_extension) + + +def main(argv=None): + import argparse + + parser = argparse.ArgumentParser( + usage="%(prog)s [OPTION] ", + description="Generate diagrams for StateMachine classes.", + ) + parser.add_argument( + "class_path", help="A fully-qualified dotted path to the StateMachine class." + ) + parser.add_argument( + "out", + help="File to generate the image using extension as the output format.", + ) + parser.add_argument( + "--events", + nargs="+", + help="Instantiate the machine and send these events before rendering.", + ) + parser.add_argument( + "--format", + choices=formatter.supported_formats(), + default=None, + help="Output as text format instead of Graphviz image.", + ) + + args = parser.parse_args(argv) + write_image(qualname=args.class_path, out=args.out, events=args.events, fmt=args.format) diff --git a/statemachine/contrib/diagram/__main__.py b/statemachine/contrib/diagram/__main__.py new file mode 100644 index 00000000..daf509ab --- /dev/null +++ b/statemachine/contrib/diagram/__main__.py @@ -0,0 +1,6 @@ +import sys + +from . import main + +if __name__ == "__main__": + sys.exit(main()) diff --git a/statemachine/contrib/diagram/extract.py b/statemachine/contrib/diagram/extract.py new file mode 100644 index 00000000..45caf9e5 --- /dev/null +++ b/statemachine/contrib/diagram/extract.py @@ -0,0 +1,288 @@ +from typing import TYPE_CHECKING +from typing import List +from typing import Set +from typing import Union + +from .model import ActionType +from .model import DiagramAction +from .model import DiagramGraph +from .model import DiagramState +from .model import DiagramTransition +from .model import StateType + +if TYPE_CHECKING: + from statemachine.state import State + from statemachine.statemachine import StateChart + from statemachine.transition import Transition + + # A StateChart class or instance — both expose the same structural metadata. + MachineRef = Union["StateChart", "type[StateChart]"] + + +def _determine_state_type(state: "State") -> StateType: + from statemachine.state import HistoryState + from statemachine.state import HistoryType + + if isinstance(state, HistoryState): + if state.type == HistoryType.DEEP: + return StateType.HISTORY_DEEP + return StateType.HISTORY_SHALLOW + if getattr(state, "parallel", False): + return StateType.PARALLEL + if state.final: + return StateType.FINAL + return StateType.REGULAR + + +def _actions_getter(machine: "MachineRef"): + from statemachine.statemachine import StateChart + + if isinstance(machine, StateChart): + + def getter(grouper): # pyright: ignore[reportRedeclaration] + return machine._callbacks.str(grouper.key) + else: + + def getter(grouper): + all_names = set(dir(machine)) + return ", ".join(str(c) for c in grouper if not c.is_convention or c.func in all_names) + + return getter + + +def _extract_state_actions(state: "State", getter) -> List[DiagramAction]: + actions: List[DiagramAction] = [] + + entry = str(getter(state.enter)) + exit_ = str(getter(state.exit)) + + if entry: + actions.append(DiagramAction(type=ActionType.ENTRY, body=entry)) + if exit_: + actions.append(DiagramAction(type=ActionType.EXIT, body=exit_)) + + for transition in state.transitions: + if transition.internal: + on_text = str(getter(transition.on)) + if on_text: + actions.append( + DiagramAction(type=ActionType.INTERNAL, body=f"{transition.event} / {on_text}") + ) + + return actions + + +def _extract_state( + state: "State", + machine: "MachineRef", + getter, + active_values: set, +) -> DiagramState: + state_type = _determine_state_type(state) + is_active = state.value in active_values + is_parallel_area = bool(state.parent and getattr(state.parent, "parallel", False)) + + children: List[DiagramState] = [] + for substate in state.states: + children.append(_extract_state(substate, machine, getter, active_values)) + for history_state in getattr(state, "history", []): + children.append(_extract_state(history_state, machine, getter, active_values)) + + actions = _extract_state_actions(state, getter) + + return DiagramState( + id=state.id, + name=state.name, + type=state_type, + actions=actions, + children=children, + is_active=is_active, + is_parallel_area=is_parallel_area, + is_initial=getattr(state, "initial", False), + ) + + +def _format_event_names(transition: "Transition") -> str: + """Build a display string for the events that trigger a transition. + + ``_expand_event_id`` registers both the Python attribute name + (``done_invoke_X``) and the SCXML dot form (``done.invoke.X``) under the + same transition. For diagram display we only want unique *semantic* events, + keeping the Python attribute name when an alias pair exists. + """ + events = list(transition.events) + if not events: + return "" + + all_ids = {str(e) for e in events} + + seen_ids: Set[str] = set() + display: List[str] = [] + for event in events: + eid = str(event) + # Skip dot-form aliases (e.g. "done.invoke.X") when the underscore + # form ("done_invoke_X") is also registered on this transition. + if "." in eid and eid.replace(".", "_") in all_ids: + continue + if eid not in seen_ids: # pragma: no branch + seen_ids.add(eid) + display.append(event.name if event.name else eid) + + return " ".join(display) + + +def _extract_transitions_from_state(state: "State") -> List[DiagramTransition]: + """Extract transitions from a single state (non-recursive).""" + result: List[DiagramTransition] = [] + for transition in state.transitions: + targets = transition.targets if transition.targets else [] + target_ids = [t.id for t in targets] + + cond_strs = [str(c) for c in transition.cond] + + result.append( + DiagramTransition( + source=transition.source.id, + targets=target_ids, + event=_format_event_names(transition), + guards=cond_strs, + is_internal=transition.internal, + ) + ) + return result + + +def _extract_all_transitions(states) -> List[DiagramTransition]: + """Recursively extract transitions from all states.""" + result: List[DiagramTransition] = [] + for state in states: + result.extend(_extract_transitions_from_state(state)) + if state.states: + result.extend(_extract_all_transitions(state.states)) + for history_state in getattr(state, "history", []): + result.extend(_extract_transitions_from_state(history_state)) + if history_state.states: # pragma: no cover + result.extend(_extract_all_transitions(history_state.states)) + return result + + +def _collect_compound_ids(states: List[DiagramState]) -> Set[str]: + """Collect IDs of states that have children (compound/parallel).""" + result: Set[str] = set() + for state in states: + if state.children: + result.add(state.id) + result.update(_collect_compound_ids(state.children)) + return result + + +def _collect_bidirectional_compound_ids( + transitions: List[DiagramTransition], + compound_ids: Set[str], +) -> Set[str]: + """Find compound states that have both outgoing and incoming explicit edges.""" + outgoing: Set[str] = set() + incoming: Set[str] = set() + for t in transitions: + if t.is_internal: + continue + # Skip implicit initial transitions + if t.source in compound_ids and not t.event and t.targets: + continue + if t.source in compound_ids: + outgoing.add(t.source) + for target_id in t.targets: + if target_id in compound_ids: + incoming.add(target_id) + return outgoing & incoming + + +def _mark_initial_transitions( + transitions: List[DiagramTransition], + compound_ids: Set[str], +) -> None: + """Mark implicit initial transitions (compound state → child, no event).""" + for t in transitions: + if t.source in compound_ids and not t.event and t.targets and not t.is_internal: + t.is_initial = True + + +def _resolve_initial_states(states: List[DiagramState]) -> None: + """Ensure exactly one state per level has is_initial=True. + + Skips parallel areas and history states. Falls back to document order + (first non-history, non-parallel-area state) when no explicit initial exists. + Recurses into children. + + Parallel areas (children of a parallel state) have their is_initial flag + cleared: all regions are auto-activated, so no initial arrow is needed. + """ + # Clear is_initial on parallel areas — all children of a parallel state + # are simultaneously active; initial arrows would be misleading. + for s in states: + if s.is_parallel_area: + s.is_initial = False + + candidates = [ + s + for s in states + if s.type not in (StateType.HISTORY_SHALLOW, StateType.HISTORY_DEEP) + and not s.is_parallel_area + ] + + has_explicit_initial = any(s.is_initial for s in candidates) + if not has_explicit_initial and candidates: + candidates[0].is_initial = True + + for state in states: + if state.children: + _resolve_initial_states(state.children) + + +def extract(machine_or_class: "MachineRef") -> DiagramGraph: + """Extract a DiagramGraph IR from a state machine instance or class. + + Accepts either a class or an instance. The class is **never** instantiated + — all structural metadata (states, transitions, name) is available on the + class itself thanks to the metaclass. Active-state highlighting is only + produced when an *instance* is passed. + + Args: + machine_or_class: A StateMachine/StateChart instance or class. + + Returns: + A DiagramGraph representing the machine's structure. + """ + from statemachine.statemachine import StateChart + + if isinstance(machine_or_class, StateChart): + machine: "MachineRef" = machine_or_class + elif isinstance(machine_or_class, type) and issubclass(machine_or_class, StateChart): + machine = machine_or_class + else: + raise TypeError(f"Expected a StateChart instance or class, got {type(machine_or_class)}") + + getter = _actions_getter(machine) + + active_values: set = set() + if isinstance(machine, StateChart) and hasattr(machine, "configuration_values"): + active_values = set(machine.configuration_values) + + states: List[DiagramState] = [] + for state in machine.states: + states.append(_extract_state(state, machine, getter, active_values)) + + transitions = _extract_all_transitions(machine.states) + + compound_ids = _collect_compound_ids(states) + bidir_ids = _collect_bidirectional_compound_ids(transitions, compound_ids) + _mark_initial_transitions(transitions, compound_ids) + _resolve_initial_states(states) + + return DiagramGraph( + name=machine.name, + states=states, + transitions=transitions, + compound_state_ids=compound_ids, + bidirectional_compound_ids=bidir_ids, + ) diff --git a/statemachine/contrib/diagram/formatter.py b/statemachine/contrib/diagram/formatter.py new file mode 100644 index 00000000..0ce8a1b0 --- /dev/null +++ b/statemachine/contrib/diagram/formatter.py @@ -0,0 +1,137 @@ +"""Unified facade for rendering state machines in multiple text formats. + +The :class:`Formatter` class provides a decorator-based registry where each +renderer declares the format names it handles. Adding a new format only +requires writing a renderer function and decorating it — no changes to +``__format__``, ``factory.py``, or ``statemachine.py``. + +A module-level :data:`formatter` instance is the single public entry point:: + + from statemachine.contrib.diagram import formatter + + print(formatter.render(sm, "mermaid")) + + @formatter.register_format("plantuml") + def _render_plantuml(machine): + ... +""" + +from typing import TYPE_CHECKING +from typing import Callable +from typing import Dict +from typing import List + +if TYPE_CHECKING: + from typing import Union + + from statemachine.statemachine import StateChart + + MachineRef = Union["StateChart", "type[StateChart]"] + + +class Formatter: + """Unified facade for rendering state machines in multiple text formats.""" + + def __init__(self) -> None: + self._formats: Dict[str, "Callable[[MachineRef], str]"] = {} + + def register_format( + self, *names: str + ) -> "Callable[[Callable[[MachineRef], str]], Callable[[MachineRef], str]]": + """Decorator factory that registers a renderer under one or more format names. + + Usage:: + + @formatter.register_format("md", "markdown") + def _render_md(machine_or_class): + ... + """ + + def decorator( + fn: "Callable[[MachineRef], str]", + ) -> "Callable[[MachineRef], str]": + for name in names: + self._formats[name] = fn + return fn + + return decorator + + def render(self, machine_or_class: "MachineRef", fmt: str) -> str: + """Render a state machine in the given text format. + + Args: + machine_or_class: A ``StateChart`` instance or class. + fmt: Format name (e.g., ``"mermaid"``, ``"dot"``, ``"md"``). + Empty string falls back to ``repr()``. + + Raises: + ValueError: If ``fmt`` is not registered. + """ + if fmt == "": + return repr(machine_or_class) + + renderer_fn = self._formats.get(fmt) + if renderer_fn is None: + primary = sorted({self._primary_name(fn) for fn in set(self._formats.values())}) + raise ValueError( + f"Unsupported format: {fmt!r}. Use {', '.join(repr(n) for n in primary)}." + ) + return renderer_fn(machine_or_class) + + def supported_formats(self) -> List[str]: + """Return sorted list of all registered format names (including aliases).""" + return sorted(self._formats) + + def _primary_name(self, fn: "Callable[[MachineRef], str]") -> str: + """Return the first registered name for a given renderer function.""" + for name, registered_fn in self._formats.items(): + if registered_fn is fn: + return name + return "?" # pragma: no cover + + +formatter = Formatter() +"""Module-level :class:`Formatter` instance — the single public entry point.""" + + +# --------------------------------------------------------------------------- +# Built-in format registrations +# --------------------------------------------------------------------------- + + +@formatter.register_format("dot") +def _render_dot(machine_or_class: "MachineRef") -> str: + from statemachine.contrib.diagram import DotGraphMachine + + return DotGraphMachine(machine_or_class).get_graph().to_string() # type: ignore[no-any-return] + + +@formatter.register_format("svg") +def _render_svg(machine_or_class: "MachineRef") -> str: + from statemachine.contrib.diagram import DotGraphMachine + + svg_bytes: bytes = DotGraphMachine(machine_or_class).get_graph().create_svg() # type: ignore[attr-defined] + return svg_bytes.decode("utf-8") + + +@formatter.register_format("mermaid") +def _render_mermaid(machine_or_class: "MachineRef") -> str: + from statemachine.contrib.diagram import MermaidGraphMachine + + return MermaidGraphMachine(machine_or_class).get_mermaid() + + +@formatter.register_format("md", "markdown") +def _render_md(machine_or_class: "MachineRef") -> str: + from statemachine.contrib.diagram.extract import extract + from statemachine.contrib.diagram.renderers.table import TransitionTableRenderer + + return TransitionTableRenderer().render(extract(machine_or_class), fmt="md") + + +@formatter.register_format("rst") +def _render_rst(machine_or_class: "MachineRef") -> str: + from statemachine.contrib.diagram.extract import extract + from statemachine.contrib.diagram.renderers.table import TransitionTableRenderer + + return TransitionTableRenderer().render(extract(machine_or_class), fmt="rst") diff --git a/statemachine/contrib/diagram/model.py b/statemachine/contrib/diagram/model.py new file mode 100644 index 00000000..3770bba1 --- /dev/null +++ b/statemachine/contrib/diagram/model.py @@ -0,0 +1,63 @@ +from dataclasses import dataclass +from dataclasses import field +from enum import Enum +from typing import List +from typing import Set + + +class StateType(Enum): + INITIAL = "initial" + REGULAR = "regular" + FINAL = "final" + HISTORY_SHALLOW = "history_shallow" + HISTORY_DEEP = "history_deep" + CHOICE = "choice" + FORK = "fork" + JOIN = "join" + JUNCTION = "junction" + PARALLEL = "parallel" + TERMINATE = "terminate" + + +class ActionType(Enum): + ENTRY = "entry" + EXIT = "exit" + INTERNAL = "internal" + + +@dataclass +class DiagramAction: + type: ActionType + body: str + + +@dataclass +class DiagramState: + id: str + name: str + type: StateType + actions: List[DiagramAction] = field(default_factory=list) + children: List["DiagramState"] = field(default_factory=list) + is_active: bool = False + is_parallel_area: bool = False + is_initial: bool = False + + +@dataclass +class DiagramTransition: + source: str + targets: List[str] = field(default_factory=list) + event: str = "" + guards: List[str] = field(default_factory=list) + actions: List[str] = field(default_factory=list) + is_internal: bool = False + is_initial: bool = False + + +@dataclass +class DiagramGraph: + name: str + states: List[DiagramState] = field(default_factory=list) + transitions: List[DiagramTransition] = field(default_factory=list) + compound_state_ids: Set[str] = field(default_factory=set) + bidirectional_compound_ids: Set[str] = field(default_factory=set) diff --git a/statemachine/contrib/diagram/renderers/__init__.py b/statemachine/contrib/diagram/renderers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/statemachine/contrib/diagram/renderers/dot.py b/statemachine/contrib/diagram/renderers/dot.py new file mode 100644 index 00000000..a33db791 --- /dev/null +++ b/statemachine/contrib/diagram/renderers/dot.py @@ -0,0 +1,531 @@ +from dataclasses import dataclass +from dataclasses import field +from typing import Dict +from typing import List +from typing import Optional +from typing import Set + +import pydot + +from ..model import ActionType +from ..model import DiagramAction +from ..model import DiagramGraph +from ..model import DiagramState +from ..model import DiagramTransition +from ..model import StateType + + +def _escape_html(text: str) -> str: + """Escape text for use inside HTML labels.""" + return text.replace("&", "&").replace("<", "<").replace(">", ">") + + +@dataclass +class DotRendererConfig: + """Configuration for the DOT renderer, matching DotGraphMachine's class attributes.""" + + graph_rankdir: str = "LR" + font_name: str = "Helvetica" + state_font_size: str = "12" + state_active_penwidth: int = 2 + state_active_fillcolor: str = "turquoise" + transition_font_size: str = "10" + graph_attrs: Dict[str, str] = field(default_factory=dict) + node_attrs: Dict[str, str] = field(default_factory=dict) + edge_attrs: Dict[str, str] = field(default_factory=dict) + + +class DotRenderer: + """Renders a DiagramGraph into a pydot.Dot graph with UML-inspired styling. + + Uses techniques inspired by state-machine-cat for cleaner visual output: + - HTML TABLE labels for states with UML compartments + - plaintext nodes with near-transparent fill + - Refined graph/node/edge defaults + """ + + def __init__(self, config: Optional[DotRendererConfig] = None): + self.config = config or DotRendererConfig() + self._compound_ids: Set[str] = set() + self._compound_bidir_ids: Set[str] = set() + + def render(self, graph: DiagramGraph) -> pydot.Dot: + """Render a DiagramGraph to a pydot.Dot object.""" + self._compound_ids = graph.compound_state_ids + self._compound_bidir_ids = graph.bidirectional_compound_ids + dot = self._create_graph(graph.name) + self._render_states(graph.states, graph.transitions, dot) + return dot + + def _create_graph(self, name: str) -> pydot.Dot: + cfg = self.config + graph_attrs = { + "fontname": cfg.font_name, + "fontsize": cfg.state_font_size, + "penwidth": "2.0", + "splines": "true", + "ordering": "out", + "compound": "true", + "nodesep": "0.3", + "ranksep": "0.3", + "forcelabels": "true", + } + graph_attrs.update(cfg.graph_attrs) + + dot = pydot.Dot( + name, + graph_type="digraph", + label=name, + rankdir=cfg.graph_rankdir, + **graph_attrs, + ) + + # Set default node attributes + node_defaults = { + "fontname": cfg.font_name, + "fontsize": cfg.state_font_size, + "penwidth": "2.0", + } + node_defaults.update(cfg.node_attrs) + dot.set_node_defaults(**node_defaults) + + # Set default edge attributes + edge_defaults = { + "fontname": cfg.font_name, + "fontsize": cfg.transition_font_size, + "labeldistance": "1.5", + } + edge_defaults.update(cfg.edge_attrs) + dot.set_edge_defaults(**edge_defaults) + + return dot + + def _state_node_id(self, state_id: str) -> str: + """Get the node ID to use for edges. Compound states use an anchor node.""" + if state_id in self._compound_ids: + return f"{state_id}_anchor" + return state_id + + def _compound_edge_anchor(self, state_id: str, direction: str) -> str: + """Return the appropriate anchor node ID for a compound ↔ other edge. + + Compound states that have both incoming and outgoing explicit transitions + get separate ``_anchor_out`` / ``_anchor_in`` nodes so Graphviz can route + the two directions through physically distinct points, avoiding overlap. + """ + if state_id in self._compound_bidir_ids: + return f"{state_id}_anchor_{direction}" + return f"{state_id}_anchor" + + def _render_states( + self, + states: List[DiagramState], + transitions: List[DiagramTransition], + parent_graph: "pydot.Dot | pydot.Subgraph", + extra_nodes: Optional[List[pydot.Node]] = None, + ) -> None: + """Render states and transitions into the parent graph.""" + initial_state = next((s for s in states if s.is_initial), None) + + # The atomic subgraph groups all non-compound states and the inner + # initial dot (when inside a compound cluster) so Graphviz places them + # in the same rank region, keeping the initial arrow short. + atomic_subgraph = pydot.Subgraph( + graph_name=f"cluster___atomic_{id(parent_graph)}", + label="", + peripheries=0, + margin=0, + cluster="true", + ) + has_atomic = False + + if initial_state: + has_atomic = ( + self._render_initial_arrow(initial_state, parent_graph, atomic_subgraph) + or has_atomic + ) + + for state in states: + if state.type in (StateType.HISTORY_SHALLOW, StateType.HISTORY_DEEP): + atomic_subgraph.add_node(self._create_history_node(state)) + has_atomic = True + elif state.children: + subgraph = self._create_compound_subgraph(state) + anchor_nodes = self._create_compound_anchor_nodes(state) + self._render_states( + state.children, transitions, subgraph, extra_nodes=anchor_nodes + ) + parent_graph.add_subgraph(subgraph) + # Add transitions originating from this compound state + self._add_transitions_for_state(state, transitions, parent_graph) + else: + atomic_subgraph.add_node(self._create_atomic_node(state)) + has_atomic = True + + has_atomic = self._place_extra_nodes( + extra_nodes, atomic_subgraph, parent_graph, has_atomic + ) + + if has_atomic: + parent_graph.add_subgraph(atomic_subgraph) + + # Add transitions for atomic/history states + for state in states: + if not state.children: + self._add_transitions_for_state(state, transitions, parent_graph) + + @staticmethod + def _place_extra_nodes( + extra_nodes: Optional[List[pydot.Node]], + atomic_subgraph: pydot.Subgraph, + parent_graph: "pydot.Dot | pydot.Subgraph", + has_atomic: bool, + ) -> bool: + """Place anchor nodes from the parent compound into the graph. + + Co-locates them with real states when possible. If there are no atomic + states at this level (e.g. a parallel state with only compound children), + adds them directly to the parent graph to avoid an empty cluster. + + Returns the updated ``has_atomic`` flag. + """ + if not extra_nodes: + return has_atomic + target = atomic_subgraph if has_atomic else parent_graph + for node in extra_nodes: + target.add_node(node) + return has_atomic or (target is atomic_subgraph) + + def _render_initial_arrow( + self, + initial_state: DiagramState, + parent_graph: "pydot.Dot | pydot.Subgraph", + atomic_subgraph: pydot.Subgraph, + ) -> bool: + """Render the black-dot initial arrow pointing to ``initial_state``. + + Returns True if nodes were added to ``atomic_subgraph``. + """ + initial_node_id = f"__initial_{id(parent_graph)}" + initial_node = self._create_initial_node(initial_node_id) + added_to_atomic = False + + extra = {} + if initial_state.children: + extra["lhead"] = f"cluster_{initial_state.id}" + + if initial_state.children or isinstance(parent_graph, pydot.Dot): + # Compound initial state, or top-level atomic initial state: + # keep the dot in a plain wrapper subgraph attached to parent. + wrapper = pydot.Subgraph( + graph_name=f"{initial_node_id}_sg", + label="", + peripheries=0, + margin=0, + ) + wrapper.add_node(initial_node) + parent_graph.add_subgraph(wrapper) + else: + # Inner (compound parent) with atomic initial state: add the + # dot directly into the atomic cluster so it shares the same + # rank region as the target state, avoiding a long arrow caused + # by the compound cluster's anchor nodes pushing step1 further. + atomic_subgraph.add_node(initial_node) + added_to_atomic = True + + parent_graph.add_edge( + pydot.Edge( + initial_node_id, + self._state_node_id(initial_state.id), + label="", + minlen=1, + weight=100, + **extra, + ) + ) + return added_to_atomic + + def _create_initial_node(self, node_id: str) -> pydot.Node: + return pydot.Node( + node_id, + label="", + shape="circle", + style="filled", + fillcolor="black", + color="black", + fixedsize="true", + width=0.15, + height=0.15, + penwidth="0", + ) + + def _create_atomic_node(self, state: DiagramState) -> pydot.Node: + """Create a node for an atomic state. + + All states use a native ``shape="rectangle"`` with ``style="rounded, filled"`` + so that Graphviz clips edges at the actual rounded border. States with + entry/exit actions embed an HTML TABLE (``border="0"``) inside the native + shape to render UML-style compartments (name + separator + actions). + """ + actions = [a for a in state.actions if a.type != ActionType.INTERNAL or a.body] + fillcolor = self.config.state_active_fillcolor if state.is_active else "white" + penwidth = self.config.state_active_penwidth if state.is_active else 2 + + if not actions: + # Simple state: native rounded rectangle + node = pydot.Node( + state.id, + label=state.name, + shape="rectangle", + style="rounded, filled", + fontname=self.config.font_name, + fontsize=self.config.state_font_size, + fillcolor=fillcolor, + penwidth=penwidth, + peripheries=2 if state.type == StateType.FINAL else 1, + ) + else: + # State with actions: native shape + HTML TABLE label (border=0). + # The native shape handles edge clipping; the TABLE provides + # UML compartment layout with
separator. + label = self._build_html_table_label(state, actions) + node = pydot.Node( + state.id, + label=f"<{label}>", + shape="rectangle", + style="rounded, filled", + fontname=self.config.font_name, + fontsize=self.config.state_font_size, + fillcolor=fillcolor, + penwidth=penwidth, + margin="0", + peripheries=2 if state.type == StateType.FINAL else 1, + ) + + return node + + def _build_html_table_label( + self, + state: DiagramState, + actions: List[DiagramAction], + ) -> str: + """Build an HTML TABLE label with UML compartments (name | actions). + + The TABLE has ``border="0"`` because the visible border is drawn by + the native Graphviz shape, ensuring edges are clipped correctly. + """ + name = _escape_html(state.name) + font_size = self.config.state_font_size + action_font_size = self.config.transition_font_size + + action_lines = "
".join( + f'{_escape_html(self._format_action(a))}' + for a in actions + ) + + return ( + f'' + f'" + f"
" + f'" + f"
' + f'{name}' + f"
' + f"{action_lines}" + f"
" + ) + + @staticmethod + def _format_action(action: DiagramAction) -> str: + if action.type == ActionType.INTERNAL: + return action.body + return f"{action.type.value} / {action.body}" + + def _create_history_node(self, state: DiagramState) -> pydot.Node: + label = "H*" if state.type == StateType.HISTORY_DEEP else "H" + return pydot.Node( + state.id, + label=label, + shape="circle", + style="filled", + fillcolor="white", + fontname=self.config.font_name, + fontsize="8pt", + fixedsize="true", + width=0.3, + height=0.3, + ) + + def _create_compound_anchor_nodes(self, state: DiagramState) -> List[pydot.Node]: + """Create invisible anchor nodes for edge routing inside a compound cluster. + + These nodes are injected into the children's atomic_subgraph so they + share the same layout row as the real states, avoiding blank space at + the top of the compound cluster. + """ + # For bidirectional compounds, all edges route through _anchor_in/_anchor_out; + # the generic _anchor node is never used and would become an orphan that + # Graphviz places arbitrarily, creating blank vertical space in the cluster. + if state.id not in self._compound_bidir_ids: + nodes = [ + pydot.Node( + f"{state.id}_anchor", + shape="point", + style="invis", + width=0, + height=0, + fixedsize="true", + ) + ] + else: + nodes = [] + for direction in ("in", "out"): + nodes.append( + pydot.Node( + f"{state.id}_anchor_{direction}", + shape="point", + style="invis", + width=0, + height=0, + fixedsize="true", + ) + ) + return nodes + + def _create_compound_subgraph(self, state: DiagramState) -> pydot.Subgraph: + """Create a cluster subgraph for a compound/parallel state.""" + style = "rounded, solid" + if state.is_parallel_area: + style = "rounded, dashed" + + label = self._build_compound_label(state) + + return pydot.Subgraph( + graph_name=f"cluster_{state.id}", + label=f"<{label}>", + style=style, + cluster="true", + penwidth="2.0", + fontname=self.config.font_name, + fontsize=self.config.state_font_size, + margin="4", + ) + + def _build_compound_label(self, state: DiagramState) -> str: + """Build HTML label for a compound/parallel subgraph.""" + name = _escape_html(state.name) + if state.type == StateType.PARALLEL: + return f"{name} ☷" + + actions = [a for a in state.actions if a.type != ActionType.INTERNAL or a.body] + if not actions: + return f"{name}" + + rows = [f"{name}"] + for action in actions: + action_text = _escape_html(self._format_action(action)) + rows.append( + f'{action_text}' + ) + return "
".join(rows) + + def _add_transitions_for_state( + self, + state: DiagramState, + all_transitions: List[DiagramTransition], + graph: "pydot.Dot | pydot.Subgraph", + ) -> None: + """Add edges for all non-internal transitions originating from this state.""" + for transition in all_transitions: + if transition.source != state.id or transition.is_internal: + continue + # Skip implicit initial transitions — represented by the black-dot initial node. + if transition.is_initial: + continue + for edge in self._create_edges(transition): + graph.add_edge(edge) + + def _create_edges(self, transition: DiagramTransition) -> List[pydot.Edge]: + """Create pydot.Edge objects for a transition.""" + target_ids: List[Optional[str]] = ( + list(transition.targets) if transition.targets else [None] + ) + + cond = ", ".join(transition.guards) + cond_html = f"
[{_escape_html(cond)}]" if cond else "" + + return [ + self._create_single_edge(transition, target_id, i, cond_html) + for i, target_id in enumerate(target_ids) + ] + + def _create_single_edge( + self, + transition: DiagramTransition, + target_id: Optional[str], + index: int, + cond_html: str, + ) -> pydot.Edge: + """Create a single pydot.Edge for one target of a transition.""" + src, dst, extra = self._resolve_edge_endpoints(transition, target_id) + has_substates = bool(extra) + html_label = self._build_edge_label(transition.event, cond_html, index) + + return pydot.Edge( + src, + dst, + label=html_label, + minlen=2 if has_substates else 1, + **extra, + ) + + def _resolve_edge_endpoints( + self, + transition: DiagramTransition, + target_id: Optional[str], + ) -> "tuple[str, str, Dict[str, str]]": + """Resolve source/destination node IDs and cluster attributes for an edge.""" + extra: Dict[str, str] = {} + source_is_compound = transition.source in self._compound_ids + target_is_compound = target_id is not None and target_id in self._compound_ids + + if source_is_compound: + extra["ltail"] = f"cluster_{transition.source}" + if target_is_compound: + extra["lhead"] = f"cluster_{target_id}" + + dst = ( + self._state_node_id(target_id) + if target_id is not None + else self._state_node_id(transition.source) + ) + src = self._state_node_id(transition.source) + + # For compound states in bidirectional pairs, route outgoing edges + # through _anchor_out and incoming through _anchor_in so Graphviz + # places them at different physical positions inside the cluster. + if source_is_compound and transition.source in self._compound_bidir_ids: + src = self._compound_edge_anchor(transition.source, "out") + extra["ltail"] = f"cluster_{transition.source}" + if target_is_compound and target_id in self._compound_bidir_ids: + dst = self._compound_edge_anchor(target_id, "in") + extra["lhead"] = f"cluster_{target_id}" + + return src, dst, extra + + def _build_edge_label(self, event: str, cond_html: str, index: int) -> str: + """Build the HTML label for a transition edge.""" + event_text = _escape_html(event) if index == 0 else "" + if not event_text and not (cond_html and index == 0): + return "" + + label_content = f"{event_text}{cond_html}" if index == 0 else "" + font_size = self.config.transition_font_size + return ( + f'<' + f'" + f'' + f"
' + f'{label_content}' + f"
>" + ) diff --git a/statemachine/contrib/diagram/renderers/mermaid.py b/statemachine/contrib/diagram/renderers/mermaid.py new file mode 100644 index 00000000..15ba61d5 --- /dev/null +++ b/statemachine/contrib/diagram/renderers/mermaid.py @@ -0,0 +1,348 @@ +from dataclasses import dataclass +from typing import Dict +from typing import List +from typing import Optional +from typing import Set + +from ..model import ActionType +from ..model import DiagramAction +from ..model import DiagramGraph +from ..model import DiagramState +from ..model import DiagramTransition +from ..model import StateType + + +@dataclass +class MermaidRendererConfig: + """Configuration for the Mermaid renderer.""" + + direction: str = "LR" + active_fill: str = "#40E0D0" + active_stroke: str = "#333" + + +class MermaidRenderer: + """Renders a DiagramGraph into a Mermaid stateDiagram-v2 source string. + + Mermaid's stateDiagram-v2 has a rendering bug + (`mermaid-js/mermaid#4052 `_) + where transitions whose source or target is a compound state + (``state X { ... }``) **inside a parallel region** crash with + ``Cannot set properties of undefined (setting 'rank')``. To work around + this, the renderer rewrites compound-state endpoints that are descendants + of a parallel state, redirecting them to the compound's initial child. + Compound states outside parallel regions are left unchanged. + """ + + def __init__(self, config: Optional[MermaidRendererConfig] = None): + self.config = config or MermaidRendererConfig() + self._active_ids: List[str] = [] + self._rendered_transitions: Set[tuple] = set() + self._compound_ids: Set[str] = set() + self._initial_child_map: Dict[str, str] = {} + self._parallel_descendant_ids: Set[str] = set() + self._all_descendants_map: Dict[str, Set[str]] = {} + + def render(self, graph: DiagramGraph) -> str: + """Render a DiagramGraph to a Mermaid stateDiagram-v2 string.""" + self._active_ids = [] + self._rendered_transitions = set() + self._compound_ids = graph.compound_state_ids + self._initial_child_map = self._build_initial_child_map(graph.states) + self._parallel_descendant_ids = self._collect_parallel_descendants(graph.states) + self._all_descendants_map = self._build_all_descendants_map(graph.states) + + lines: List[str] = [] + lines.append("stateDiagram-v2") + lines.append(f" direction {self.config.direction}") + + top_ids = {s.id for s in graph.states} + self._render_states(graph.states, graph.transitions, lines, indent=1) + self._render_initial_and_final(graph.states, lines, indent=1) + self._render_scope_transitions(graph.transitions, top_ids, lines, indent=1) + + if self._active_ids: + cfg = self.config + lines.append("") + lines.append(f" classDef active fill:{cfg.active_fill},stroke:{cfg.active_stroke}") + for sid in self._active_ids: + lines.append(f" {sid}:::active") + + return "\n".join(lines) + "\n" + + def _build_initial_child_map(self, states: List[DiagramState]) -> Dict[str, str]: + """Build a map from compound state ID to its initial child ID (recursive).""" + result: Dict[str, str] = {} + for state in states: + if state.children: + initial = next((c for c in state.children if c.is_initial), None) + if initial: + result[state.id] = initial.id + result.update(self._build_initial_child_map(state.children)) + return result + + @staticmethod + def _collect_parallel_descendants( + states: List[DiagramState], + inside_parallel: bool = False, + ) -> Set[str]: + """Collect IDs of all states that are descendants of a parallel state.""" + result: Set[str] = set() + for state in states: + if inside_parallel: + result.add(state.id) + child_inside = inside_parallel or state.type == StateType.PARALLEL + result.update( + MermaidRenderer._collect_parallel_descendants(state.children, child_inside) + ) + return result + + def _build_all_descendants_map(self, states: List[DiagramState]) -> Dict[str, Set[str]]: + """Map each compound state ID to the set of all its descendant IDs.""" + result: Dict[str, Set[str]] = {} + for state in states: + if state.children: + result[state.id] = self._collect_recursive_descendants(state.children) + result.update(self._build_all_descendants_map(state.children)) + return result + + @staticmethod + def _collect_recursive_descendants(states: List[DiagramState]) -> Set[str]: + """Collect all state IDs in a subtree recursively.""" + ids: Set[str] = set() + for s in states: + ids.add(s.id) + ids.update(MermaidRenderer._collect_recursive_descendants(s.children)) + return ids + + def _resolve_endpoint(self, state_id: str) -> str: + """Resolve a transition endpoint for Mermaid compatibility. + + Only redirects compound states that are inside a parallel region — + this is where Mermaid's rendering bug (mermaid-js/mermaid#4052) occurs. + Compound states outside parallel regions are left unchanged. + """ + if ( + state_id in self._compound_ids + and state_id in self._parallel_descendant_ids + and state_id in self._initial_child_map + ): + return self._initial_child_map[state_id] + return state_id + + def _render_states( + self, + states: List[DiagramState], + transitions: List[DiagramTransition], + lines: List[str], + indent: int, + ) -> None: + for state in states: + if state.type in (StateType.HISTORY_SHALLOW, StateType.HISTORY_DEEP): + label = "H*" if state.type == StateType.HISTORY_DEEP else "H" + pad = " " * indent + lines.append(f'{pad}state "{label}" as {state.id}') + continue + + if state.type == StateType.CHOICE: + pad = " " * indent + lines.append(f"{pad}state {state.id} <>") + continue + + if state.type == StateType.FORK: + pad = " " * indent + lines.append(f"{pad}state {state.id} <>") + continue + + if state.type == StateType.JOIN: + pad = " " * indent + lines.append(f"{pad}state {state.id} <>") + continue + + if state.children: + self._render_compound_state(state, transitions, lines, indent) + else: + self._render_atomic_state(state, lines, indent) + + def _render_atomic_state( + self, + state: DiagramState, + lines: List[str], + indent: int, + ) -> None: + pad = " " * indent + + if state.name != state.id: + lines.append(f'{pad}state "{state.name}" as {state.id}') + + actions = [a for a in state.actions if a.type != ActionType.INTERNAL or a.body] + if actions: + for action in actions: + lines.append(f"{pad}{state.id} : {self._format_action(action)}") + + if state.is_active: + self._active_ids.append(state.id) + + def _render_compound_state( + self, + state: DiagramState, + transitions: List[DiagramTransition], + lines: List[str], + indent: int, + ) -> None: + pad = " " * indent + + if state.type == StateType.PARALLEL: + lines.append(f'{pad}state "{state.name}" as {state.id} {{') + regions = [c for c in state.children if c.is_parallel_area or c.children] + for i, region in enumerate(regions): + if i > 0: + lines.append(f"{pad} --") + self._render_compound_state(region, transitions, lines, indent + 1) + lines.append(f"{pad}}}") + else: + label = state.name if state.name != state.id else "" + if label: + lines.append(f'{pad}state "{label}" as {state.id} {{') + else: + lines.append(f"{pad}state {state.id} {{") + + initial_child = next((c for c in state.children if c.is_initial), None) + if initial_child: + lines.append(f"{pad} [*] --> {initial_child.id}") + + self._render_states(state.children, transitions, lines, indent + 1) + + # Render transitions scoped to this compound + child_ids = self._collect_all_descendant_ids(state.children) + self._render_scope_transitions(transitions, child_ids, lines, indent + 1) + + # Final state transitions + for child in state.children: + if child.type == StateType.FINAL: + lines.append(f"{pad} {child.id} --> [*]") + + lines.append(f"{pad}}}") + + if state.is_active: + self._active_ids.append(state.id) + + def _collect_all_descendant_ids(self, states: List[DiagramState]) -> Set[str]: + """Collect all state IDs in a subtree (direct children only for scope).""" + ids: Set[str] = set() + for s in states: + ids.add(s.id) + return ids + + def _render_scope_transitions( + self, + transitions: List[DiagramTransition], + scope_ids: Set[str], + lines: List[str], + indent: int, + ) -> None: + """Render transitions that belong to this scope level. + + A transition belongs to scope S if all its endpoints are *reachable* + from S (either directly in S or descendants of a compound in S) **and** + the transition is not fully internal to a single compound in S (those + are rendered by the compound's inner scope). + + This allows cross-boundary transitions (e.g., an outer state targeting + a history pseudo-state inside a compound) to be rendered at the correct + scope level — Mermaid draws the arrow crossing the compound border. + + Mermaid crashes when the source or target is a compound state inside a + parallel region (mermaid-js/mermaid#4052). For those cases, endpoints + are redirected to the compound's initial child via ``_resolve_endpoint``. + """ + # Build the descendant sets for compounds in this scope + compound_descendants: Dict[str, Set[str]] = {} + expanded: Set[str] = set(scope_ids) + for sid in scope_ids: + if sid in self._all_descendants_map: + compound_descendants[sid] = self._all_descendants_map[sid] + expanded |= self._all_descendants_map[sid] + + for t in transitions: + if t.is_initial or t.is_internal: + continue + + targets = t.targets if t.targets else [t.source] + + # All endpoints must be reachable from this scope + if t.source not in expanded: + continue + if not all(target in expanded for target in targets): + continue + + # Skip transitions fully internal to a single compound — + # those will be rendered by the compound's inner scope. + if self._is_fully_internal(t.source, targets, compound_descendants): + continue + + # Resolve endpoints for rendering (redirect compound → initial child) + source = self._resolve_endpoint(t.source) + resolved_targets = [self._resolve_endpoint(tid) for tid in targets] + + for target in resolved_targets: + key = (source, target, t.event) + if key in self._rendered_transitions: + continue + self._rendered_transitions.add(key) + self._render_single_transition(t, source, target, lines, indent) + + @staticmethod + def _is_fully_internal( + source: str, + targets: List[str], + compound_descendants: Dict[str, Set[str]], + ) -> bool: + """Check if all endpoints belong to the same compound's descendants.""" + for descendants in compound_descendants.values(): + if source in descendants and all(tgt in descendants for tgt in targets): + return True + return False + + def _render_single_transition( + self, + transition: DiagramTransition, + source: str, + target: str, + lines: List[str], + indent: int, + ) -> None: + pad = " " * indent + label_parts: List[str] = [] + if transition.event: + label_parts.append(transition.event) + if transition.guards: + label_parts.append(f"[{', '.join(transition.guards)}]") + + label = " ".join(label_parts) + if label: + lines.append(f"{pad}{source} --> {target} : {label}") + else: + lines.append(f"{pad}{source} --> {target}") + + @staticmethod + def _format_action(action: DiagramAction) -> str: + if action.type == ActionType.INTERNAL: + return action.body + return f"{action.type.value} / {action.body}" + + def _render_initial_and_final( + self, + states: List[DiagramState], + lines: List[str], + indent: int, + ) -> None: + """Render top-level [*] --> initial and final --> [*] arrows.""" + pad = " " * indent + initial = next((s for s in states if s.is_initial), None) + if initial: + lines.append(f"{pad}[*] --> {initial.id}") + + for state in states: + if state.type == StateType.FINAL: + lines.append(f"{pad}{state.id} --> [*]") diff --git a/statemachine/contrib/diagram/renderers/table.py b/statemachine/contrib/diagram/renderers/table.py new file mode 100644 index 00000000..eeaa18ec --- /dev/null +++ b/statemachine/contrib/diagram/renderers/table.py @@ -0,0 +1,105 @@ +from typing import List + +from ..model import DiagramGraph +from ..model import DiagramState +from ..model import DiagramTransition + + +class TransitionTableRenderer: + """Renders a DiagramGraph as a transition table in markdown or RST format.""" + + def render(self, graph: DiagramGraph, fmt: str = "md") -> str: + """Render the transition table. + + Args: + graph: The diagram IR to render. + fmt: Output format — ``"md"`` for markdown, ``"rst"`` for reStructuredText. + + Returns: + The formatted transition table as a string. + """ + rows = self._collect_rows(graph.states, graph.transitions) + + if fmt == "rst": + return self._render_rst(rows) + return self._render_md(rows) + + def _collect_rows( + self, + states: List[DiagramState], + transitions: List[DiagramTransition], + ) -> "List[tuple[str, str, str, str]]": + """Collect (State, Event, Guard, Target) tuples from the IR.""" + rows: List[tuple[str, str, str, str]] = [] + state_names = self._build_state_name_map(states) + + for t in transitions: + if t.is_initial or t.is_internal: + continue + + source_name = state_names.get(t.source, t.source) + guard = ", ".join(t.guards) if t.guards else "" + event = t.event or "" + + if t.targets: + for target_id in t.targets: + target_name = state_names.get(target_id, target_id) + rows.append((source_name, event, guard, target_name)) + else: + rows.append((source_name, event, guard, source_name)) + + return rows + + def _build_state_name_map(self, states: List[DiagramState]) -> dict: + """Build a mapping from state ID to display name, recursively.""" + result: dict = {} + for state in states: + result[state.id] = state.name + if state.children: + result.update(self._build_state_name_map(state.children)) + return result + + def _render_md(self, rows: "List[tuple[str, str, str, str]]") -> str: + """Render as a markdown table.""" + headers = ("State", "Event", "Guard", "Target") + col_widths = [len(h) for h in headers] + + for row in rows: + for i, cell in enumerate(row): + col_widths[i] = max(col_widths[i], len(cell)) + + def _fmt_row(cells: "tuple[str, ...]") -> str: + parts = [cell.ljust(col_widths[i]) for i, cell in enumerate(cells)] + return "| " + " | ".join(parts) + " |" + + lines = [_fmt_row(headers)] + lines.append("| " + " | ".join("-" * w for w in col_widths) + " |") + for row in rows: + lines.append(_fmt_row(row)) + + return "\n".join(lines) + "\n" + + def _render_rst(self, rows: "List[tuple[str, str, str, str]]") -> str: + """Render as an RST grid table.""" + headers = ("State", "Event", "Guard", "Target") + col_widths = [len(h) for h in headers] + + for row in rows: + for i, cell in enumerate(row): + col_widths[i] = max(col_widths[i], len(cell)) + + def _border(char: str = "-") -> str: + return "+" + "+".join(char * (w + 2) for w in col_widths) + "+" + + def _data_row(cells: "tuple[str, ...]") -> str: + parts = [f" {cell.ljust(col_widths[i])} " for i, cell in enumerate(cells)] + return "|" + "|".join(parts) + "|" + + lines = [_border("-")] + lines.append(_data_row(headers)) + lines.append(_border("=")) + for row in rows: + lines.append(_data_row(row)) + lines.append(_border("-")) + + return "\n".join(lines) + "\n" diff --git a/statemachine/contrib/diagram/sphinx_ext.py b/statemachine/contrib/diagram/sphinx_ext.py new file mode 100644 index 00000000..84ab50f3 --- /dev/null +++ b/statemachine/contrib/diagram/sphinx_ext.py @@ -0,0 +1,280 @@ +"""Sphinx extension providing the ``statemachine-diagram`` directive. + +Usage in MyST Markdown:: + + ```{statemachine-diagram} mypackage.module.MyMachine + :events: start, ship + :caption: After shipping + ``` + +The directive imports the state machine class, optionally instantiates it and +sends events, then renders an SVG diagram inline in the documentation. +""" + +from __future__ import annotations + +import hashlib +import html as html_mod +import os +import re +from typing import TYPE_CHECKING +from typing import Any +from typing import ClassVar + +from docutils import nodes +from docutils.parsers.rst import directives +from sphinx.util.docutils import SphinxDirective + +if TYPE_CHECKING: + from sphinx.application import Sphinx + + +def _align_spec(argument: str) -> str: + return str(directives.choice(argument, ("left", "center", "right"))) + + +def _parse_events(value: str) -> list[str]: + """Parse a comma-separated list of event names.""" + return [e.strip() for e in value.split(",") if e.strip()] + + +# Match the outer ... element, stripping XML prologue/DOCTYPE. +_SVG_TAG_RE = re.compile(r"()", re.DOTALL) + +# Match fixed width/height attributes (e.g. width="702pt" height="170pt"). +_SVG_WIDTH_RE = re.compile(r'\bwidth="([^"]*(?:pt|px))"') +_SVG_HEIGHT_RE = re.compile(r'\bheight="([^"]*(?:pt|px))"') + + +class StateMachineDiagram(SphinxDirective): + """Render a state machine diagram from an importable class path. + + Supports the same layout options as the standard ``image`` and ``figure`` + directives (``width``, ``height``, ``scale``, ``align``, ``target``, + ``class``, ``name``), plus state-machine-specific options (``events``, + ``caption``, ``figclass``). + """ + + has_content: ClassVar[bool] = False + required_arguments: ClassVar[int] = 1 + optional_arguments: ClassVar[int] = 0 + option_spec: ClassVar[dict[str, Any]] = { + # State-machine options + "events": directives.unchanged, + "format": directives.unchanged, + # Standard image/figure options + "caption": directives.unchanged, + "alt": directives.unchanged, + "width": directives.unchanged, + "height": directives.unchanged, + "scale": directives.unchanged, + "align": _align_spec, + "target": directives.unchanged, + "class": directives.class_option, + "name": directives.unchanged, + "figclass": directives.class_option, + } + + def run(self) -> list[nodes.Node]: + qualname = self.arguments[0] + + try: + from statemachine.contrib.diagram import formatter + from statemachine.contrib.diagram import import_sm + + sm_class = import_sm(qualname) + except (ImportError, ValueError) as exc: + return [ + self.state_machine.reporter.warning( + f"statemachine-diagram: could not import {qualname!r}: {exc}", + line=self.lineno, + ) + ] + + if "events" in self.options: + machine = sm_class() + for event_name in _parse_events(self.options["events"]): + machine.send(event_name) + else: + machine = sm_class + + output_format = self.options.get("format", "").strip().lower() + + if output_format == "mermaid": + return self._run_mermaid(machine, formatter, qualname) + + try: + svg_text = formatter.render(machine, "svg") + except Exception as exc: + return [ + self.state_machine.reporter.warning( + f"statemachine-diagram: failed to generate diagram for {qualname!r}: {exc}", + line=self.lineno, + ) + ] + + svg_tag, intrinsic_width, intrinsic_height = self._prepare_svg(svg_text) + svg_styles = self._build_svg_styles(intrinsic_width, intrinsic_height) + svg_tag = svg_tag.replace("{svg_tag}' + if target: + img_html = f'{img_html}' + + wrapper_classes = self._build_wrapper_classes() + class_attr = f' class="{" ".join(wrapper_classes)}"' + + if "caption" in self.options: + caption = html_mod.escape(self.options["caption"]) + figclass = self.options.get("figclass", []) + if figclass: + class_attr = f' class="{" ".join(wrapper_classes + figclass)}"' + html = ( + f"\n" + f" {img_html}\n" + f"
{caption}
\n" + f"" + ) + else: + html = f"{img_html}" + + raw_node = nodes.raw("", html, format="html") + + if "name" in self.options: + self.add_name(raw_node) + + return [raw_node] + + def _run_mermaid(self, machine: object, formatter: Any, qualname: str) -> list[nodes.Node]: + """Render a Mermaid diagram using sphinxcontrib-mermaid's node type.""" + try: + mermaid_src = formatter.render(machine, "mermaid") + except Exception as exc: + return [ + self.state_machine.reporter.warning( + f"statemachine-diagram: failed to generate mermaid for {qualname!r}: {exc}", + line=self.lineno, + ) + ] + + try: + from sphinxcontrib.mermaid import ( # type: ignore[import-untyped] + mermaid as MermaidNode, + ) + except ImportError: + # Fallback: emit a raw code block if sphinxcontrib-mermaid is not installed + code_node = nodes.literal_block(mermaid_src, mermaid_src) + code_node["language"] = "mermaid" + return [code_node] + + node = MermaidNode() + node["code"] = mermaid_src + node["options"] = {} + + caption = self.options.get("caption") + if caption: + figure_node = nodes.figure() + figure_node += node + figure_node += nodes.caption(caption, caption) + if "name" in self.options: + self.add_name(figure_node) + return [figure_node] + + if "name" in self.options: + self.add_name(node) + return [node] + + def _prepare_svg(self, svg_text: str) -> tuple[str, str, str]: + """Extract the ```` element and its intrinsic dimensions.""" + match = _SVG_TAG_RE.search(svg_text) + svg_tag = match.group(1) if match else svg_text + + width_match = _SVG_WIDTH_RE.search(svg_tag) + height_match = _SVG_HEIGHT_RE.search(svg_tag) + intrinsic_width = width_match.group(1) if width_match else "" + intrinsic_height = height_match.group(1) if height_match else "" + + # Remove fixed dimensions — sizing is controlled via inline styles. + svg_tag = _SVG_WIDTH_RE.sub("", svg_tag) + svg_tag = _SVG_HEIGHT_RE.sub("", svg_tag) + + return svg_tag, intrinsic_width, intrinsic_height + + def _build_svg_styles(self, intrinsic_width: str, intrinsic_height: str) -> str: + """Build an inline ``style`` attribute for the ```` element.""" + parts: list[str] = [] + + # Width: explicit > scaled intrinsic > intrinsic as max-width. + user_width = self.options.get("width", "") + scale = self.options.get("scale", "") + if user_width: + parts.append(f"width: {user_width}") + elif scale and intrinsic_width: + factor = int(scale.rstrip("%")) / 100 + value, unit = _split_length(intrinsic_width) + parts.append(f"width: {value * factor:.1f}{unit}") + elif intrinsic_width: + parts.append(f"max-width: {intrinsic_width}") + + # Height: explicit > scaled intrinsic > auto. + user_height = self.options.get("height", "") + if user_height: + parts.append(f"height: {user_height}") + elif scale and intrinsic_height: + factor = int(scale.rstrip("%")) / 100 + value, unit = _split_length(intrinsic_height) + parts.append(f"height: {value * factor:.1f}{unit}") + else: + parts.append("height: auto") + + return f'style="{"; ".join(parts)}"' + + def _resolve_target(self, svg_text: str) -> str: + """Return the href for the wrapper ```` tag, if any. + + When ``:target:`` is given without a value (or as empty string), the + raw SVG is written to ``_images/`` and linked so the user can open + the full diagram in a new browser tab for zooming. + """ + if "target" not in self.options: + return "" + target = (self.options["target"] or "").strip() + if target: + return target + + # Auto-generate a standalone SVG file for zoom. + qualname = self.arguments[0] + events_key = self.options.get("events", "") + identity = f"{qualname}:{events_key}" + digest = hashlib.sha1(identity.encode()).hexdigest()[:8] + filename = f"statemachine-{digest}.svg" + + outdir = os.path.join(self.env.app.outdir, "_images") + os.makedirs(outdir, exist_ok=True) + outpath = os.path.join(outdir, filename) + with open(outpath, "w", encoding="utf-8") as f: + f.write(svg_text) + + return f"/_images/{filename}" + + def _build_wrapper_classes(self) -> list[str]: + """Build CSS class list for the outer wrapper element.""" + css_classes: list[str] = self.options.get("class", []) + align = self.options.get("align", "center") + return ["statemachine-diagram", f"align-{align}"] + css_classes + + +def _split_length(value: str) -> tuple[float, str]: + """Split a CSS length like ``'702pt'`` into ``(702.0, 'pt')``.""" + match = re.match(r"([0-9.]+)(.*)", value) + if match: + return float(match.group(1)), match.group(2) + return 0.0, value + + +def setup(app: "Sphinx") -> dict[str, Any]: + app.add_directive("statemachine-diagram", StateMachineDiagram) + return {"version": "0.1", "parallel_read_safe": True, "parallel_write_safe": True} diff --git a/statemachine/engines/async_.py b/statemachine/engines/async_.py index 67239fd8..9e055610 100644 --- a/statemachine/engines/async_.py +++ b/statemachine/engines/async_.py @@ -1,6 +1,5 @@ import asyncio import contextvars -import logging from itertools import chain from time import time from typing import TYPE_CHECKING @@ -17,12 +16,8 @@ from .base import BaseEngine if TYPE_CHECKING: - from ..event import Event from ..transition import Transition -logger = logging.getLogger(__name__) - - # ContextVar to distinguish reentrant calls (from within callbacks) from # concurrent external calls. asyncio propagates context to child tasks # (e.g., those created by asyncio.gather in the callback system), so a @@ -109,6 +104,23 @@ async def _conditions_match(self, transition: "Transition", trigger_data: Trigge transition.cond.key, *args, on_error=on_error, **kwargs ) + async def _first_transition_that_matches( # type: ignore[override] + self, + state: State, + trigger_data: TriggerData, + predicate: Callable, + ) -> "Transition | None": + for s in chain([state], state.ancestors()): + transition: "Transition" + for transition in s.transitions: + if ( + not transition.initial + and predicate(transition, trigger_data.event) + and await self._conditions_match(transition, trigger_data) + ): + return transition + return None + async def _select_transitions( # type: ignore[override] self, trigger_data: TriggerData, predicate: Callable ) -> "OrderedSet[Transition]": @@ -116,22 +128,8 @@ async def _select_transitions( # type: ignore[override] atomic_states = (state for state in self.sm.configuration if state.is_atomic) - async def first_transition_that_matches( - state: State, event: "Event | None" - ) -> "Transition | None": - for s in chain([state], state.ancestors()): - transition: "Transition" - for transition in s.transitions: - if ( - not transition.initial - and predicate(transition, event) - and await self._conditions_match(transition, trigger_data) - ): - return transition - return None - for state in atomic_states: - transition = await first_transition_that_matches(state, trigger_data.event) + transition = await self._first_transition_that_matches(state, trigger_data, predicate) if transition is not None: enabled_transitions.add(transition) @@ -179,7 +177,7 @@ async def _exit_states( # type: ignore[override] args, kwargs = await self._get_args_kwargs(info.transition, trigger_data) if info.state is not None: # pragma: no branch - logger.debug("%s Exiting state: %s", self._log_id, info.state) + self._debug("%s Exiting state: %s", self._log_id, info.state) await self.sm._callbacks.async_call( info.state.exit.key, *args, on_error=on_error, **kwargs ) @@ -234,7 +232,7 @@ async def _enter_states( # noqa: C901 target=target, ) - logger.debug("%s Entering state: %s", self._log_id, target) + self._debug("%s Entering state: %s", self._log_id, target) self._add_state_to_configuration(target) on_entry_result = await self.sm._callbacks.async_call( @@ -274,7 +272,7 @@ async def _enter_states( # noqa: C901 async def microstep(self, transitions: "List[Transition]", trigger_data: TriggerData): self._microstep_count += 1 - logger.debug( + self._debug( "%s macro:%d micro:%d transitions: %s", self._log_id, self._macrostep_count, @@ -366,7 +364,7 @@ async def processing_loop( # noqa: C901 return None _ctx_token = _in_processing_loop.set(True) - logger.debug("%s Processing loop started: %s", self._log_id, self.sm.current_state_value) + self._debug("%s Processing loop started: %s", self._log_id, self.sm.current_state_value) first_result = self._sentinel try: took_events = True @@ -378,7 +376,7 @@ async def processing_loop( # noqa: C901 # Phase 1: eventless transitions and internal events while not macrostep_done: self._microstep_count = 0 - logger.debug( + self._debug( "%s Macrostep %d: eventless/internal queue", self._log_id, self._macrostep_count, @@ -394,7 +392,7 @@ async def processing_loop( # noqa: C901 internal_event = self.internal_queue.pop() enabled_transitions = await self.select_transitions(internal_event) if enabled_transitions: - logger.debug( + self._debug( "%s Enabled transitions: %s", self._log_id, enabled_transitions ) took_events = True @@ -412,9 +410,7 @@ async def processing_loop( # noqa: C901 await self._run_microstep(enabled_transitions, internal_event) # Phase 3: external events - logger.debug( - "%s Macrostep %d: external queue", self._log_id, self._macrostep_count - ) + self._debug("%s Macrostep %d: external queue", self._log_id, self._macrostep_count) while not self.external_queue.is_empty(): self.clear_cache() took_events = True @@ -429,7 +425,7 @@ async def processing_loop( # noqa: C901 self._macrostep_count += 1 self._microstep_count = 0 - logger.debug( + self._debug( "%s macrostep %d: event=%s", self._log_id, self._macrostep_count, @@ -453,7 +449,7 @@ async def processing_loop( # noqa: C901 event_future = external_event.future try: enabled_transitions = await self.select_transitions(external_event) - logger.debug( + self._debug( "%s Enabled transitions: %s", self._log_id, enabled_transitions ) if enabled_transitions: @@ -494,7 +490,7 @@ async def processing_loop( # noqa: C901 _in_processing_loop.reset(_ctx_token) self._processing.release() - logger.debug("%s Processing loop ended", self._log_id) + self._debug("%s Processing loop ended", self._log_id) result = first_result if first_result is not self._sentinel else None # If the caller has a future, await it (already resolved by now). if caller_future is not None: diff --git a/statemachine/engines/base.py b/statemachine/engines/base.py index c197f83b..360398dd 100644 --- a/statemachine/engines/base.py +++ b/statemachine/engines/base.py @@ -11,11 +11,8 @@ from typing import Dict from typing import List from typing import cast -from weakref import ReferenceType -from weakref import ref from ..event import BoundEvent -from ..event import Event from ..event_data import EventData from ..event_data import TriggerData from ..exceptions import InvalidDefinition @@ -88,7 +85,7 @@ def remove(self, send_id: str): class BaseEngine: def __init__(self, sm: "StateChart"): - self._sm: ReferenceType["StateChart"] = ref(sm) + self.sm: "StateChart" = sm self.external_queue = EventQueue() self.internal_queue = EventQueue() self._sentinel = object() @@ -99,17 +96,12 @@ def __init__(self, sm: "StateChart"): self._macrostep_count: int = 0 self._microstep_count: int = 0 self._log_id = f"[{type(sm).__name__}]" + self._debug = logger.debug if logger.isEnabledFor(logging.DEBUG) else lambda *a, **k: None self._root_parallel_final_pending: "State | None" = None def empty(self): # pragma: no cover return self.external_queue.is_empty() - @property - def sm(self) -> "StateChart": - sm = self._sm() - assert sm, "StateMachine has been destroyed" - return sm - def clear_cache(self): """Clears the cache. Should be called at the start of each processing loop.""" self._cache.clear() @@ -125,7 +117,7 @@ def put(self, trigger_data: TriggerData, internal: bool = False, _delayed: bool self.external_queue.put(trigger_data) if not _delayed: - logger.debug( + self._debug( "%s New event '%s' put on the '%s' queue", self._log_id, trigger_data.event, @@ -180,7 +172,7 @@ def _send_error_execution(self, error: Exception, trigger_data: TriggerData): If already processing an error.execution event, ignore to avoid infinite loops. """ - logger.debug( + self._debug( "%s Error %s captured while executing event=%s", self._log_id, error, @@ -346,6 +338,23 @@ def select_transitions(self, trigger_data: TriggerData) -> OrderedSet[Transition """ return self._select_transitions(trigger_data, lambda t, e: t.match(e)) + def _first_transition_that_matches( + self, + state: State, + trigger_data: TriggerData, + predicate: Callable, + ) -> "Transition | None": + for s in chain([state], state.ancestors()): + transition: Transition + for transition in s.transitions: + if ( + not transition.initial + and predicate(transition, trigger_data.event) + and self._conditions_match(transition, trigger_data) + ): + return transition + return None + def _select_transitions( self, trigger_data: TriggerData, predicate: Callable ) -> OrderedSet[Transition]: @@ -355,23 +364,8 @@ def _select_transitions( # Get atomic states, TODO: sorted by document order atomic_states = (state for state in self.sm.configuration if state.is_atomic) - def first_transition_that_matches( - state: State, event: "Event | None" - ) -> "Transition | None": - for s in chain([state], state.ancestors()): - transition: Transition - for transition in s.transitions: - if ( - not transition.initial - and predicate(transition, event) - and self._conditions_match(transition, trigger_data) - ): - return transition - - return None - for state in atomic_states: - transition = first_transition_that_matches(state, trigger_data.event) + transition = self._first_transition_that_matches(state, trigger_data, predicate) if transition is not None: enabled_transitions.add(transition) @@ -382,7 +376,7 @@ def microstep(self, transitions: List[Transition], trigger_data: TriggerData): This includes exiting states, executing transition content, and entering states. """ self._microstep_count += 1 - logger.debug( + self._debug( "%s macro:%d micro:%d transitions: %s", self._log_id, self._macrostep_count, @@ -469,7 +463,7 @@ def _prepare_exit_states( states_to_exit, key=lambda x: x.state and x.state.document_order or 0, reverse=True ) result = OrderedSet([info.state for info in ordered_states if info.state]) - logger.debug("%s States to exit: %s", self._log_id, result) + self._debug("%s States to exit: %s", self._log_id, result) # Update history for info in ordered_states: @@ -480,7 +474,7 @@ def _prepare_exit_states( else: # shallow history history_value = [s for s in self.sm.configuration if s.parent == state] - logger.debug( + self._debug( "%s Saving '%s.%s' history state: '%s'", self._log_id, state, @@ -494,7 +488,7 @@ def _prepare_exit_states( def _remove_state_from_configuration(self, state: State): """Remove a state from the configuration if not using atomic updates.""" if not self.sm.atomic_configuration_update: - self.sm.configuration -= {state} + self.sm._config.discard(state) def _exit_states( self, enabled_transitions: List[Transition], trigger_data: TriggerData @@ -512,7 +506,7 @@ def _exit_states( # Execute `onexit` handlers — same per-block error isolation as onentry. if info.state is not None: # pragma: no branch - logger.debug("%s Exiting state: %s", self._log_id, info.state) + self._debug("%s Exiting state: %s", self._log_id, info.state) self.sm._callbacks.call(info.state.exit.key, *args, on_error=on_error, **kwargs) self._remove_state_from_configuration(info.state) @@ -566,17 +560,29 @@ def _prepare_entry_states( states_targets_to_enter = OrderedSet(info.state for info in ordered_states if info.state) - new_configuration = cast( - OrderedSet[State], (previous_configuration - states_to_exit) | states_targets_to_enter + # Build new configuration in a single pass instead of two set operations + # (- and |) that each allocate an intermediate OrderedSet. + new_configuration = OrderedSet( + s for s in previous_configuration if s not in states_to_exit ) - logger.debug("%s States to enter: %s", self._log_id, states_targets_to_enter) + new_configuration.update(states_targets_to_enter) + self._debug("%s States to enter: %s", self._log_id, states_targets_to_enter) return ordered_states, states_for_default_entry, default_history_content, new_configuration def _add_state_to_configuration(self, target: State): """Add a state to the configuration if not using atomic updates.""" if not self.sm.atomic_configuration_update: - self.sm.configuration |= {target} + self.sm._config.add(target) + + def stop(self): + """Stop this engine externally (e.g. when a parent cancels a child invocation).""" + self._debug("%s Stopping engine", self._log_id) + self.running = False + try: + self._invoke_manager.cancel_all() + except Exception: # pragma: no cover + self._debug("%s Error stopping engine", self._log_id, exc_info=True) def __del__(self): try: @@ -586,7 +592,7 @@ def __del__(self): def _handle_final_state(self, target: State, on_entry_result: list): """Handle final state entry: queue done events. No direct callback dispatch.""" - logger.debug("%s Reached final state: %s", self._log_id, target) + self._debug("%s Reached final state: %s", self._log_id, target) if target.parent is None: self._invoke_manager.cancel_all() self.running = False @@ -665,7 +671,7 @@ def _enter_states( # noqa: C901 target=target, ) - logger.debug("%s Entering state: %s", self._log_id, target) + self._debug("%s Entering state: %s", self._log_id, target) self._add_state_to_configuration(target) # Execute `onentry` handlers — each handler is a separate block per @@ -765,7 +771,7 @@ def add_descendant_states_to_enter( # noqa: C901 parent_id = state.parent and state.parent.id default_history_content[parent_id] = [info] if state.id in self.sm.history_values: - logger.debug( + self._debug( "%s History state '%s.%s' %s restoring: '%s'", self._log_id, state.parent, @@ -795,7 +801,7 @@ def add_descendant_states_to_enter( # noqa: C901 ) else: # Handle default history content - logger.debug( + self._debug( "%s History state '%s.%s' default content: %s", self._log_id, state.parent, @@ -804,7 +810,8 @@ def add_descendant_states_to_enter( # noqa: C901 ) for transition in state.transitions: - info_history = StateTransition(transition=transition, state=transition.target) + target = cast(State, transition.target) + info_history = StateTransition(transition=transition, state=target) default_history_content[parent_id].append(info_history) self.add_descendant_states_to_enter( info_history, @@ -813,7 +820,8 @@ def add_descendant_states_to_enter( # noqa: C901 default_history_content, ) # noqa: E501 for transition in state.transitions: - info_history = StateTransition(transition=transition, state=transition.target) + target = cast(State, transition.target) + info_history = StateTransition(transition=transition, state=target) self.add_ancestor_states_to_enter( info_history, diff --git a/statemachine/engines/sync.py b/statemachine/engines/sync.py index 6c856505..627b51ae 100644 --- a/statemachine/engines/sync.py +++ b/statemachine/engines/sync.py @@ -1,4 +1,3 @@ -import logging from time import sleep from time import time from typing import TYPE_CHECKING @@ -14,8 +13,6 @@ if TYPE_CHECKING: from ..transition import Transition -logger = logging.getLogger(__name__) - class SyncEngine(BaseEngine): def _run_microstep(self, enabled_transitions, trigger_data): @@ -77,7 +74,7 @@ def processing_loop(self, caller_future=None): # noqa: C901 # We will collect the first result as the processing result to keep backwards compatibility # so we need to use a sentinel object instead of `None` because the first result may # be also `None`, and on this case the `first_result` may be overridden by another result. - logger.debug("%s Processing loop started: %s", self._log_id, self.sm.current_state_value) + self._debug("%s Processing loop started: %s", self._log_id, self.sm.current_state_value) first_result = self._sentinel try: took_events = True @@ -92,7 +89,7 @@ def processing_loop(self, caller_future=None): # noqa: C901 # handles eventless transitions and internal events while not macrostep_done: self._microstep_count = 0 - logger.debug( + self._debug( "%s Macrostep %d: eventless/internal queue", self._log_id, self._macrostep_count, @@ -110,7 +107,7 @@ def processing_loop(self, caller_future=None): # noqa: C901 internal_event = self.internal_queue.pop() enabled_transitions = self.select_transitions(internal_event) if enabled_transitions: - logger.debug( + self._debug( "%s Enabled transitions: %s", self._log_id, enabled_transitions ) took_events = True @@ -130,9 +127,7 @@ def processing_loop(self, caller_future=None): # noqa: C901 self._run_microstep(enabled_transitions, internal_event) # Process external events - logger.debug( - "%s Macrostep %d: external queue", self._log_id, self._macrostep_count - ) + self._debug("%s Macrostep %d: external queue", self._log_id, self._macrostep_count) while not self.external_queue.is_empty(): self.clear_cache() took_events = True @@ -147,7 +142,7 @@ def processing_loop(self, caller_future=None): # noqa: C901 self._macrostep_count += 1 self._microstep_count = 0 - logger.debug( + self._debug( "%s macrostep %d: event=%s", self._log_id, self._macrostep_count, @@ -158,7 +153,7 @@ def processing_loop(self, caller_future=None): # noqa: C901 self._invoke_manager.handle_external_event(external_event) enabled_transitions = self.select_transitions(external_event) - logger.debug("%s Enabled transitions: %s", self._log_id, enabled_transitions) + self._debug("%s Enabled transitions: %s", self._log_id, enabled_transitions) if enabled_transitions: try: result = self.microstep(list(enabled_transitions), external_event) @@ -177,7 +172,7 @@ def processing_loop(self, caller_future=None): # noqa: C901 finally: self._processing.release() - logger.debug("%s Processing loop ended", self._log_id) + self._debug("%s Processing loop ended", self._log_id) return first_result if first_result is not self._sentinel else None def enabled_events(self, *args, **kwargs): diff --git a/statemachine/event.py b/statemachine/event.py index 91c98058..f5986340 100644 --- a/statemachine/event.py +++ b/statemachine/event.py @@ -9,6 +9,7 @@ from .exceptions import InvalidDefinition from .i18n import _ from .transition_mixin import AddCallbacksMixin +from .utils import humanize_id if TYPE_CHECKING: from .statemachine import StateChart @@ -88,6 +89,15 @@ def __new__( id = transitions transitions = None + if id is not None and not isinstance(id, str): + raise InvalidDefinition( + _( + "Event() received a non-string 'id' ({cls_name}). " + "To combine multiple transitions under one event, " + "use the | operator: t1 | t2." + ).format(cls_name=type(id).__name__) + ) + _has_real_id = id is not None id = str(id) if _has_real_id else f"__event__{uuid4().hex}" @@ -98,7 +108,7 @@ def __new__( if name: instance.name = name elif _has_real_id: - instance.name = str(id).replace("_", " ").capitalize() + instance.name = humanize_id(id) else: instance.name = "" if transitions: diff --git a/statemachine/event_data.py b/statemachine/event_data.py index a54c0cc0..9eebfe41 100644 --- a/statemachine/event_data.py +++ b/statemachine/event_data.py @@ -63,8 +63,8 @@ class EventData: source: "State" = field(init=False) """The :ref:`State` which :ref:`statemachine` was in when the Event started.""" - target: "State" = field(init=False) - """The destination :ref:`State` of the :ref:`transition`.""" + target: "State | None" = field(init=False) + """The destination :ref:`State` of the :ref:`transition`, or ``None`` for targetless.""" def __post_init__(self): self.state = self.transition.source diff --git a/statemachine/events.py b/statemachine/events.py index 2fe2be01..16cd681e 100644 --- a/statemachine/events.py +++ b/statemachine/events.py @@ -30,7 +30,7 @@ def add(self, events): if isinstance(event, Event): self._items.append(event) else: - self._items.append(Event(id=event, name=event)) + self._items.append(Event(id=event)) return self diff --git a/statemachine/factory.py b/statemachine/factory.py index d470e3bd..2e9b1c3b 100644 --- a/statemachine/factory.py +++ b/statemachine/factory.py @@ -1,3 +1,4 @@ +import re from typing import Any from typing import Dict from typing import List @@ -91,6 +92,42 @@ def __init__( cls._check() cls._setup() + cls._expand_docstring() + + _STATECHART_RE = re.compile(r"\{statechart:(\w+)\}") + + def _expand_docstring(cls) -> None: + """Replace ``{statechart:FORMAT}`` placeholders in the class docstring.""" + doc = cls.__doc__ + if not doc: + return + + from .contrib.diagram.formatter import formatter + + def _replace(match: "re.Match[str]") -> str: + fmt = match.group(1) + rendered = formatter.render(cls, fmt) # type: ignore[arg-type] + + # Respect the indentation of the placeholder line. + line_start = doc.rfind("\n", 0, match.start()) + if line_start == -1: + indent = "" + else: + indent_match = re.match(r"[ \t]*", doc[line_start + 1 : match.start()]) + indent = indent_match.group() if indent_match else "" + + if indent: + lines = rendered.split("\n") + rendered = lines[0] + "\n" + "\n".join(indent + line for line in lines[1:]) + + return rendered + + cls.__doc__ = cls._STATECHART_RE.sub(_replace, doc) + + def __format__(cls, fmt: str) -> str: + from .contrib.diagram.formatter import formatter + + return formatter.render(cls, fmt) # type: ignore[arg-type] def _initials_by_document_order( # noqa: C901 cls, states: List[State], parent: "State | None" = None, order: int = 1 @@ -273,7 +310,7 @@ def add_from_attributes(cls, attrs): # noqa: C901 cls.add_state(key, value) elif isinstance(value, (Transition, TransitionList)): event_id = _expand_event_id(key) - cls.add_event(event=Event(transitions=value, id=event_id, name=key)) + cls.add_event(event=Event(transitions=value, id=event_id)) elif isinstance(value, (Event,)): if value._has_real_id: event_id = value.id @@ -301,7 +338,7 @@ def _add_unbounded_callback(cls, attr_name, func): # machinery that is stored at ``func.attr_name`` setattr(cls, func.attr_name, func) if func.is_event: - cls.add_event(event=Event(func._transitions, id=attr_name, name=attr_name)) + cls.add_event(event=Event(func._transitions, id=attr_name)) def add_state(cls, id, state: State): state._set_id(id) diff --git a/statemachine/invoke.py b/statemachine/invoke.py index 9c775563..5d09c14d 100644 --- a/statemachine/invoke.py +++ b/statemachine/invoke.py @@ -7,7 +7,6 @@ """ import asyncio -import logging import threading import uuid from concurrent.futures import Future @@ -33,8 +32,6 @@ from .state import State from .statemachine import StateChart -logger = logging.getLogger(__name__) - @runtime_checkable class IInvoke(Protocol): @@ -51,12 +48,7 @@ def _stop_child_machine(child: "StateChart | None") -> None: """Stop a child state machine and cancel all its invocations.""" if child is None: return - logger.debug("invoke: stopping child machine %s", type(child).__name__) - try: - child._engine.running = False - child._engine._invoke_manager.cancel_all() - except Exception: - logger.debug("Error stopping child machine", exc_info=True) + child._engine.stop() class _InvokeCallableWrapper: @@ -282,6 +274,14 @@ def __init__(self, engine: "BaseEngine"): self._active: Dict[str, Invocation] = {} self._pending: "List[Tuple[State, dict]]" = [] + @property + def _debug(self): + return self._engine._debug + + @property + def _log_id(self): + return self._engine._log_id + @property def sm(self) -> "StateChart": return self._engine.sm @@ -302,7 +302,7 @@ def mark_for_invoke(self, state: "State", event_kwargs: "dict | None" = None): def cancel_for_state(self, state: "State"): """Called by ``_exit_states()`` before exiting a state.""" - logger.debug("invoke cancel_for_state: %s", state.id) + self._debug("%s invoke cancel_for_state: %s", self._log_id, state.id) for inv_id, inv in list(self._active.items()): if inv.state_id == state.id and not inv.ctx.cancelled.is_set(): self._cancel(inv_id) @@ -313,7 +313,7 @@ def cancel_for_state(self, state: "State"): def cancel_all(self): """Cancel all active invocations.""" - logger.debug("invoke cancel_all: %d active", len(self._active)) + self._debug("%s invoke cancel_all: %d active", self._log_id, len(self._active)) for inv_id in list(self._active.keys()): self._cancel(inv_id) self._cleanup_terminated() @@ -362,7 +362,7 @@ def _spawn_one_sync(self, callback: "CallbackWrapper", **kwargs): invocation._handler = handler self._active[ctx.invokeid] = invocation - logger.debug("invoke spawn sync: %s on state %s", ctx.invokeid, state.id) + self._debug("%s invoke spawn sync: %s on state %s", self._log_id, ctx.invokeid, state.id) thread = threading.Thread( target=self._run_sync_handler, @@ -400,8 +400,11 @@ def _run_sync_handler( self.sm.send("error.execution", error=e) finally: invocation.terminated = True - logger.debug( - "invoke %s: completed (cancelled=%s)", ctx.invokeid, ctx.cancelled.is_set() + self._debug( + "%s invoke %s: completed (cancelled=%s)", + self._log_id, + ctx.invokeid, + ctx.cancelled.is_set(), ) # --- Async spawning --- @@ -431,7 +434,7 @@ def _spawn_one_async(self, callback: "CallbackWrapper", **kwargs): invocation._handler = handler self._active[ctx.invokeid] = invocation - logger.debug("invoke spawn async: %s on state %s", ctx.invokeid, state.id) + self._debug("%s invoke spawn async: %s on state %s", self._log_id, ctx.invokeid, state.id) loop = asyncio.get_running_loop() task = loop.create_task(self._run_async_handler(callback, handler, ctx, invocation)) @@ -469,8 +472,11 @@ async def _run_async_handler( await self.sm.send("error.execution", error=e) finally: invocation.terminated = True - logger.debug( - "invoke %s: completed (cancelled=%s)", ctx.invokeid, ctx.cancelled.is_set() + self._debug( + "%s invoke %s: completed (cancelled=%s)", + self._log_id, + ctx.invokeid, + ctx.cancelled.is_set(), ) # --- Cancel --- @@ -480,7 +486,7 @@ def _cancel(self, invokeid: str): if not invocation or invocation.ctx.cancelled.is_set(): return - logger.debug("invoke cancel: %s", invokeid) + self._debug("%s invoke cancel: %s", self._log_id, invokeid) # 1) Signal cancellation so the handler can check and stop early. invocation.ctx.cancelled.set() @@ -490,7 +496,7 @@ def _cancel(self, invokeid: str): try: handler.on_cancel() except Exception: - logger.debug("Error in on_cancel for %s", invokeid, exc_info=True) + self._debug("%s Error in on_cancel for %s", self._log_id, invokeid, exc_info=True) # 3) Cancel the async task (raises CancelledError at next await). if invocation.task is not None and not invocation.task.done(): @@ -564,7 +570,9 @@ def handle_external_event(self, trigger_data) -> None: and handler.autoforward and hasattr(handler, "on_event") ): - logger.debug("invoke autoforward: %s -> %s", event_name, inv.invokeid) + self._debug( + "%s invoke autoforward: %s -> %s", self._log_id, event_name, inv.invokeid + ) handler.on_event(event_name, **trigger_data.kwargs) def _make_context( diff --git a/statemachine/io/scxml/actions.py b/statemachine/io/scxml/actions.py index 1da3cc62..b737cd2e 100644 --- a/statemachine/io/scxml/actions.py +++ b/statemachine/io/scxml/actions.py @@ -28,6 +28,7 @@ from .schema import ScriptAction logger = logging.getLogger(__name__) +_debug = logger.debug if logger.isEnabledFor(logging.DEBUG) else lambda *a, **k: None protected_attrs = _event_data_kwargs | {"_sessionid", "_ioprocessors", "_name", "_event"} @@ -220,7 +221,7 @@ def __init__(self, cond: str, processor=None): def __call__(self, *args, **kwargs): result = _eval(self.action, **kwargs) - logger.debug("Cond %s -> %s", self.action, result) + _debug("Cond %s -> %s", self.action, result) return result @staticmethod @@ -298,7 +299,7 @@ def __call__(self, *args, **kwargs): f"{self.action.location}" ) setattr(obj, attr, value) - logger.debug(f"Assign: {self.action.location} = {value!r}") + _debug("Assign: %s = %r", self.action.location, value) class Log(CallableAction): @@ -381,7 +382,7 @@ def create_raise_action_callable(action: RaiseAction) -> Callable: def raise_action(*args, **kwargs): machine: StateChart = kwargs["machine"] - Event(id=action.event, name=action.event, internal=True, _sm=machine).put() + Event(id=action.event, internal=True, _sm=machine).put() raise_action.action = action # type: ignore[attr-defined] return raise_action @@ -491,7 +492,7 @@ def send_action(*args, **kwargs): # noqa: C901 continue params_values[param.name] = _eval(param.expr, **kwargs) - Event(id=event, name=event, delay=delay, internal=internal, _sm=machine).put( + Event(id=event, delay=delay, internal=internal, _sm=machine).put( *content, send_id=send_id, **params_values, diff --git a/statemachine/state.py b/statemachine/state.py index 32c436ff..1d77bc9b 100644 --- a/statemachine/state.py +++ b/statemachine/state.py @@ -1,7 +1,6 @@ from enum import Enum from typing import TYPE_CHECKING from typing import Any -from typing import Dict from typing import Generator from typing import List from typing import cast @@ -12,11 +11,11 @@ from .callbacks import CallbackSpecList from .event import _expand_event_id from .exceptions import InvalidDefinition -from .exceptions import StateMachineError from .i18n import _ from .invoke import normalize_invoke_callbacks from .transition import Transition from .transition_list import TransitionList +from .utils import humanize_id if TYPE_CHECKING: from .statemachine import StateChart @@ -118,10 +117,8 @@ class State: Args: name: A human-readable representation of the state. Default is derived - from the name of the variable assigned to the state machine class. - The name is derived from the id using this logic:: - - name = id.replace("_", " ").capitalize() + from the name of the variable assigned to the state machine class, + by replacing ``_`` and ``.`` with spaces and capitalizing the first word. value: A specific value to the storage and retrieval of states. If specified, you can use It to map a more friendly representation to a low-level @@ -246,6 +243,7 @@ def __init__( raise InvalidDefinition(_("'donedata' can only be specified on final states.")) self.enter.add(donedata, priority=CallbackPriority.INLINE) self.document_order = 0 + self._hash = id(self) self._init_states() def _init_states(self): @@ -267,7 +265,7 @@ def __eq__(self, other): ) def __hash__(self): - return hash(repr(self)) + return self._hash def _setup(self): self.enter.add("on_enter_state", priority=CallbackPriority.GENERIC, is_convention=True) @@ -294,22 +292,6 @@ def __repr__(self): def __str__(self): return self.name - def __get__(self, machine, owner): - if machine is None: - return self - return self.for_instance(machine=machine, cache=machine._states_for_instance) - - def __set__(self, instance, value): - raise StateMachineError( - _("State overriding is not allowed. Trying to add '{}' to {}").format(value, self.id) - ) - - def for_instance(self, machine: "StateChart", cache: Dict["State", "State"]) -> "State": - if self not in cache: - cache[self] = InstanceState(self, machine) - - return cache[self] - @property def id(self) -> str: return self._id @@ -319,7 +301,8 @@ def _set_id(self, id: str) -> "State": if self.value is None: self.value = id if not self.name: - self.name = self._id.replace("_", " ").capitalize() + self.name = humanize_id(self._id) + self._hash = hash((self.name, self._id)) return self @@ -366,67 +349,52 @@ def is_descendant(self, state: "State") -> bool: class InstanceState(State): - """ """ + """Per-instance proxy for a State, delegating attribute access to the underlying State. + + Uses ``__getattr__`` for automatic delegation of instance attributes (name, value, + transitions, etc.) and explicit property overrides for attributes that access private + fields or have custom logic (id, initial, final, parallel, is_active). + """ def __init__( self, state: State, machine: "StateChart", ): - self._state = ref(state) + self._state = state self._machine = ref(machine) + self._hash = hash(state) self._init_states() - def _ref(self) -> State: - """Dereference the weakref, raising if the referent has been collected.""" - state = self._state() - assert state is not None - return state - - @property - def name(self): - return self._ref().name - - @property - def value(self): - return self._ref().value - - @property - def transitions(self): - return self._ref().transitions - - @property - def enter(self): - return self._ref().enter - - @property - def exit(self): - return self._ref().exit - - @property - def invoke(self): - return self._ref().invoke + def __getattr__(self, name: str): + value = getattr(self._state, name) + self.__dict__[name] = value + return value def __eq__(self, other): - return self._ref() == other + return self._state == other def __hash__(self): - return hash(repr(self._ref())) + return self._hash def __repr__(self): - return repr(self._ref()) + return repr(self._state) + + @property + def id(self) -> str: + return self._state._id @property def initial(self): - return self._ref()._initial + return self._state._initial @property def final(self): - return self._ref()._final + return self._state._final @property - def id(self) -> str: - return (self._state() or self)._id # type: ignore[union-attr] + def parallel(self): + return self._state._parallel @property def is_active(self): @@ -434,34 +402,6 @@ def is_active(self): assert machine is not None return self.value in machine.configuration_values - @property - def is_atomic(self): - return self._ref().is_atomic - - @property - def parent(self): - return self._ref().parent - - @property - def states(self): - return self._ref().states - - @property - def history(self): - return self._ref().history - - @property - def parallel(self): - return self._ref().parallel - - @property - def is_compound(self): - return self._ref().is_compound - - @property - def document_order(self): - return self._ref().document_order - class AnyState(State): """A special state that works as a "ANY" placeholder. diff --git a/statemachine/statemachine.py b/statemachine/statemachine.py index 6a5fce59..d33ea122 100644 --- a/statemachine/statemachine.py +++ b/statemachine/statemachine.py @@ -16,6 +16,7 @@ from .callbacks import CallbacksRegistry from .callbacks import SpecListGrouper from .callbacks import SpecReference +from .configuration import Configuration from .dispatcher import Listener from .dispatcher import Listeners from .engines.async_ import AsyncEngine @@ -24,12 +25,14 @@ from .event_data import TriggerData from .exceptions import InvalidDefinition from .exceptions import InvalidStateValue +from .exceptions import StateMachineError from .exceptions import TransitionNotAllowed from .factory import StateMachineMetaclass from .graph import iterate_states_and_transitions from .i18n import _ from .model import Model from .signature import SignatureAdapter +from .state import InstanceState from .utils import run_async_from_sync if TYPE_CHECKING: @@ -150,7 +153,7 @@ def __init__( [start_value] if start_value is not None else list(self.start_configuration_values) ) self._callbacks = CallbacksRegistry() - self._states_for_instance: Dict[State, State] = {} + self._config = self._build_configuration() self._listeners: Dict[int, Any] = {} """Listeners that provides attributes to be used as callbacks.""" @@ -193,6 +196,22 @@ def _resolve_class_listeners(self, **kwargs: Any) -> List[object]: resolved.append(instance) return resolved + def _build_configuration(self) -> Configuration: + """Create InstanceState entries and return a new Configuration.""" + instance_states: Dict[str, Any] = {} + events = self.__class__._events + for state in self.states_map.values(): + ist = InstanceState(state, self) + instance_states[state.id] = ist + if state.id not in events: + vars(self)[state.id] = ist + return Configuration( + instance_states=instance_states, + model=self.model, + state_field=self.state_field, + states_map=self.states_map, + ) + def activate_initial_state(self) -> Any: result = self._engine.activate_initial_state() if not isawaitable(result): @@ -205,6 +224,14 @@ def _processing_loop(self, caller_future: "Any | None" = None) -> Any: return result return run_async_from_sync(result) + def __setattr__(self, name, value): + # Fast path: internal/private attributes are never state IDs. + if not name.startswith("_") and name in self.__class__.states_map: + raise StateMachineError( + _("State overriding is not allowed. Trying to add '{}' to {}").format(value, name) + ) + super().__setattr__(name, value) + def __repr__(self): configuration_ids = [s.id for s in self.configuration] return ( @@ -212,10 +239,15 @@ def __repr__(self): f"configuration={configuration_ids!r})" ) + def __format__(self, fmt: str) -> str: + from .contrib.diagram.formatter import formatter + + return formatter.render(self, fmt) + def __getstate__(self): - state = self.__dict__.copy() + state = {k: v for k, v in self.__dict__.items() if not isinstance(v, InstanceState)} del state["_callbacks"] - del state["_states_for_instance"] + del state["_config"] del state["_engine"] return state @@ -223,7 +255,7 @@ def __setstate__(self, state: Dict[str, Any]) -> None: listeners = state.pop("_listeners") self.__dict__.update(state) # type: ignore[attr-defined] self._callbacks = CallbacksRegistry() - self._states_for_instance = {} + self._config = self._build_configuration() self._listeners = {} # _listeners already contained both class-level and runtime listeners @@ -335,44 +367,16 @@ def _graph(self): def configuration_values(self) -> OrderedSet[Any]: """The state configuration values is the set of currently active states's values (or ids if no custom value is defined).""" - if isinstance(self.current_state_value, OrderedSet): - return self.current_state_value - return OrderedSet([self.current_state_value]) + return self._config.values @property def configuration(self) -> OrderedSet["State"]: """The set of currently active states.""" - if self.current_state_value is None: - return OrderedSet() - - if not isinstance(self.current_state_value, MutableSet): - return OrderedSet( - [ - self.states_map[self.current_state_value].for_instance( - machine=self, - cache=self._states_for_instance, - ) - ] - ) - - return OrderedSet( - [ - self.states_map[value].for_instance( - machine=self, - cache=self._states_for_instance, - ) - for value in self.current_state_value - ] - ) + return self._config.states @configuration.setter def configuration(self, new_configuration: OrderedSet["State"]): - if len(new_configuration) == 0: - self.current_state_value = None - elif len(new_configuration) == 1: - self.current_state_value = new_configuration.pop().value - else: - self.current_state_value = OrderedSet(s.value for s in new_configuration) + self._config.states = new_configuration @property def current_state_value(self): @@ -381,17 +385,11 @@ def current_state_value(self): This is a low level API, that can be used to assign any valid state value completely bypassing all the hooks and validations. """ - return getattr(self.model, self.state_field, None) + return self._config.value @current_state_value.setter def current_state_value(self, value): - if ( - value is not None - and not isinstance(value, MutableSet) - and value not in self.states_map - ): - raise InvalidStateValue(value) - setattr(self.model, self.state_field, value) + self._config.value = value @property def current_state(self) -> "State | MutableSet[State]": @@ -405,36 +403,7 @@ def current_state(self) -> "State | MutableSet[State]": DeprecationWarning, stacklevel=2, ) - current_value = self.current_state_value - - try: - if isinstance(current_value, list): - return OrderedSet( - [ - self.states_map[value].for_instance( - machine=self, - cache=self._states_for_instance, - ) - for value in current_value - ] - ) - - state: State = self.states_map[current_value].for_instance( - machine=self, - cache=self._states_for_instance, - ) - return state - except KeyError as err: - if self.current_state_value is None: - raise InvalidStateValue( - self.current_state_value, - _( - "There's no current state set. In async code, " - "did you activate the initial state? " - "(e.g., `await sm.activate_initial_state()`)" - ), - ) from err - raise InvalidStateValue(self.current_state_value) from err + return self._config.current_state @current_state.setter def current_state(self, value): # pragma: no cover diff --git a/statemachine/utils.py b/statemachine/utils.py index d3661888..ac1ddbb1 100644 --- a/statemachine/utils.py +++ b/statemachine/utils.py @@ -1,7 +1,10 @@ import asyncio +import re import threading from typing import Any +_SEPARATOR_RE = re.compile(r"[_.]") + _cached_loop = threading.local() """Loop that will be used when the SM is running in a synchronous context. One loop per thread.""" @@ -26,6 +29,21 @@ def ensure_iterable(obj): return [obj] +def humanize_id(id: str) -> str: + """Convert a machine identifier to a human-readable name. + + Splits on ``_`` and ``.`` separators and capitalizes the first word. + + >>> humanize_id("go") + 'Go' + >>> humanize_id("done_state_parent") + 'Done state parent' + >>> humanize_id("error.execution") + 'Error execution' + """ + return _SEPARATOR_RE.sub(" ", id).strip().capitalize() + + def run_async_from_sync(coroutine: "Any") -> "Any": """ Compatibility layer to run an async coroutine from a synchronous context. diff --git a/tests/conftest.py b/tests/conftest.py index 105dad84..f229e518 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -22,86 +22,32 @@ def current_time(): @pytest.fixture() def campaign_machine(): - "Define a new class for each test" - from statemachine import State - from statemachine import StateChart - - class CampaignMachine(StateChart): - "A workflow machine" - - draft = State(initial=True) - producing = State("Being produced") - closed = State(final=True) - - add_job = draft.to(draft) | producing.to(producing) - produce = draft.to(producing) - deliver = producing.to(closed) + from tests.machines.workflow.campaign_machine import CampaignMachine return CampaignMachine @pytest.fixture() def campaign_machine_with_validator(): - "Define a new class for each test" - from statemachine import State - from statemachine import StateChart - - class CampaignMachine(StateChart): - "A workflow machine" - - draft = State(initial=True) - producing = State("Being produced") - closed = State(final=True) - - add_job = draft.to(draft) | producing.to(producing) - produce = draft.to(producing, validators="can_produce") - deliver = producing.to(closed) + from tests.machines.workflow.campaign_machine_with_validator import ( + CampaignMachineWithValidator, + ) - def can_produce(*args, **kwargs): - if "goods" not in kwargs: - raise LookupError("Goods not found.") - - return CampaignMachine + return CampaignMachineWithValidator @pytest.fixture() def campaign_machine_with_final_state(): - "Define a new class for each test" - from statemachine import State - from statemachine import StateChart - - class CampaignMachine(StateChart): - "A workflow machine" - - draft = State(initial=True) - producing = State("Being produced") - closed = State(final=True) - - add_job = draft.to(draft) | producing.to(producing) - produce = draft.to(producing) - deliver = producing.to(closed) + from tests.machines.workflow.campaign_machine import CampaignMachine return CampaignMachine @pytest.fixture() def campaign_machine_with_values(): - "Define a new class for each test" - from statemachine import State - from statemachine import StateChart - - class CampaignMachineWithKeys(StateChart): - "A workflow machine" - - draft = State(initial=True, value=1) - producing = State("Being produced", value=2) - closed = State(value=3, final=True) - - add_job = draft.to(draft) | producing.to(producing) - produce = draft.to(producing) - deliver = producing.to(closed) + from tests.machines.workflow.campaign_machine_with_values import CampaignMachineWithValues - return CampaignMachineWithKeys + return CampaignMachineWithValues @pytest.fixture() @@ -153,18 +99,7 @@ def classic_traffic_light_machine_allow_event(classic_traffic_light_machine): @pytest.fixture() def reverse_traffic_light_machine(): - from statemachine import State - from statemachine import StateChart - - class ReverseTrafficLightMachine(StateChart): - "A traffic light machine" - - green = State(initial=True) - yellow = State() - red = State() - - stop = red.from_(yellow, green, red) - cycle = green.from_(red) | yellow.from_(green) | red.from_(yellow) | red.from_.itself() + from tests.machines.workflow.reverse_traffic_light import ReverseTrafficLightMachine return ReverseTrafficLightMachine diff --git a/tests/examples/sqlite_persistent_model_machine.py b/tests/examples/sqlite_persistent_model_machine.py new file mode 100644 index 00000000..da2f26df --- /dev/null +++ b/tests/examples/sqlite_persistent_model_machine.py @@ -0,0 +1,446 @@ +""" +SQLite-backed approval workflow +================================ + +Real-world state machines often need to survive process restarts. This example +shows how to **persist a StateChart configuration to a relational database**, +using the same property getter/setter pattern that ORMs like Django and +SQLAlchemy use under the hood. + +We build a **document approval workflow** where each document must pass both a +legal and a technical review (parallel tracks) before it can be approved. If +**any** reviewer rejects, the document is rejected immediately — the entire +parallel state is exited at once. + +The example also compares two configuration update strategies controlled by +:attr:`~statemachine.statemachine.StateChart.atomic_configuration_update`: + +- **Incremental** (``False``, ``StateChart`` default, SCXML-spec compliant): + the configuration is updated state-by-state as the engine enters and exits + states during a microstep. +- **Atomic** (``True``, ``StateMachine`` default): the full configuration is + computed first and written in a single operation — fewer database writes + per transition. + +""" + +import sqlite3 + +from statemachine.orderedset import OrderedSet + +from statemachine import State +from statemachine import StateChart + +# %% +# Database abstraction +# -------------------- +# +# ``WorkflowDB`` manages two tables: +# +# - **documents** — each row is a domain entity with ``id``, ``title``, +# ``author``, and a ``state`` column that holds the serialized state chart +# configuration. +# - **state_history** — an append-only log of every state mutation, useful for +# auditing, debugging, or building a timeline view. +# +# The state is serialized as a comma-separated string. ``NULL`` means +# "no state yet" (the state chart will enter its initial state on creation). + + +class WorkflowDB: + """SQLite persistence layer for document workflows.""" + + def __init__(self): + self.conn = sqlite3.connect(":memory:") + self.conn.execute( + "CREATE TABLE documents (" + " id INTEGER PRIMARY KEY AUTOINCREMENT," + " title TEXT NOT NULL," + " author TEXT NOT NULL," + " state TEXT" + ")" + ) + self.conn.execute( + "CREATE TABLE state_history (" + " id INTEGER PRIMARY KEY AUTOINCREMENT," + " document_id INTEGER NOT NULL REFERENCES documents(id)," + " old_state TEXT," + " new_state TEXT" + ")" + ) + self.conn.commit() + + def insert_document(self, title, author): + """Insert a new document and return its id.""" + cur = self.conn.execute( + "INSERT INTO documents (title, author) VALUES (?, ?)", (title, author) + ) + self.conn.commit() + return cur.lastrowid + + def find_document(self, doc_id): + """Return ``(title, author)`` for the given document.""" + return self.conn.execute( + "SELECT title, author FROM documents WHERE id = ?", (doc_id,) + ).fetchone() + + def get_state(self, doc_id): + """Read state from the DB and deserialize.""" + raw = self.conn.execute("SELECT state FROM documents WHERE id = ?", (doc_id,)).fetchone()[ + 0 + ] + if raw is None: + return None + parts = raw.split(",") + return parts[0] if len(parts) == 1 else OrderedSet(parts) + + def set_state(self, doc_id, value): + """Serialize state, persist it, and record the mutation in history.""" + old_raw = self.conn.execute( + "SELECT state FROM documents WHERE id = ?", (doc_id,) + ).fetchone()[0] + + if value is None: + new_raw = None + elif isinstance(value, OrderedSet): + new_raw = ",".join(str(v) for v in value) + else: + new_raw = str(value) + + self.conn.execute("UPDATE documents SET state = ? WHERE id = ?", (new_raw, doc_id)) + self.conn.execute( + "INSERT INTO state_history (document_id, old_state, new_state) VALUES (?, ?, ?)", + (doc_id, old_raw, new_raw), + ) + self.conn.commit() + + def all_documents(self): + """Return all rows from the documents table.""" + return self.conn.execute( + "SELECT id, title, author, state FROM documents ORDER BY id" + ).fetchall() + + def history_for(self, doc_id): + """Return the state mutation history for a specific document.""" + return self.conn.execute( + "SELECT id, old_state, new_state FROM state_history WHERE document_id = ? ORDER BY id", + (doc_id,), + ).fetchall() + + def mutation_count(self): + """Return the total number of state mutations recorded.""" + return self.conn.execute("SELECT COUNT(*) FROM state_history").fetchone()[0] + + def close(self): + self.conn.close() + + +# %% +# Domain model +# ------------ +# +# ``Document`` is a domain entity. Its ``state`` property reads from and writes +# to the database **on every access** — each getter call returns a **freshly +# deserialized** object. This is exactly how Django model fields and +# SQLAlchemy column properties work: the ORM never hands you the same Python +# object twice. +# +# Each ``Document`` owns a workflow instance, following the same pattern as +# :class:`~statemachine.mixins.MachineMixin`: the model holds a reference to +# its state machine. The workflow class is injected at creation time, keeping +# the model decoupled from any specific chart definition. + + +class Document: + """A document that needs approval.""" + + def __init__(self, store, doc_id, title, author): + self.store = store + self.id = doc_id + self.title = title + self.author = author + self.workflow: "ApprovalWorkflow | None" = None + + @classmethod + def create(cls, store, workflow_cls, title, author): + """Insert a new document into the DB and start its workflow.""" + doc_id = store.insert_document(title, author) + doc = cls(store, doc_id, title, author) + doc.workflow = workflow_cls(model=doc) + return doc + + @classmethod + def load(cls, store, workflow_cls, doc_id): + """Restore a document and its workflow from the DB.""" + title, author = store.find_document(doc_id) + doc = cls(store, doc_id, title, author) + doc.workflow = workflow_cls(model=doc) + return doc + + @property + def state(self): + return self.store.get_state(self.id) + + @state.setter + def state(self, value): + self.store.set_state(self.id, value) + + def __repr__(self): + config = list(self.workflow.configuration_values) if self.workflow else "?" + return f"Document(#{self.id} {self.title!r} by {self.author}, state={config})" + + +# %% +# Approval workflow +# ----------------- +# +# A document starts as a **draft**. When submitted, it enters a **parallel** +# review state: legal and technical tracks run independently. +# +# - **Both approve** → ``done.state.review`` fires → **approved** +# - **Any reviewer rejects** → exits the entire parallel state → **rejected** + + +class ApprovalWorkflow(StateChart): + """Document approval with parallel legal and technical review tracks.""" + + draft = State("Draft", initial=True) + + class review(State.Parallel): + class legal_track(State.Compound): + legal_pending = State("Legal Pending", initial=True) + legal_approved = State("Legal Approved", final=True) + + approve_legal = legal_pending.to(legal_approved) + + class tech_track(State.Compound): + tech_pending = State("Tech Pending", initial=True) + tech_approved = State("Tech Approved", final=True) + + approve_tech = tech_pending.to(tech_approved) + + submit = draft.to(review) + + approved = State("Approved", final=True) + rejected = State("Rejected", final=True) + + done_state_review = review.to(approved) + reject_legal = review.to(rejected) + reject_tech = review.to(rejected) + + +# %% +# Here is the workflow diagram — note the two parallel regions inside +# ``review`` and the ``reject_legal`` / ``reject_tech`` transitions that exit +# the entire parallel state at once. + +sm = ApprovalWorkflow() + +# %% + +sm + + +# %% +# Display helper +# ~~~~~~~~~~~~~~ + + +def print_table(headers, rows): + """Print a simple formatted table.""" + widths = [len(h) for h in headers] + for row in rows: + for i, val in enumerate(row): + widths[i] = max(widths[i], len(str(val) if val is not None else "NULL")) + fmt = " ".join(f"{{:<{w}}}" for w in widths) + print(fmt.format(*headers)) + print(" ".join("-" * w for w in widths)) + for row in rows: + print(fmt.format(*(str(v) if v is not None else "NULL" for v in row))) + + +# %% +# Incremental configuration updates +# ---------------------------------- +# +# ``StateChart`` defaults to ``atomic_configuration_update=False``, following +# the SCXML specification: the configuration is modified state-by-state as the +# engine enters and exits states during each microstep (``configuration.add()`` +# and ``configuration.discard()`` in the W3C algorithm). +# +# Each ``add()`` or ``discard()`` call triggers the model's ``state`` property +# setter, which writes to the database. This means you'll see **one DB write +# per state** entered or exited — fine for correctness, but chatty for +# persistence layers. + +db_inc = WorkflowDB() + +alice = Document.create(db_inc, ApprovalWorkflow, "RFC-001: API Redesign", "Alice") +bob = Document.create(db_inc, ApprovalWorkflow, "RFC-002: DB Migration", "Bob") + +print(f"Created: {alice}") +print(f"Created: {bob}") + +assert alice.state == "draft" +assert bob.state == "draft" + +# %% +# Alice's document goes through full approval. + +alice.workflow.send("submit") +print(f"After submit: {alice}") + +alice.workflow.send("approve_legal") +print(f"Legal approved: {alice}") +assert "legal_approved" in alice.workflow.configuration_values +assert "tech_pending" in alice.workflow.configuration_values + +alice.workflow.send("approve_tech") +print(f"Fully approved: {alice}") + +# %% +# When both tracks reach their final state, ``done.state.review`` fires +# automatically and the workflow transitions to **approved**. + +assert alice.workflow.approved.is_active +assert alice.state == "approved" + +# %% +# Bob's document is **rejected** by the legal team. The ``reject_legal`` +# event transitions out of the ``review`` parallel state, exiting all child +# states at once — even though technical review hasn't started yet. + +bob.workflow.send("submit") +bob.workflow.send("reject_legal") +print(f"Rejected: {bob}") +assert bob.workflow.rejected.is_active +assert bob.state == "rejected" + +# %% +# Documents table (incremental mode) +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +print() +print_table(["id", "title", "author", "state"], db_inc.all_documents()) + +# %% +# State mutation history — Alice's document +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# +# Every ``add()`` / ``discard()`` call during state entry or exit is a +# separate DB write. The history reveals the step-by-step construction and +# teardown of the parallel configuration: +# +# ``draft`` → ``NULL`` → ``review`` → add ``legal_track`` → add +# ``legal_pending`` → add ``tech_track`` → add ``tech_pending`` → ... + +print() +print_table(["#", "old_state", "new_state"], db_inc.history_for(alice.id)) + +inc_mutations = db_inc.mutation_count() +print(f"\nTotal mutations (incremental, 2 docs): {inc_mutations}") + + +# %% +# Atomic configuration updates +# ----------------------------- +# +# Setting ``atomic_configuration_update=True`` changes the strategy: the +# engine computes the full new configuration first, then writes it in a +# **single** ``setattr`` call. This means one DB write per microstep instead +# of one per state — a significant reduction for parallel charts. +# +# We can enable this with a one-line subclass: + + +class ApprovalWorkflowAtomic(ApprovalWorkflow): + """Same workflow with atomic configuration updates.""" + + atomic_configuration_update = True + + +# %% +# Run the same scenario with atomic updates. + +db_atom = WorkflowDB() + +alice2 = Document.create(db_atom, ApprovalWorkflowAtomic, "RFC-001: API Redesign", "Alice") +bob2 = Document.create(db_atom, ApprovalWorkflowAtomic, "RFC-002: DB Migration", "Bob") + +alice2.workflow.send("submit") +alice2.workflow.send("approve_legal") +alice2.workflow.send("approve_tech") +assert alice2.state == "approved" + +bob2.workflow.send("submit") +bob2.workflow.send("reject_legal") +assert bob2.state == "rejected" + +print(f"Alice: {alice2}") +print(f"Bob: {bob2}") + +# %% +# State mutation history — Alice's document (atomic mode) +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# +# Each microstep now produces **one** DB write with the full configuration. +# No intermediate states are visible. + +print() +print_table(["#", "old_state", "new_state"], db_atom.history_for(alice2.id)) + +atom_mutations = db_atom.mutation_count() +print(f"\nTotal mutations (atomic, 2 docs): {atom_mutations}") + +# %% +# Comparison +# ~~~~~~~~~~ +# +# Both modes produce identical final states, but atomic mode generates +# significantly fewer database writes — especially with parallel states where +# many children are entered and exited simultaneously. + +print(f"\nIncremental: {inc_mutations} mutations") +print(f"Atomic: {atom_mutations} mutations") +assert atom_mutations < inc_mutations + + +# %% +# State restoration from the database +# ------------------------------------ +# +# The real test of persistence: delete the Python objects and recreate them +# from the database. The state chart should resume exactly where it left off, +# preserving even parallel configurations. + +alice_id = alice.id +alice_config = list(alice.workflow.configuration_values) +del alice + +alice_restored = Document.load(db_inc, ApprovalWorkflow, alice_id) +print(f"Restored: {alice_restored}") +assert list(alice_restored.workflow.configuration_values) == alice_config + +# %% +# Bob's rejection is also preserved — no state is lost. + +bob_id = bob.id +del bob + +bob_restored = Document.load(db_inc, ApprovalWorkflow, bob_id) +print(f"Restored: {bob_restored}") +assert bob_restored.state == "rejected" + +# %% +# Final documents table +# ~~~~~~~~~~~~~~~~~~~~~~ + +print() +print_table(["id", "title", "author", "state"], db_inc.all_documents()) + + +# %% +# Cleanup. + +db_inc.close() +db_atom.close() diff --git a/tests/machines/__init__.py b/tests/machines/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/machines/compound/__init__.py b/tests/machines/compound/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/machines/compound/middle_earth_journey.py b/tests/machines/compound/middle_earth_journey.py new file mode 100644 index 00000000..6f087e5c --- /dev/null +++ b/tests/machines/compound/middle_earth_journey.py @@ -0,0 +1,25 @@ +from statemachine import State +from statemachine import StateChart + + +class MiddleEarthJourney(StateChart): + class rivendell(State.Compound): + council = State(initial=True) + preparing = State() + + get_ready = council.to(preparing) + + class moria(State.Compound): + gates = State(initial=True) + bridge = State(final=True) + + cross = gates.to(bridge) + + class lothlorien(State.Compound): + mirror = State(initial=True) + departure = State(final=True) + + leave = mirror.to(departure) + + march_to_moria = rivendell.to(moria) + march_to_lorien = moria.to(lothlorien) diff --git a/tests/machines/compound/middle_earth_journey_two_compounds.py b/tests/machines/compound/middle_earth_journey_two_compounds.py new file mode 100644 index 00000000..f1719527 --- /dev/null +++ b/tests/machines/compound/middle_earth_journey_two_compounds.py @@ -0,0 +1,18 @@ +from statemachine import State +from statemachine import StateChart + + +class MiddleEarthJourneyTwoCompounds(StateChart): + class rivendell(State.Compound): + council = State(initial=True) + preparing = State() + + get_ready = council.to(preparing) + + class moria(State.Compound): + gates = State(initial=True) + bridge = State(final=True) + + cross = gates.to(bridge) + + march_to_moria = rivendell.to(moria) diff --git a/tests/machines/compound/middle_earth_journey_with_finals.py b/tests/machines/compound/middle_earth_journey_with_finals.py new file mode 100644 index 00000000..e5cc0148 --- /dev/null +++ b/tests/machines/compound/middle_earth_journey_with_finals.py @@ -0,0 +1,25 @@ +from statemachine import State +from statemachine import StateChart + + +class MiddleEarthJourneyWithFinals(StateChart): + class rivendell(State.Compound): + council = State(initial=True) + preparing = State(final=True) + + get_ready = council.to(preparing) + + class moria(State.Compound): + gates = State(initial=True) + bridge = State(final=True) + + cross = gates.to(bridge) + + class lothlorien(State.Compound): + mirror = State(initial=True) + departure = State(final=True) + + leave = mirror.to(departure) + + march_to_moria = rivendell.to(moria) + march_to_lorien = moria.to(lothlorien) diff --git a/tests/machines/compound/moria_expedition.py b/tests/machines/compound/moria_expedition.py new file mode 100644 index 00000000..dc287da7 --- /dev/null +++ b/tests/machines/compound/moria_expedition.py @@ -0,0 +1,15 @@ +from statemachine import State +from statemachine import StateChart + + +class MoriaExpedition(StateChart): + class moria(State.Compound): + class upper_halls(State.Compound): + entrance = State(initial=True) + bridge = State(final=True) + + cross = entrance.to(bridge) + + assert isinstance(upper_halls, State) + depths = State(final=True) + descend = upper_halls.to(depths) diff --git a/tests/machines/compound/moria_expedition_with_escape.py b/tests/machines/compound/moria_expedition_with_escape.py new file mode 100644 index 00000000..4fc4d4d9 --- /dev/null +++ b/tests/machines/compound/moria_expedition_with_escape.py @@ -0,0 +1,18 @@ +from statemachine import State +from statemachine import StateChart + + +class MoriaExpeditionWithEscape(StateChart): + class moria(State.Compound): + class upper_halls(State.Compound): + entrance = State(initial=True) + bridge = State() + + cross = entrance.to(bridge) + + assert isinstance(upper_halls, State) + depths = State(final=True) + descend = upper_halls.to(depths) + + daylight = State(final=True) + escape = moria.to(daylight) diff --git a/tests/machines/compound/quest_for_erebor.py b/tests/machines/compound/quest_for_erebor.py new file mode 100644 index 00000000..b5a96b70 --- /dev/null +++ b/tests/machines/compound/quest_for_erebor.py @@ -0,0 +1,13 @@ +from statemachine import State +from statemachine import StateChart + + +class QuestForErebor(StateChart): + class lonely_mountain(State.Compound): + approach = State(initial=True) + inside = State(final=True) + + enter_mountain = approach.to(inside) + + victory = State(final=True) + done_state_lonely_mountain = lonely_mountain.to(victory) diff --git a/tests/machines/compound/shire_to_rivendell.py b/tests/machines/compound/shire_to_rivendell.py new file mode 100644 index 00000000..70de8951 --- /dev/null +++ b/tests/machines/compound/shire_to_rivendell.py @@ -0,0 +1,13 @@ +from statemachine import State +from statemachine import StateChart + + +class ShireToRivendell(StateChart): + class shire(State.Compound): + bag_end = State(initial=True) + green_dragon = State() + + visit_pub = bag_end.to(green_dragon) + + road = State(final=True) + depart = shire.to(road) diff --git a/tests/machines/donedata/__init__.py b/tests/machines/donedata/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/machines/donedata/destroy_the_ring.py b/tests/machines/donedata/destroy_the_ring.py new file mode 100644 index 00000000..2c65e7de --- /dev/null +++ b/tests/machines/donedata/destroy_the_ring.py @@ -0,0 +1,20 @@ +from statemachine import Event +from statemachine import State +from statemachine import StateChart + + +class DestroyTheRing(StateChart): + class quest(State.Compound): + traveling = State(initial=True) + completed = State(final=True, donedata="get_quest_result") + + finish = traveling.to(completed) + + def get_quest_result(self): + return {"ring_destroyed": True, "hero": "frodo"} + + epilogue = State(final=True) + done_state_quest = Event(quest.to(epilogue, on="capture_result")) # type: ignore[arg-type] + + def capture_result(self, ring_destroyed=None, hero=None, **kwargs): + self.received = {"ring_destroyed": ring_destroyed, "hero": hero} diff --git a/tests/machines/donedata/destroy_the_ring_simple.py b/tests/machines/donedata/destroy_the_ring_simple.py new file mode 100644 index 00000000..02197f9e --- /dev/null +++ b/tests/machines/donedata/destroy_the_ring_simple.py @@ -0,0 +1,17 @@ +from statemachine import Event +from statemachine import State +from statemachine import StateChart + + +class DestroyTheRingSimple(StateChart): + class quest(State.Compound): + traveling = State(initial=True) + completed = State(final=True, donedata="get_result") + + finish = traveling.to(completed) + + def get_result(self): + return {"outcome": "victory"} + + celebration = State(final=True) + done_state_quest = Event(quest.to(celebration)) # type: ignore[arg-type] diff --git a/tests/machines/donedata/nested_quest_donedata.py b/tests/machines/donedata/nested_quest_donedata.py new file mode 100644 index 00000000..c1787a32 --- /dev/null +++ b/tests/machines/donedata/nested_quest_donedata.py @@ -0,0 +1,22 @@ +from statemachine import Event +from statemachine import State +from statemachine import StateChart + + +class NestedQuestDoneData(StateChart): + class outer(State.Compound): + class inner(State.Compound): + start = State(initial=True) + end = State(final=True, donedata="inner_result") + + go = start.to(end) + + def inner_result(self): + return {"level": "inner"} + + assert isinstance(inner, State) + after_inner = State(final=True) + done_state_inner = Event(inner.to(after_inner)) # type: ignore[arg-type] + + final = State(final=True) + done_state_outer = Event(outer.to(final)) # type: ignore[arg-type] diff --git a/tests/machines/donedata/quest_for_erebor_done_convention.py b/tests/machines/donedata/quest_for_erebor_done_convention.py new file mode 100644 index 00000000..553297c9 --- /dev/null +++ b/tests/machines/donedata/quest_for_erebor_done_convention.py @@ -0,0 +1,13 @@ +from statemachine import State +from statemachine import StateChart + + +class QuestForEreborDoneConvention(StateChart): + class quest(State.Compound): + traveling = State(initial=True) + arrived = State(final=True) + + finish = traveling.to(arrived) + + celebration = State(final=True) + done_state_quest = quest.to(celebration) diff --git a/tests/machines/donedata/quest_for_erebor_explicit_id.py b/tests/machines/donedata/quest_for_erebor_explicit_id.py new file mode 100644 index 00000000..075f6eca --- /dev/null +++ b/tests/machines/donedata/quest_for_erebor_explicit_id.py @@ -0,0 +1,14 @@ +from statemachine import Event +from statemachine import State +from statemachine import StateChart + + +class QuestForEreborExplicitId(StateChart): + class quest(State.Compound): + traveling = State(initial=True) + arrived = State(final=True) + + finish = traveling.to(arrived) + + celebration = State(final=True) + done_state_quest = Event(quest.to(celebration), id="done.state.quest") # type: ignore[arg-type] diff --git a/tests/machines/donedata/quest_for_erebor_multi_word.py b/tests/machines/donedata/quest_for_erebor_multi_word.py new file mode 100644 index 00000000..7fb842da --- /dev/null +++ b/tests/machines/donedata/quest_for_erebor_multi_word.py @@ -0,0 +1,13 @@ +from statemachine import State +from statemachine import StateChart + + +class QuestForEreborMultiWord(StateChart): + class lonely_mountain(State.Compound): + approach = State(initial=True) + inside = State(final=True) + + enter_mountain = approach.to(inside) + + victory = State(final=True) + done_state_lonely_mountain = lonely_mountain.to(victory) diff --git a/tests/machines/donedata/quest_for_erebor_with_event.py b/tests/machines/donedata/quest_for_erebor_with_event.py new file mode 100644 index 00000000..e2767fab --- /dev/null +++ b/tests/machines/donedata/quest_for_erebor_with_event.py @@ -0,0 +1,14 @@ +from statemachine import Event +from statemachine import State +from statemachine import StateChart + + +class QuestForEreborWithEvent(StateChart): + class quest(State.Compound): + traveling = State(initial=True) + arrived = State(final=True) + + finish = traveling.to(arrived) + + celebration = State(final=True) + done_state_quest = Event(quest.to(celebration)) # type: ignore[arg-type] diff --git a/tests/machines/error/__init__.py b/tests/machines/error/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/machines/error/error_convention_event.py b/tests/machines/error/error_convention_event.py new file mode 100644 index 00000000..72c99ce5 --- /dev/null +++ b/tests/machines/error/error_convention_event.py @@ -0,0 +1,16 @@ +from statemachine import Event +from statemachine import State +from statemachine import StateChart + + +class ErrorConventionEventSC(StateChart): + """Using Event without explicit id with error_ prefix auto-registers dot notation.""" + + s1 = State("s1", initial=True) + error_state = State("error_state", final=True) + + go = s1.to(s1, on="bad_action") + error_execution = Event(s1.to(error_state)) + + def bad_action(self): + raise RuntimeError("action failed") diff --git a/tests/machines/error/error_convention_transition_list.py b/tests/machines/error/error_convention_transition_list.py new file mode 100644 index 00000000..f1445387 --- /dev/null +++ b/tests/machines/error/error_convention_transition_list.py @@ -0,0 +1,15 @@ +from statemachine import State +from statemachine import StateChart + + +class ErrorConventionTransitionListSC(StateChart): + """Using bare TransitionList with error_ prefix auto-registers dot notation.""" + + s1 = State("s1", initial=True) + error_state = State("error_state", final=True) + + go = s1.to(s1, on="bad_action") + error_execution = s1.to(error_state) + + def bad_action(self): + raise RuntimeError("action failed") diff --git a/tests/machines/error/error_in_action_sc.py b/tests/machines/error/error_in_action_sc.py new file mode 100644 index 00000000..b016bba9 --- /dev/null +++ b/tests/machines/error/error_in_action_sc.py @@ -0,0 +1,15 @@ +from statemachine import Event +from statemachine import State +from statemachine import StateChart + + +class ErrorInActionSC(StateChart): + s1 = State("s1", initial=True) + s2 = State("s2") + error_state = State("error_state", final=True) + + go = s1.to(s2, on="bad_action") + error_execution = Event(s1.to(error_state) | s2.to(error_state), id="error.execution") + + def bad_action(self): + raise RuntimeError("action failed") diff --git a/tests/machines/error/error_in_action_sm_with_flag.py b/tests/machines/error/error_in_action_sm_with_flag.py new file mode 100644 index 00000000..95e03435 --- /dev/null +++ b/tests/machines/error/error_in_action_sm_with_flag.py @@ -0,0 +1,17 @@ +from statemachine import Event +from statemachine import State +from statemachine import StateChart + + +class ErrorInActionSMWithFlag(StateChart): + """StateChart subclass (catch_errors_as_events = True by default).""" + + s1 = State("s1", initial=True) + s2 = State("s2") + error_state = State("error_state", final=True) + + go = s1.to(s2, on="bad_action") + error_execution = Event(s1.to(error_state) | s2.to(error_state), id="error.execution") + + def bad_action(self): + raise RuntimeError("action failed") diff --git a/tests/machines/error/error_in_after_sc.py b/tests/machines/error/error_in_after_sc.py new file mode 100644 index 00000000..f507d82c --- /dev/null +++ b/tests/machines/error/error_in_after_sc.py @@ -0,0 +1,15 @@ +from statemachine import Event +from statemachine import State +from statemachine import StateChart + + +class ErrorInAfterSC(StateChart): + s1 = State("s1", initial=True) + s2 = State("s2") + error_state = State("error_state", final=True) + + go = s1.to(s2, after="bad_after") + error_execution = Event(s2.to(error_state), id="error.execution") + + def bad_after(self): + raise RuntimeError("after failed") diff --git a/tests/machines/error/error_in_error_handler_sc.py b/tests/machines/error/error_in_error_handler_sc.py new file mode 100644 index 00000000..49d5de4e --- /dev/null +++ b/tests/machines/error/error_in_error_handler_sc.py @@ -0,0 +1,24 @@ +from statemachine import Event +from statemachine import State +from statemachine import StateChart + + +class ErrorInErrorHandlerSC(StateChart): + """Error in error.execution handler should not cause infinite loop.""" + + s1 = State("s1", initial=True) + s2 = State("s2") + s3 = State("s3", final=True) + + go = s1.to(s2, on="bad_action") + finish = s2.to(s3) + error_execution = Event( + s1.to(s1, on="bad_error_handler") | s2.to(s2, on="bad_error_handler"), + id="error.execution", + ) + + def bad_action(self): + raise RuntimeError("action failed") + + def bad_error_handler(self): + raise RuntimeError("error handler also failed") diff --git a/tests/machines/error/error_in_guard_sc.py b/tests/machines/error/error_in_guard_sc.py new file mode 100644 index 00000000..24be427a --- /dev/null +++ b/tests/machines/error/error_in_guard_sc.py @@ -0,0 +1,14 @@ +from statemachine import Event +from statemachine import State +from statemachine import StateChart + + +class ErrorInGuardSC(StateChart): + initial = State("initial", initial=True) + error_state = State("error_state", final=True) + + go = initial.to(initial, cond="bad_guard") | initial.to(initial) + error_execution = Event(initial.to(error_state), id="error.execution") + + def bad_guard(self): + raise RuntimeError("guard failed") diff --git a/tests/machines/error/error_in_guard_sm.py b/tests/machines/error/error_in_guard_sm.py new file mode 100644 index 00000000..1b622453 --- /dev/null +++ b/tests/machines/error/error_in_guard_sm.py @@ -0,0 +1,15 @@ +from statemachine import State +from statemachine import StateChart + + +class ErrorInGuardSM(StateChart): + """StateChart subclass with catch_errors_as_events=False: exceptions should propagate.""" + + catch_errors_as_events = False + + initial = State("initial", initial=True) + + go = initial.to(initial, cond="bad_guard") | initial.to(initial) + + def bad_guard(self): + raise RuntimeError("guard failed") diff --git a/tests/machines/error/error_in_on_enter_sc.py b/tests/machines/error/error_in_on_enter_sc.py new file mode 100644 index 00000000..966e9c9b --- /dev/null +++ b/tests/machines/error/error_in_on_enter_sc.py @@ -0,0 +1,15 @@ +from statemachine import Event +from statemachine import State +from statemachine import StateChart + + +class ErrorInOnEnterSC(StateChart): + s1 = State("s1", initial=True) + s2 = State("s2") + error_state = State("error_state", final=True) + + go = s1.to(s2) + error_execution = Event(s1.to(error_state) | s2.to(error_state), id="error.execution") + + def on_enter_s2(self): + raise RuntimeError("on_enter failed") diff --git a/tests/machines/eventless/__init__.py b/tests/machines/eventless/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/machines/eventless/auto_advance.py b/tests/machines/eventless/auto_advance.py new file mode 100644 index 00000000..00c5dc56 --- /dev/null +++ b/tests/machines/eventless/auto_advance.py @@ -0,0 +1,15 @@ +from statemachine import State +from statemachine import StateChart + + +class AutoAdvance(StateChart): + class journey(State.Compound): + step1 = State(initial=True) + step2 = State() + step3 = State(final=True) + + step1.to(step2) + step2.to(step3) + + done = State(final=True) + done_state_journey = journey.to(done) diff --git a/tests/machines/eventless/beacon_chain.py b/tests/machines/eventless/beacon_chain.py new file mode 100644 index 00000000..9747e00a --- /dev/null +++ b/tests/machines/eventless/beacon_chain.py @@ -0,0 +1,13 @@ +from statemachine import State +from statemachine import StateChart + + +class BeaconChain(StateChart): + class beacons(State.Compound): + first = State(initial=True) + last = State(final=True) + + first.to(last) + + signal_received = State(final=True) + done_state_beacons = beacons.to(signal_received) diff --git a/tests/machines/eventless/beacon_chain_lighting.py b/tests/machines/eventless/beacon_chain_lighting.py new file mode 100644 index 00000000..90771e63 --- /dev/null +++ b/tests/machines/eventless/beacon_chain_lighting.py @@ -0,0 +1,18 @@ +from statemachine import State +from statemachine import StateChart + + +class BeaconChainLighting(StateChart): + class chain(State.Compound): + amon_din = State(initial=True) + eilenach = State() + nardol = State() + halifirien = State(final=True) + + # Eventless chain: each fires immediately + amon_din.to(eilenach) + eilenach.to(nardol) + nardol.to(halifirien) + + all_lit = State(final=True) + done_state_chain = chain.to(all_lit) diff --git a/tests/machines/eventless/coordinated_advance.py b/tests/machines/eventless/coordinated_advance.py new file mode 100644 index 00000000..c80b97a9 --- /dev/null +++ b/tests/machines/eventless/coordinated_advance.py @@ -0,0 +1,18 @@ +from statemachine import State +from statemachine import StateChart + + +class CoordinatedAdvance(StateChart): + class forces(State.Parallel): + class vanguard(State.Compound): + waiting = State(initial=True) + advanced = State(final=True) + + move_forward = waiting.to(advanced) + + class rearguard(State.Compound): + holding = State(initial=True) + moved_up = State(final=True) + + # Eventless: advance only when vanguard has advanced + holding.to(moved_up, cond="In('advanced')") diff --git a/tests/machines/eventless/ring_corruption.py b/tests/machines/eventless/ring_corruption.py new file mode 100644 index 00000000..67893553 --- /dev/null +++ b/tests/machines/eventless/ring_corruption.py @@ -0,0 +1,18 @@ +from statemachine import State +from statemachine import StateChart + + +class RingCorruption(StateChart): + resisting = State(initial=True) + corrupted = State(final=True) + + # eventless: no event name + resisting.to(corrupted, cond="is_corrupted") + + ring_power = 0 + + def is_corrupted(self): + return self.ring_power > 5 + + def increase_power(self): + self.ring_power += 3 diff --git a/tests/machines/eventless/ring_corruption_with_bear_ring.py b/tests/machines/eventless/ring_corruption_with_bear_ring.py new file mode 100644 index 00000000..f211b1fb --- /dev/null +++ b/tests/machines/eventless/ring_corruption_with_bear_ring.py @@ -0,0 +1,18 @@ +from statemachine import State +from statemachine import StateChart + + +class RingCorruptionWithBearRing(StateChart): + resisting = State(initial=True) + corrupted = State(final=True) + + resisting.to(corrupted, cond="is_corrupted") + bear_ring = resisting.to.itself(internal=True, on="increase_power") + + ring_power = 0 + + def is_corrupted(self): + return self.ring_power > 5 + + def increase_power(self): + self.ring_power += 2 diff --git a/tests/machines/eventless/ring_corruption_with_tick.py b/tests/machines/eventless/ring_corruption_with_tick.py new file mode 100644 index 00000000..4a6f1c0d --- /dev/null +++ b/tests/machines/eventless/ring_corruption_with_tick.py @@ -0,0 +1,15 @@ +from statemachine import State +from statemachine import StateChart + + +class RingCorruptionWithTick(StateChart): + resisting = State(initial=True) + corrupted = State(final=True) + + resisting.to(corrupted, cond="is_corrupted") + tick = resisting.to.itself(internal=True) + + ring_power = 0 + + def is_corrupted(self): + return self.ring_power > 5 diff --git a/tests/machines/history/__init__.py b/tests/machines/history/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/machines/history/deep_memory_of_moria.py b/tests/machines/history/deep_memory_of_moria.py new file mode 100644 index 00000000..44e15b03 --- /dev/null +++ b/tests/machines/history/deep_memory_of_moria.py @@ -0,0 +1,21 @@ +from statemachine import HistoryState +from statemachine import State +from statemachine import StateChart + + +class DeepMemoryOfMoria(StateChart): + class moria(State.Compound): + class halls(State.Compound): + entrance = State(initial=True) + chamber = State() + + explore = entrance.to(chamber) + + assert isinstance(halls, State) + h = HistoryState(type="deep") + bridge = State(final=True) + flee = halls.to(bridge) + + outside = State() + escape = moria.to(outside) + return_deep = outside.to(moria.h) # type: ignore[has-type] diff --git a/tests/machines/history/gollum_personality.py b/tests/machines/history/gollum_personality.py new file mode 100644 index 00000000..8f7a1abb --- /dev/null +++ b/tests/machines/history/gollum_personality.py @@ -0,0 +1,17 @@ +from statemachine import HistoryState +from statemachine import State +from statemachine import StateChart + + +class GollumPersonality(StateChart): + class personality(State.Compound): + smeagol = State(initial=True) + gollum = State() + h = HistoryState() + + dark_side = smeagol.to(gollum) + light_side = gollum.to(smeagol) + + outside = State() + leave = personality.to(outside) + return_via_history = outside.to(personality.h) diff --git a/tests/machines/history/gollum_personality_default_gollum.py b/tests/machines/history/gollum_personality_default_gollum.py new file mode 100644 index 00000000..861dcaa3 --- /dev/null +++ b/tests/machines/history/gollum_personality_default_gollum.py @@ -0,0 +1,17 @@ +from statemachine import HistoryState +from statemachine import State +from statemachine import StateChart + + +class GollumPersonalityDefaultGollum(StateChart): + class personality(State.Compound): + smeagol = State(initial=True) + gollum = State() + h = HistoryState() + + dark_side = smeagol.to(gollum) + _ = h.to(gollum) # default: gollum (not the initial smeagol) + + outside = State(initial=True) + enter_via_history = outside.to(personality.h) + leave = personality.to(outside) diff --git a/tests/machines/history/gollum_personality_with_default.py b/tests/machines/history/gollum_personality_with_default.py new file mode 100644 index 00000000..41b5d9be --- /dev/null +++ b/tests/machines/history/gollum_personality_with_default.py @@ -0,0 +1,17 @@ +from statemachine import HistoryState +from statemachine import State +from statemachine import StateChart + + +class GollumPersonalityWithDefault(StateChart): + class personality(State.Compound): + smeagol = State(initial=True) + gollum = State() + h = HistoryState() + + dark_side = smeagol.to(gollum) + _ = h.to(smeagol) # default: smeagol + + outside = State(initial=True) + enter_via_history = outside.to(personality.h) + leave = personality.to(outside) diff --git a/tests/machines/history/shallow_moria.py b/tests/machines/history/shallow_moria.py new file mode 100644 index 00000000..2b85e43b --- /dev/null +++ b/tests/machines/history/shallow_moria.py @@ -0,0 +1,21 @@ +from statemachine import HistoryState +from statemachine import State +from statemachine import StateChart + + +class ShallowMoria(StateChart): + class moria(State.Compound): + class halls(State.Compound): + entrance = State(initial=True) + chamber = State() + + explore = entrance.to(chamber) + + assert isinstance(halls, State) + h = HistoryState() + bridge = State(final=True) + flee = halls.to(bridge) + + outside = State() + escape = moria.to(outside) + return_shallow = outside.to(moria.h) # type: ignore[has-type] diff --git a/tests/machines/in_condition/__init__.py b/tests/machines/in_condition/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/machines/in_condition/combined_guard.py b/tests/machines/in_condition/combined_guard.py new file mode 100644 index 00000000..29f379c1 --- /dev/null +++ b/tests/machines/in_condition/combined_guard.py @@ -0,0 +1,18 @@ +from statemachine import State +from statemachine import StateChart + + +class CombinedGuard(StateChart): + class positions(State.Parallel): + class scout(State.Compound): + out = State(initial=True) + back = State(final=True) + + return_scout = out.to(back) + + class warrior(State.Compound): + idle = State(initial=True) + attacking = State(final=True) + + # Only attacks when scout is back + charge = idle.to(attacking, cond="In('back')") diff --git a/tests/machines/in_condition/descendant_check.py b/tests/machines/in_condition/descendant_check.py new file mode 100644 index 00000000..354847d8 --- /dev/null +++ b/tests/machines/in_condition/descendant_check.py @@ -0,0 +1,15 @@ +from statemachine import State +from statemachine import StateChart + + +class DescendantCheck(StateChart): + class realm(State.Compound): + village = State(initial=True) + castle = State() + + ascend = village.to(castle) + + conquered = State(final=True) + # Guarded by being inside the castle + conquer = realm.to(conquered, cond="In('castle')") + explore = realm.to.itself(internal=True) # type: ignore[attr-defined] diff --git a/tests/machines/in_condition/eventless_in.py b/tests/machines/in_condition/eventless_in.py new file mode 100644 index 00000000..7ffbdf7c --- /dev/null +++ b/tests/machines/in_condition/eventless_in.py @@ -0,0 +1,18 @@ +from statemachine import State +from statemachine import StateChart + + +class EventlessIn(StateChart): + class coordination(State.Parallel): + class leader(State.Compound): + planning = State(initial=True) + ready = State(final=True) + + get_ready = planning.to(ready) + + class follower(State.Compound): + waiting = State(initial=True) + moving = State(final=True) + + # Eventless: move when leader is ready + waiting.to(moving, cond="In('ready')") diff --git a/tests/machines/in_condition/fellowship.py b/tests/machines/in_condition/fellowship.py new file mode 100644 index 00000000..9b9e2674 --- /dev/null +++ b/tests/machines/in_condition/fellowship.py @@ -0,0 +1,18 @@ +from statemachine import State +from statemachine import StateChart + + +class Fellowship(StateChart): + class positions(State.Parallel): + class frodo(State.Compound): + shire_f = State(initial=True) + mordor_f = State(final=True) + + journey = shire_f.to(mordor_f) + + class sam(State.Compound): + shire_s = State(initial=True) + mordor_s = State(final=True) + + # Sam follows Frodo: eventless, guarded by In('mordor_f') + shire_s.to(mordor_s, cond="In('mordor_f')") diff --git a/tests/machines/in_condition/fellowship_coordination.py b/tests/machines/in_condition/fellowship_coordination.py new file mode 100644 index 00000000..87aa81bd --- /dev/null +++ b/tests/machines/in_condition/fellowship_coordination.py @@ -0,0 +1,18 @@ +from statemachine import State +from statemachine import StateChart + + +class FellowshipCoordination(StateChart): + class mission(State.Parallel): + class scouts(State.Compound): + scouting = State(initial=True) + reported = State(final=True) + + report = scouting.to(reported) + + class army(State.Compound): + waiting = State(initial=True) + marching = State(final=True) + + # Army marches only after scouts report + waiting.to(marching, cond="In('reported')") diff --git a/tests/machines/in_condition/gate_of_moria.py b/tests/machines/in_condition/gate_of_moria.py new file mode 100644 index 00000000..eb01c423 --- /dev/null +++ b/tests/machines/in_condition/gate_of_moria.py @@ -0,0 +1,13 @@ +from statemachine import State +from statemachine import StateChart + + +class GateOfMoria(StateChart): + outside = State(initial=True) + at_gate = State() + inside = State(final=True) + + approach = outside.to(at_gate) + # Can only enter if we are at the gate + enter_gate = outside.to(inside, cond="In('at_gate')") + speak_friend = at_gate.to(inside) diff --git a/tests/machines/parallel/__init__.py b/tests/machines/parallel/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/machines/parallel/session.py b/tests/machines/parallel/session.py new file mode 100644 index 00000000..78ec6d74 --- /dev/null +++ b/tests/machines/parallel/session.py @@ -0,0 +1,17 @@ +from statemachine import State +from statemachine import StateChart + + +class Session(StateChart): + class session(State.Parallel): + class ui(State.Compound): + active = State(initial=True) + closed = State(final=True) + + close_ui = active.to(closed) + + class backend(State.Compound): + running = State(initial=True) + stopped = State(final=True) + + stop_backend = running.to(stopped) diff --git a/tests/machines/parallel/session_with_done_state.py b/tests/machines/parallel/session_with_done_state.py new file mode 100644 index 00000000..0804e751 --- /dev/null +++ b/tests/machines/parallel/session_with_done_state.py @@ -0,0 +1,20 @@ +from statemachine import State +from statemachine import StateChart + + +class SessionWithDoneState(StateChart): + class session(State.Parallel): + class ui(State.Compound): + active = State(initial=True) + closed = State(final=True) + + close_ui = active.to(closed) + + class backend(State.Compound): + running = State(initial=True) + stopped = State(final=True) + + stop_backend = running.to(stopped) + + finished = State(final=True) + done_state_session = session.to(finished) diff --git a/tests/machines/parallel/two_towers.py b/tests/machines/parallel/two_towers.py new file mode 100644 index 00000000..3abcda59 --- /dev/null +++ b/tests/machines/parallel/two_towers.py @@ -0,0 +1,20 @@ +from statemachine import State +from statemachine import StateChart + + +class TwoTowers(StateChart): + class battle(State.Parallel): + class helms_deep(State.Compound): + fighting = State(initial=True) + victory = State(final=True) + + win = fighting.to(victory) + + class isengard(State.Compound): + besieging = State(initial=True) + flooded = State(final=True) + + flood = besieging.to(flooded) + + aftermath = State(final=True) + done_state_battle = battle.to(aftermath) diff --git a/tests/machines/parallel/war_of_the_ring.py b/tests/machines/parallel/war_of_the_ring.py new file mode 100644 index 00000000..04fb6d2b --- /dev/null +++ b/tests/machines/parallel/war_of_the_ring.py @@ -0,0 +1,25 @@ +from statemachine import State +from statemachine import StateChart + + +class WarOfTheRing(StateChart): + class war(State.Parallel): + class frodos_quest(State.Compound): + shire = State(initial=True) + mordor = State() + mount_doom = State(final=True) + + journey = shire.to(mordor) + destroy_ring = mordor.to(mount_doom) + + class aragorns_path(State.Compound): + ranger = State(initial=True) + king = State(final=True) + + coronation = ranger.to(king) + + class gandalfs_defense(State.Compound): + rohan = State(initial=True) + gondor = State(final=True) + + ride_to_gondor = rohan.to(gondor) diff --git a/tests/machines/parallel/war_with_exit.py b/tests/machines/parallel/war_with_exit.py new file mode 100644 index 00000000..f89b5986 --- /dev/null +++ b/tests/machines/parallel/war_with_exit.py @@ -0,0 +1,20 @@ +from statemachine import State +from statemachine import StateChart + + +class WarWithExit(StateChart): + class war(State.Parallel): + class front_a(State.Compound): + fighting = State(initial=True) + won = State(final=True) + + win_a = fighting.to(won) + + class front_b(State.Compound): + holding = State(initial=True) + held = State(final=True) + + hold_b = holding.to(held) + + peace = State(final=True) + truce = war.to(peace) diff --git a/tests/machines/showcase_actions.py b/tests/machines/showcase_actions.py new file mode 100644 index 00000000..5569b838 --- /dev/null +++ b/tests/machines/showcase_actions.py @@ -0,0 +1,16 @@ +from statemachine import State +from statemachine import StateChart + + +class ActionsSC(StateChart): + off = State(initial=True) + on = State() + done = State(final=True) + + power_on = off.to(on) + shutdown = on.to(done) + + def on_exit_off(self): ... + def on_enter_on(self): ... + def on_exit_on(self): ... + def on_enter_done(self): ... diff --git a/tests/machines/showcase_compound.py b/tests/machines/showcase_compound.py new file mode 100644 index 00000000..2125c1e5 --- /dev/null +++ b/tests/machines/showcase_compound.py @@ -0,0 +1,15 @@ +from statemachine import State +from statemachine import StateChart + + +class CompoundSC(StateChart): + class active(State.Compound, name="Active"): + idle = State(initial=True) + working = State() + begin = idle.to(working) + + off = State(initial=True) + done = State(final=True) + + turn_on = off.to(active) + turn_off = active.to(done) diff --git a/tests/machines/showcase_deep_history.py b/tests/machines/showcase_deep_history.py new file mode 100644 index 00000000..1963c590 --- /dev/null +++ b/tests/machines/showcase_deep_history.py @@ -0,0 +1,21 @@ +from statemachine import HistoryState +from statemachine import State +from statemachine import StateChart + + +class DeepHistorySC(StateChart): + class outer(State.Compound, name="Outer"): + class inner(State.Compound, name="Inner"): + a = State(initial=True) + b = State() + go = a.to(b) + + start = State(initial=True) + enter_inner = start.to(inner) + h = HistoryState(type="deep") + + away = State(initial=True) + + dive = away.to(outer) + leave = outer.to(away) + restore = away.to(outer.h) diff --git a/tests/machines/showcase_guards.py b/tests/machines/showcase_guards.py new file mode 100644 index 00000000..8619e986 --- /dev/null +++ b/tests/machines/showcase_guards.py @@ -0,0 +1,16 @@ +from statemachine import State +from statemachine import StateChart + + +class GuardSC(StateChart): + pending = State(initial=True) + approved = State(final=True) + rejected = State(final=True) + + def is_valid(self): + return True + + def is_invalid(self): + return False + + review = pending.to(approved, cond="is_valid") | pending.to(rejected, cond="is_invalid") diff --git a/tests/machines/showcase_history.py b/tests/machines/showcase_history.py new file mode 100644 index 00000000..73c3f404 --- /dev/null +++ b/tests/machines/showcase_history.py @@ -0,0 +1,17 @@ +from statemachine import HistoryState +from statemachine import State +from statemachine import StateChart + + +class HistorySC(StateChart): + class process(State.Compound, name="Process"): + step1 = State(initial=True) + step2 = State() + advance = step1.to(step2) + h = HistoryState() + + paused = State(initial=True) + + pause = process.to(paused) + resume = paused.to(process.h) + begin = paused.to(process) diff --git a/tests/machines/showcase_internal.py b/tests/machines/showcase_internal.py new file mode 100644 index 00000000..530296f2 --- /dev/null +++ b/tests/machines/showcase_internal.py @@ -0,0 +1,12 @@ +from statemachine import State +from statemachine import StateChart + + +class InternalSC(StateChart): + monitoring = State(initial=True) + done = State(final=True) + + def log_status(self): ... + + check = monitoring.to.itself(internal=True, on="log_status") + stop = monitoring.to(done) diff --git a/tests/machines/showcase_parallel.py b/tests/machines/showcase_parallel.py new file mode 100644 index 00000000..7ade93ae --- /dev/null +++ b/tests/machines/showcase_parallel.py @@ -0,0 +1,21 @@ +from statemachine import State +from statemachine import StateChart + + +class ParallelSC(StateChart): + class both(State.Parallel, name="Both"): + class left(State.Compound, name="Left"): + l1 = State(initial=True) + l2 = State(final=True) + go_l = l1.to(l2) + + class right(State.Compound, name="Right"): + r1 = State(initial=True) + r2 = State(final=True) + go_r = r1.to(r2) + + start = State(initial=True) + end = State(final=True) + + enter = start.to(both) + done_state_both = both.to(end) diff --git a/tests/machines/showcase_parallel_compound.py b/tests/machines/showcase_parallel_compound.py new file mode 100644 index 00000000..049def76 --- /dev/null +++ b/tests/machines/showcase_parallel_compound.py @@ -0,0 +1,34 @@ +from statemachine import State +from statemachine import StateChart + + +class ParallelCompoundSC(StateChart): + """Parallel regions with a cross-boundary transition into an inner compound. + + The ``rebuild`` transition targets ``pipeline.build`` — a compound state + inside a parallel region. This is the exact pattern that triggers + `mermaid-js/mermaid#4052 `_; + the Mermaid renderer works around it by redirecting the arrow to the + compound's initial child. + + {statechart:rst} + """ + + class pipeline(State.Parallel, name="Pipeline"): + class build(State.Compound, name="Build"): + compile = State(initial=True) + link = State(final=True) + do_build = compile.to(link) + + class test(State.Compound, name="Test"): + unit = State(initial=True) + e2e = State(final=True) + do_test = unit.to(e2e) + + idle = State(initial=True) + review = State() + + start = idle.to(pipeline) + done_state_pipeline = pipeline.to(review) + rebuild = review.to(pipeline.build) + accept = review.to(idle) diff --git a/tests/machines/showcase_self_transition.py b/tests/machines/showcase_self_transition.py new file mode 100644 index 00000000..59995db9 --- /dev/null +++ b/tests/machines/showcase_self_transition.py @@ -0,0 +1,10 @@ +from statemachine import State +from statemachine import StateChart + + +class SelfTransitionSC(StateChart): + counting = State(initial=True) + done = State(final=True) + + increment = counting.to.itself() + stop = counting.to(done) diff --git a/tests/machines/showcase_simple.py b/tests/machines/showcase_simple.py new file mode 100644 index 00000000..ca99839d --- /dev/null +++ b/tests/machines/showcase_simple.py @@ -0,0 +1,16 @@ +from statemachine import State +from statemachine import StateChart + + +class SimpleSC(StateChart): + """A simple three-state machine. + + {statechart:rst} + """ + + idle = State(initial=True) + running = State() + done = State(final=True) + + start = idle.to(running) + finish = running.to(done) diff --git a/tests/machines/transition_from_any.py b/tests/machines/transition_from_any.py new file mode 100644 index 00000000..3006fc69 --- /dev/null +++ b/tests/machines/transition_from_any.py @@ -0,0 +1,30 @@ +from statemachine import State +from statemachine import StateChart + + +class OrderWorkflow(StateChart): + pending = State(initial=True) + processing = State() + done = State() + completed = State(final=True) + cancelled = State(final=True) + + process = pending.to(processing) + complete = processing.to(done) + finish = done.to(completed) + cancel = cancelled.from_.any() + + +class OrderWorkflowCompound(StateChart): + class active(State.Compound): + pending = State(initial=True) + processing = State() + done = State(final=True) + + process = pending.to(processing) + complete = processing.to(done) + + completed = State(final=True) + cancelled = State(final=True) + done_state_active = active.to(completed) + cancel = active.to(cancelled) diff --git a/tests/machines/tutorial_coffee_order.py b/tests/machines/tutorial_coffee_order.py new file mode 100644 index 00000000..f29ecd07 --- /dev/null +++ b/tests/machines/tutorial_coffee_order.py @@ -0,0 +1,13 @@ +from statemachine import State +from statemachine import StateChart + + +class CoffeeOrder(StateChart): + pending = State(initial=True) + preparing = State() + ready = State() + picked_up = State(final=True) + + start = pending.to(preparing) + finish = preparing.to(ready) + pick_up = ready.to(picked_up) diff --git a/tests/machines/validators/__init__.py b/tests/machines/validators/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/machines/validators/multi_validator.py b/tests/machines/validators/multi_validator.py new file mode 100644 index 00000000..445c693e --- /dev/null +++ b/tests/machines/validators/multi_validator.py @@ -0,0 +1,19 @@ +from statemachine import State +from statemachine import StateChart + + +class MultiValidator(StateChart): + """Machine with multiple validators — first failure stops the chain.""" + + idle = State(initial=True) + active = State(final=True) + + start = idle.to(active, validators=["check_a", "check_b"]) + + def check_a(self, **kwargs): + if not kwargs.get("a_ok"): + raise ValueError("A failed") + + def check_b(self, **kwargs): + if not kwargs.get("b_ok"): + raise ValueError("B failed") diff --git a/tests/machines/validators/order_validation.py b/tests/machines/validators/order_validation.py new file mode 100644 index 00000000..3b8f749e --- /dev/null +++ b/tests/machines/validators/order_validation.py @@ -0,0 +1,17 @@ +from statemachine import State +from statemachine import StateChart + + +class OrderValidation(StateChart): + """StateChart with catch_errors_as_events=True (the default).""" + + pending = State(initial=True) + confirmed = State() + cancelled = State(final=True) + + confirm = pending.to(confirmed, validators="check_stock") + cancel = confirmed.to(cancelled) + + def check_stock(self, quantity=0, **kwargs): + if quantity <= 0: + raise ValueError("Quantity must be positive") diff --git a/tests/machines/validators/order_validation_no_error_events.py b/tests/machines/validators/order_validation_no_error_events.py new file mode 100644 index 00000000..3e55565b --- /dev/null +++ b/tests/machines/validators/order_validation_no_error_events.py @@ -0,0 +1,19 @@ +from statemachine import State +from statemachine import StateChart + + +class OrderValidationNoErrorEvents(StateChart): + """Same machine but with catch_errors_as_events=False.""" + + catch_errors_as_events = False + + pending = State(initial=True) + confirmed = State() + cancelled = State(final=True) + + confirm = pending.to(confirmed, validators="check_stock") + cancel = confirmed.to(cancelled) + + def check_stock(self, quantity=0, **kwargs): + if quantity <= 0: + raise ValueError("Quantity must be positive") diff --git a/tests/machines/validators/validator_fallthrough.py b/tests/machines/validators/validator_fallthrough.py new file mode 100644 index 00000000..8b0a7ce8 --- /dev/null +++ b/tests/machines/validators/validator_fallthrough.py @@ -0,0 +1,20 @@ +from statemachine import State +from statemachine import StateChart + + +class ValidatorFallthrough(StateChart): + """Machine with multiple transitions for the same event. + + When the first transition's validator rejects, the exception propagates + immediately — the engine does NOT fall through to the next transition. + """ + + idle = State(initial=True) + path_a = State(final=True) + path_b = State(final=True) + + go = idle.to(path_a, validators="must_be_premium") | idle.to(path_b) + + def must_be_premium(self, **kwargs): + if not kwargs.get("premium"): + raise PermissionError("Premium required") diff --git a/tests/machines/validators/validator_with_cond.py b/tests/machines/validators/validator_with_cond.py new file mode 100644 index 00000000..d3351054 --- /dev/null +++ b/tests/machines/validators/validator_with_cond.py @@ -0,0 +1,17 @@ +from statemachine import State +from statemachine import StateChart + + +class ValidatorWithCond(StateChart): + """Machine that combines validators and conditions on the same transition.""" + + idle = State(initial=True) + active = State(final=True) + + start = idle.to(active, validators="check_auth", cond="has_permission") + + has_permission = False + + def check_auth(self, token=None, **kwargs): + if token != "valid": + raise PermissionError("Invalid token") diff --git a/tests/machines/validators/validator_with_error_transition.py b/tests/machines/validators/validator_with_error_transition.py new file mode 100644 index 00000000..d069e2f9 --- /dev/null +++ b/tests/machines/validators/validator_with_error_transition.py @@ -0,0 +1,25 @@ +from statemachine import State +from statemachine import StateChart + + +class ValidatorWithErrorTransition(StateChart): + """Machine with both a validator and an error.execution transition. + + The error.execution transition should NOT be triggered by validator + rejection — only by actual execution errors in actions. + """ + + idle = State(initial=True) + active = State() + error_state = State(final=True) + + start = idle.to(active, validators="check_input") + do_work = active.to.itself(on="risky_action") + error_execution = active.to(error_state) + + def check_input(self, value=None, **kwargs): + if value is None: + raise ValueError("Input required") + + def risky_action(self, **kwargs): + raise RuntimeError("Boom") diff --git a/tests/machines/workflow/__init__.py b/tests/machines/workflow/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/machines/workflow/campaign_machine.py b/tests/machines/workflow/campaign_machine.py new file mode 100644 index 00000000..fddbcc1f --- /dev/null +++ b/tests/machines/workflow/campaign_machine.py @@ -0,0 +1,14 @@ +from statemachine import State +from statemachine import StateChart + + +class CampaignMachine(StateChart): + "A workflow machine" + + draft = State(initial=True) + producing = State("Being produced") + closed = State(final=True) + + add_job = draft.to(draft) | producing.to(producing) + produce = draft.to(producing) + deliver = producing.to(closed) diff --git a/tests/machines/workflow/campaign_machine_with_validator.py b/tests/machines/workflow/campaign_machine_with_validator.py new file mode 100644 index 00000000..29f69049 --- /dev/null +++ b/tests/machines/workflow/campaign_machine_with_validator.py @@ -0,0 +1,18 @@ +from statemachine import State +from statemachine import StateChart + + +class CampaignMachineWithValidator(StateChart): + "A workflow machine" + + draft = State(initial=True) + producing = State("Being produced") + closed = State(final=True) + + add_job = draft.to(draft) | producing.to(producing) + produce = draft.to(producing, validators="can_produce") + deliver = producing.to(closed) + + def can_produce(*args, **kwargs): + if "goods" not in kwargs: + raise LookupError("Goods not found.") diff --git a/tests/machines/workflow/campaign_machine_with_values.py b/tests/machines/workflow/campaign_machine_with_values.py new file mode 100644 index 00000000..6dc6e75a --- /dev/null +++ b/tests/machines/workflow/campaign_machine_with_values.py @@ -0,0 +1,14 @@ +from statemachine import State +from statemachine import StateChart + + +class CampaignMachineWithValues(StateChart): + "A workflow machine" + + draft = State(initial=True, value=1) + producing = State("Being produced", value=2) + closed = State(value=3, final=True) + + add_job = draft.to(draft) | producing.to(producing) + produce = draft.to(producing) + deliver = producing.to(closed) diff --git a/tests/machines/workflow/reverse_traffic_light.py b/tests/machines/workflow/reverse_traffic_light.py new file mode 100644 index 00000000..c41c8681 --- /dev/null +++ b/tests/machines/workflow/reverse_traffic_light.py @@ -0,0 +1,13 @@ +from statemachine import State +from statemachine import StateChart + + +class ReverseTrafficLightMachine(StateChart): + "A traffic light machine" + + green = State(initial=True) + yellow = State() + red = State() + + stop = red.from_(yellow, green, red) + cycle = green.from_(red) | yellow.from_(green) | red.from_(yellow) | red.from_.itself() diff --git a/tests/test_api_contract.py b/tests/test_api_contract.py new file mode 100644 index 00000000..0ac7d2d6 --- /dev/null +++ b/tests/test_api_contract.py @@ -0,0 +1,280 @@ +"""Contract tests: observable behavior of public Configuration APIs. + +Documents the exact values returned by each public API across all supported +topologies (flat, compound, parallel, complex parallel) and lifecycle phases +(initial state, after transitions, final state). + +APIs under test (StateChart): + sm.current_state_value -- raw value stored on the model + sm.configuration_values -- OrderedSet of raw values + sm.configuration -- OrderedSet[State] + sm.current_state -- State or OrderedSet[State] (deprecated) + +API under test (Model): + model.state -- raw attribute on the model object +""" + +import warnings +from typing import Any + +import pytest +from statemachine.orderedset import OrderedSet + +from statemachine import State +from statemachine import StateChart + +# --------------------------------------------------------------------------- +# Model +# --------------------------------------------------------------------------- + + +class Model: + """Explicit model to verify raw state persistence independently.""" + + def __init__(self): + self.state: Any = None + + +# --------------------------------------------------------------------------- +# Topologies +# --------------------------------------------------------------------------- + + +class FlatSC(StateChart): + s1 = State(initial=True) + s2 = State() + s3 = State(final=True) + + go = s1.to(s2) + finish = s2.to(s3) + + +class CompoundSC(StateChart): + class parent(State.Compound): + child1 = State(initial=True) + child2 = State() + move = child1.to(child2) + + done = State(final=True) + leave = parent.to(done) + + +class ParallelSC(StateChart): + class regions(State.Parallel): + class region_a(State.Compound): + a1 = State(initial=True) + a2 = State() + go_a = a1.to(a2) + + class region_b(State.Compound): + b1 = State(initial=True) + b2 = State() + go_b = b1.to(b2) + + +class ComplexParallelSC(StateChart): + class top(State.Parallel): + class left(State.Compound): + class nested(State.Compound): + l1 = State(initial=True) + l2 = State() + move_l = l1.to(l2) + + left_done = State(final=True) + finish_left = nested.to(left_done) + + class right(State.Compound): + r1 = State(initial=True) + r2 = State() + move_r = r1.to(r2) + + +# --------------------------------------------------------------------------- +# Assertion helper +# --------------------------------------------------------------------------- + + +def assert_contract(sm, model, expected_ids: set): + """Assert the full observable API contract. + + When exactly one state is active, the model stores a scalar and + ``current_state`` returns a single ``State``. When multiple states + are active (compound/parallel), the model stores an ``OrderedSet`` + and ``current_state`` returns ``OrderedSet[State]``. + """ + scalar = len(expected_ids) == 1 + + # model.state and current_state_value point to the same object + assert model.state is sm.current_state_value + + if scalar: + val = next(iter(expected_ids)) + assert model.state == val + assert not isinstance(model.state, OrderedSet) + else: + assert isinstance(model.state, OrderedSet) + assert set(model.state) == expected_ids + + # configuration_values -- always OrderedSet of raw values + assert isinstance(sm.configuration_values, OrderedSet) + assert set(sm.configuration_values) == expected_ids + + # configuration -- always OrderedSet[State] + assert len(sm.configuration) == len(expected_ids) + assert {s.id for s in sm.configuration} == expected_ids + + # current_state (deprecated) -- unwrapped when single + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + cs = sm.current_state + if scalar: + assert not isinstance(cs, OrderedSet) + assert cs.id == next(iter(expected_ids)) + else: + assert isinstance(cs, OrderedSet) + assert {s.id for s in cs} == expected_ids + + +# --------------------------------------------------------------------------- +# Main contract matrix: topology x lifecycle x engine +# --------------------------------------------------------------------------- + + +SCENARIOS = [ + # -- Flat -- + pytest.param(FlatSC, [], {"s1"}, id="flat-initial"), + pytest.param(FlatSC, ["go"], {"s2"}, id="flat-after-go"), + pytest.param(FlatSC, ["go", "finish"], {"s3"}, id="flat-final"), + # -- Compound -- + pytest.param(CompoundSC, [], {"parent", "child1"}, id="compound-initial"), + pytest.param(CompoundSC, ["move"], {"parent", "child2"}, id="compound-inner-move"), + pytest.param(CompoundSC, ["leave"], {"done"}, id="compound-exit"), + # -- Parallel -- + pytest.param( + ParallelSC, + [], + {"regions", "region_a", "a1", "region_b", "b1"}, + id="parallel-initial", + ), + pytest.param( + ParallelSC, + ["go_a"], + {"regions", "region_a", "a2", "region_b", "b1"}, + id="parallel-one-region", + ), + pytest.param( + ParallelSC, + ["go_a", "go_b"], + {"regions", "region_a", "a2", "region_b", "b2"}, + id="parallel-both-regions", + ), + # -- Complex parallel -- + pytest.param( + ComplexParallelSC, + [], + {"top", "left", "nested", "l1", "right", "r1"}, + id="complex-initial", + ), + pytest.param( + ComplexParallelSC, + ["move_l"], + {"top", "left", "nested", "l2", "right", "r1"}, + id="complex-nested-move", + ), + pytest.param( + ComplexParallelSC, + ["move_r"], + {"top", "left", "nested", "l1", "right", "r2"}, + id="complex-other-region", + ), + pytest.param( + ComplexParallelSC, + ["move_l", "move_r"], + {"top", "left", "nested", "l2", "right", "r2"}, + id="complex-both-regions", + ), + pytest.param( + ComplexParallelSC, + ["finish_left"], + {"top", "left", "left_done", "right", "r1"}, + id="complex-exit-nested", + ), +] + + +@pytest.mark.parametrize(("sc_class", "events", "expected_ids"), SCENARIOS) +async def test_configuration_contract(sm_runner, sc_class, events, expected_ids): + model = Model() + sm = await sm_runner.start(sc_class, model=model) + for event in events: + await sm_runner.send(sm, event) + assert_contract(sm, model, expected_ids) + + +# --------------------------------------------------------------------------- +# Model setter contract +# --------------------------------------------------------------------------- + +SETTER_SCENARIOS = [ + pytest.param(FlatSC, "s2", {"s2"}, id="scalar-on-flat"), + pytest.param( + CompoundSC, + OrderedSet(["parent", "child2"]), + {"parent", "child2"}, + id="orderedset-on-compound", + ), + pytest.param(CompoundSC, "done", {"done"}, id="scalar-collapses-orderedset"), +] + + +@pytest.mark.parametrize(("sc_class", "new_value", "expected_ids"), SETTER_SCENARIOS) +async def test_setter_contract(sm_runner, sc_class, new_value, expected_ids): + model = Model() + sm = await sm_runner.start(sc_class, model=model) + sm.current_state_value = new_value + assert_contract(sm, model, expected_ids) + + +async def test_set_none_clears_configuration(sm_runner): + model = Model() + sm = await sm_runner.start(FlatSC, model=model) + + sm.current_state_value = None + + assert model.state is None + assert sm.current_state_value is None + assert sm.configuration_values == OrderedSet() + assert sm.configuration == OrderedSet() + + +# --------------------------------------------------------------------------- +# Uninitialized state (async-only: sync enters initial state in __init__) +# --------------------------------------------------------------------------- + +UNINITIALIZED_SCENARIOS = [ + pytest.param(FlatSC, {"s1"}, id="flat"), + pytest.param(CompoundSC, {"parent", "child1"}, id="compound"), + pytest.param( + ParallelSC, + {"regions", "region_a", "a1", "region_b", "b1"}, + id="parallel", + ), +] + + +@pytest.mark.parametrize(("sc_class", "expected_ids"), UNINITIALIZED_SCENARIOS) +async def test_uninitialized_then_activated(sc_class, expected_ids): + from tests.conftest import _AsyncListener + + model = Model() + sm = sc_class(model=model, listeners=[_AsyncListener()]) + + # Before activation: all APIs reflect empty configuration + assert model.state is None + assert sm.current_state_value is None + assert sm.configuration_values == OrderedSet() + assert sm.configuration == OrderedSet() + + # After activation: full contract holds + await sm.activate_initial_state() + assert_contract(sm, model, expected_ids) diff --git a/tests/test_callbacks.py b/tests/test_callbacks.py index 048c3477..e593c75b 100644 --- a/tests/test_callbacks.py +++ b/tests/test_callbacks.py @@ -320,7 +320,7 @@ def on_start(self): def test_issue_417_cannot_start(self, model_class, sm_class, mock_calls): model = model_class(0) sm = sm_class(model, 0) - with pytest.raises(sm.TransitionNotAllowed, match="Can't start when in Created"): + with pytest.raises(sm.TransitionNotAllowed, match="Can't Start when in Created"): sm.start() mock_calls.assert_not_called() diff --git a/tests/test_configuration.py b/tests/test_configuration.py new file mode 100644 index 00000000..07f13ea6 --- /dev/null +++ b/tests/test_configuration.py @@ -0,0 +1,217 @@ +"""Tests for the Configuration class internals. + +These tests cover branches in statemachine/configuration.py that are not +exercised by the higher-level state machine tests. +""" + +import warnings + +from statemachine.orderedset import OrderedSet + +from statemachine import State +from statemachine import StateChart + + +class ParallelSM(StateChart): + """A parallel state chart for testing multi-element configuration.""" + + s1 = State(initial=True) + s2 = State() + s3 = State(final=True) + + go = s1.to(s2) + finish = s2.to(s3) + + +class TestConfigurationStatesSetter: + def test_set_empty_configuration(self): + sm = ParallelSM() + assert len(sm.configuration) > 0 + + sm.configuration = OrderedSet() + assert sm.current_state_value is None + + def test_set_multi_element_configuration(self): + sm = ParallelSM() + s1_inst = sm.s1 + s2_inst = sm.s2 + + sm.configuration = OrderedSet([s1_inst, s2_inst]) + assert isinstance(sm.current_state_value, OrderedSet) + assert sm.current_state_value == OrderedSet([ParallelSM.s1.value, ParallelSM.s2.value]) + + +class TestConfigurationValueSetter: + def test_set_value_none_writes_none_to_model(self): + sm = ParallelSM() + assert sm.current_state_value is not None + + sm.current_state_value = None + assert sm.current_state_value is None + assert sm.configuration_values == OrderedSet() + + def test_set_value_plain_set_coerces_to_ordered_set(self): + sm = ParallelSM() + s1_val = ParallelSM.s1.value + s2_val = ParallelSM.s2.value + + # Assign a plain set (MutableSet but not OrderedSet) + sm.current_state_value = {s1_val, s2_val} + # Model should store an OrderedSet (denormalized back to it) + assert isinstance(sm.current_state_value, OrderedSet) + assert sm.current_state_value == OrderedSet([s1_val, s2_val]) + + +class TestReadFromModelNonOrderedSet: + def test_read_from_model_coerces_plain_set(self): + """When the model stores a plain set, _read_from_model coerces it.""" + sm = ParallelSM() + s1_val = ParallelSM.s1.value + s2_val = ParallelSM.s2.value + + # Bypass the value setter to place a plain set on the model + setattr(sm._config._model, sm._config._state_field, {s1_val, s2_val}) + + values = sm._config._read_from_model() + assert isinstance(values, OrderedSet) + assert values == OrderedSet([s1_val, s2_val]) + + +class TestConfigurationDiscard: + def test_discard_nonmatching_scalar(self): + sm = ParallelSM() + # current value is s1 (scalar) + assert sm.current_state_value == ParallelSM.s1.value + + # discard s2 — should be a no-op since s2 is not active + sm._config.discard(ParallelSM.s2) + assert sm.current_state_value == ParallelSM.s1.value + + +class TestConfigurationCurrentState: + def test_current_state_with_multiple_active_states(self): + sm = ParallelSM() + s1_inst = sm.s1 + s2_inst = sm.s2 + sm.configuration = OrderedSet([s1_inst, s2_inst]) + + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + result = sm.current_state + assert isinstance(result, OrderedSet) + assert len(result) == 2 + + +# --------------------------------------------------------------------------- +# Regression tests: add()/discard() must go through the property setter +# so that models with deserializing properties persist the updated value. +# --------------------------------------------------------------------------- + + +class SerializingModel: + """A model that serializes/deserializes state on every access, + simulating a DB-backed property (e.g., Django model field). + """ + + def __init__(self): + self._raw: str | None = None + + @property + def state(self): + if self._raw is None: + return None + parts = self._raw.split(",") + if len(parts) == 1: + return parts[0] + return OrderedSet(parts) + + @state.setter + def state(self, value): + if value is None: + self._raw = None + elif isinstance(value, OrderedSet): + self._raw = ",".join(str(v) for v in value) + else: + self._raw = str(value) + + +class WarSC(StateChart): + """Parallel state chart with two regions for testing.""" + + class war(State.Parallel): + class region_a(State.Compound): + a1 = State(initial=True) + a2 = State() + move_a = a1.to(a2) + + class region_b(State.Compound): + b1 = State(initial=True) + b2 = State() + move_b = b1.to(b2) + + +class TestAddDiscard: + """Verify add()/discard() always write back through model setter.""" + + def test_add_calls_setter_on_serializing_model(self): + model = SerializingModel() + sm = WarSC(model=model) + + # After initial entry, all parallel states should be active + config_values = sm.configuration_values + assert len(config_values) == 5 # war, region_a, a1, region_b, b1 + + def test_discard_calls_setter_on_serializing_model(self): + model = SerializingModel() + sm = WarSC(model=model) + + initial_count = len(sm.configuration_values) + assert initial_count == 5 + + # Trigger a transition in region_a: a1 -> a2 + sm.send("move_a") + config_values = sm.configuration_values + # a1 should be replaced by a2; still 5 states + assert len(config_values) == 5 + assert "a2" in config_values + assert "a1" not in config_values + + def test_parallel_lifecycle_with_serializing_model(self): + model = SerializingModel() + sm = WarSC(model=model) + + # Move both regions + sm.send("move_a") + sm.send("move_b") + + config_values = sm.configuration_values + assert len(config_values) == 5 + assert "a2" in config_values + assert "b2" in config_values + assert "a1" not in config_values + assert "b1" not in config_values + + def test_state_restoration_from_serialized_model(self): + model = SerializingModel() + sm = WarSC(model=model) + sm.send("move_a") + + # Save the raw state + raw_state = model._raw + + # Create a new model with the same raw state and a new SM + model2 = SerializingModel() + model2._raw = raw_state + sm2 = WarSC(model=model2) + + assert sm2.configuration_values == sm.configuration_values + + async def test_parallel_with_serializing_model_both_engines(self, sm_runner): + model = SerializingModel() + sm = await sm_runner.start(WarSC, model=model) + + assert len(sm.configuration_values) == 5 + + await sm_runner.send(sm, "move_a") + assert "a2" in sm.configuration_values + assert len(sm.configuration_values) == 5 diff --git a/tests/test_contrib_diagram.py b/tests/test_contrib_diagram.py index 54c94cbd..cb03dc9a 100644 --- a/tests/test_contrib_diagram.py +++ b/tests/test_contrib_diagram.py @@ -1,18 +1,68 @@ +import re from contextlib import contextmanager from unittest import mock +from xml.etree import ElementTree import pytest +from docutils import nodes from statemachine.contrib.diagram import DotGraphMachine from statemachine.contrib.diagram import main from statemachine.contrib.diagram import quickchart_write_svg -from statemachine.transition import Transition +from statemachine.contrib.diagram.extract import _format_event_names +from statemachine.contrib.diagram.model import ActionType +from statemachine.contrib.diagram.model import StateType +from statemachine.contrib.diagram.renderers.dot import DotRenderer +from statemachine.event import Event -from statemachine import HistoryState from statemachine import State from statemachine import StateChart pytestmark = pytest.mark.usefixtures("requires_dot_installed") +SVG_NS = {"svg": "http://www.w3.org/2000/svg"} + + +def _parse_svg(graph): + """Generate SVG from a pydot graph and parse it as XML.""" + svg_bytes = graph.create_svg() + return ElementTree.fromstring(svg_bytes) + + +def _find_state_node(svg_root, state_id): + """Find the SVG element for a state node by its title text.""" + for g in svg_root.iter("{http://www.w3.org/2000/svg}g"): + if g.get("class") != "node": + continue + title = g.find("{http://www.w3.org/2000/svg}title") + if title is not None and title.text == state_id: + return g + return None + + +def _has_rectangular_fill(node_g): + """Check if a node group has a with a colored fill. + + A fill inside a state node means the background is rectangular + (no rounded corners), which is a visual regression — state backgrounds + should use with curves to match the rounded border. + + Ignores white fills and arrow-related polygons (which are in edge groups). + """ + for polygon in node_g.findall("{http://www.w3.org/2000/svg}polygon"): + fill = polygon.get("fill", "none") + if fill not in ("none", "white", "black", "#ffffff"): + return True + return False + + +def _path_has_curves(d_attr): + """Check if an SVG path `d` attribute contains curve commands (C, c, Q, q, A, a). + + Rounded corners are drawn with cubic Bezier curves (C command). + A rectangular shape only has M (move) and L (line) commands. + """ + return bool(re.search(r"[CcQqAa]", d_attr)) + @pytest.fixture( params=[ @@ -88,6 +138,24 @@ def test_generate_complain_about_bad_sm_path(self, capsys, tmp_path): ] ) + def test_generate_image_with_events(self, tmp_path): + """CLI --events instantiates the machine and sends events before rendering.""" + out = tmp_path / "sm.png" + + main( + [ + "tests.examples.traffic_light_machine.TrafficLightMachine", + str(out), + "--events", + "cycle", + "cycle", + "cycle", + ] + ) + + assert out.exists() + assert out.stat().st_size > 0 + def test_generate_complain_about_module_without_sm(self, tmp_path): out = tmp_path / "sm.svg" @@ -95,6 +163,109 @@ def test_generate_complain_about_module_without_sm(self, tmp_path): with pytest.raises(ValueError, match=expected_error): main(["tests.examples", str(out)]) + def test_format_mermaid(self, tmp_path): + out = tmp_path / "sm.mmd" + + main( + [ + "tests.examples.traffic_light_machine.TrafficLightMachine", + str(out), + "--format", + "mermaid", + ] + ) + + content = out.read_text() + assert "stateDiagram-v2" in content + assert "green --> yellow : Cycle" in content + + def test_format_md(self, tmp_path): + out = tmp_path / "sm.md" + + main( + [ + "tests.examples.traffic_light_machine.TrafficLightMachine", + str(out), + "--format", + "md", + ] + ) + + content = out.read_text() + assert "| State" in content + assert "Cycle" in content + + def test_format_rst(self, tmp_path): + out = tmp_path / "sm.rst" + + main( + [ + "tests.examples.traffic_light_machine.TrafficLightMachine", + str(out), + "--format", + "rst", + ] + ) + + content = out.read_text() + assert "+---" in content + assert "Cycle" in content + + def test_format_mermaid_stdout(self, capsys): + main( + [ + "tests.examples.traffic_light_machine.TrafficLightMachine", + "-", + "--format", + "mermaid", + ] + ) + + captured = capsys.readouterr() + assert "stateDiagram-v2" in captured.out + + def test_format_md_stdout(self, capsys): + main( + [ + "tests.examples.traffic_light_machine.TrafficLightMachine", + "-", + "--format", + "md", + ] + ) + + captured = capsys.readouterr() + assert "| State" in captured.out + + def test_stdout_default_svg(self, capsys): + """Default format to stdout writes SVG bytes.""" + main( + [ + "tests.examples.traffic_light_machine.TrafficLightMachine", + "-", + ] + ) + + captured = capsys.readouterr() + assert " child1" in dot + # Verify the initial edge exists (from the black-dot initial node to child1) + # The implicit initial transition from the compound state itself is NOT rendered + # as an edge — it is represented only by the black-dot initial node inside the cluster. + assert "parent_anchor -> child1" not in dot + assert "-> child1" in dot def test_history_state_shallow_diagram(): """DOT output contains an 'H' circle node for shallow history state.""" - h = HistoryState(name="H") - h._set_id("h_shallow") + from statemachine.contrib.diagram.model import DiagramState - graph_maker = DotGraphMachine.__new__(DotGraphMachine) - graph_maker.font_name = "Arial" - node = graph_maker._history_node(h) + state = DiagramState(id="h_shallow", name="H", type=StateType.HISTORY_SHALLOW) + renderer = DotRenderer() + node = renderer._create_history_node(state) attrs = node.obj_dict["attributes"] assert attrs["label"] in ("H", '"H"') assert attrs["shape"] == "circle" @@ -250,13 +429,11 @@ def test_history_state_shallow_diagram(): def test_history_state_deep_diagram(): """DOT output contains an 'H*' circle node for deep history state.""" - h = HistoryState(name="H*", type="deep") - h._set_id("h_deep") + from statemachine.contrib.diagram.model import DiagramState - graph_maker = DotGraphMachine.__new__(DotGraphMachine) - graph_maker.font_name = "Arial" - node = graph_maker._history_node(h) - # Verify the node renders correctly in DOT output + state = DiagramState(id="h_deep", name="H*", type=StateType.HISTORY_DEEP) + renderer = DotRenderer() + node = renderer._create_history_node(state) dot_str = node.to_string() assert "H*" in dot_str assert "circle" in dot_str @@ -264,25 +441,12 @@ def test_history_state_deep_diagram(): def test_history_state_default_transition(): """History state's default transition appears as an edge in the diagram.""" - child1 = State("child1", initial=True) - child1._set_id("child1") - child2 = State("child2") - child2._set_id("child2") - - h = HistoryState(name="H") - h._set_id("hist") - # Add a default transition from history to child1 - t = Transition(source=h, target=child1, initial=True) - h.transitions.add_transitions(t) + from statemachine.contrib.diagram.model import DiagramTransition - parent = State("parent", states=[child1, child2], history=[h]) - parent._set_id("parent") - - graph_maker = DotGraphMachine.__new__(DotGraphMachine) - graph_maker.font_name = "Arial" - graph_maker.transition_font_size = "9pt" - - edges = graph_maker._transition_as_edges(t) + transition = DiagramTransition(source="hist", targets=["child1"], event="") + renderer = DotRenderer() + renderer._compound_ids = set() + edges = renderer._create_edges(transition) assert len(edges) == 1 edge = edges[0] assert edge.obj_dict["points"] == ("hist", "child1") @@ -320,26 +484,17 @@ def test_history_state_in_graph_states(): def test_multi_target_transition_diagram(): """Edges are created for all targets of a multi-target transition.""" - source = State("source", initial=True) - source._set_id("source") - target1 = State("target1") - target1._set_id("target1") - target2 = State("target2") - target2._set_id("target2") - - t = Transition(source=source, target=[target1, target2]) - t._events.add("go") - - graph_maker = DotGraphMachine.__new__(DotGraphMachine) - graph_maker.font_name = "Arial" - graph_maker.transition_font_size = "9pt" + from statemachine.contrib.diagram.model import DiagramTransition - edges = graph_maker._transition_as_edges(t) + transition = DiagramTransition(source="source", targets=["target1", "target2"], event="go") + renderer = DotRenderer() + renderer._compound_ids = set() + edges = renderer._create_edges(transition) assert len(edges) == 2 assert edges[0].obj_dict["points"] == ("source", "target1") assert edges[1].obj_dict["points"] == ("source", "target2") # Only the first edge gets a label - assert edges[0].obj_dict["attributes"]["label"] == "go" + assert "go" in edges[0].obj_dict["attributes"]["label"] assert edges[1].obj_dict["attributes"]["label"] == "" @@ -373,5 +528,1275 @@ class region2(State.Compound, name="Region2"): assert "cluster_region2" in dot # Parallel indicator assert "☷" in dot - # Verify initial edges exist for compound states (top and regions) - assert "top_anchor -> entry" in dot + # Implicit initial transitions from compound states are NOT rendered as edges — + # they are represented by the black-dot initial node inside each cluster. + assert "top_anchor -> entry" not in dot + assert "-> entry" in dot + + +class TestSVGShapeConsistency: + """Verify that active and inactive states render with the same shape in SVG. + + These tests parse the generated SVG to catch visual regressions that are + hard to spot by inspecting DOT source alone. For example, using `bgcolor` + on a `` instead of a `
` causes Graphviz to render a rectangular + `` behind a rounded `` border — the DOT looks fine but the + visual result is broken. + """ + + def test_active_state_has_no_rectangular_fill(self): + """Active state background must use rounded , not rectangular .""" + from tests.examples.traffic_light_machine import TrafficLightMachine + + sm = TrafficLightMachine() # starts in Green + graph = DotGraphMachine(sm).get_graph() + svg = _parse_svg(graph) + + green_node = _find_state_node(svg, "green") + assert green_node is not None, "Could not find 'green' node in SVG" + assert not _has_rectangular_fill(green_node), ( + "Active state 'green' has a rectangular fill — " + "expected a rounded fill to match the border shape" + ) + + def test_active_and_inactive_states_use_same_svg_element_type(self): + """Active and inactive states must both render as rounded elements. + + With ``shape=rectangle`` + ``style="rounded, filled"``, Graphviz renders + each state as a single ```` with cubic Bezier curves (``C`` commands) + for rounded corners. Both the fill and stroke are in the same ````. + + A regression would be if the active state rendered differently — e.g., a + rectangular ```` for the fill behind a rounded ```` border. + """ + from tests.examples.traffic_light_machine import TrafficLightMachine + + sm = TrafficLightMachine() + graph = DotGraphMachine(sm).get_graph() + svg = _parse_svg(graph) + + for state_id in ("green", "yellow", "red"): + node = _find_state_node(svg, state_id) + assert node is not None, f"Could not find '{state_id}' node in SVG" + + # Each state should have at least one with rounded curves + paths = node.findall("{http://www.w3.org/2000/svg}path") + assert len(paths) >= 1, ( + f"State '{state_id}' should have at least 1 , found {len(paths)}" + ) + for p in paths: + assert _path_has_curves(p.get("d", "")), ( + f"State '{state_id}' has a without curves — not rounded" + ) + + def test_no_state_node_has_rectangular_colored_fill(self): + """No state in the diagram should have a rectangular colored fill.""" + + class SM(StateChart): + s1 = State(initial=True) + s2 = State() + s3 = State(final=True) + go = s1.to(s2) + finish = s2.to(s3) + + sm = SM() + sm.go() # move to s2 + graph = DotGraphMachine(sm).get_graph() + svg = _parse_svg(graph) + + for state_id in ("s1", "s2", "s3"): + node = _find_state_node(svg, state_id) + if node is None: + continue + assert not _has_rectangular_fill(node), ( + f"State '{state_id}' has a rectangular colored fill" + ) + + +class TestExtract: + """Tests for extract.py edge cases.""" + + def test_deep_history_state_type(self): + """Deep history state is correctly typed in the extracted graph.""" + from statemachine.contrib.diagram.extract import extract + + from tests.machines.showcase_deep_history import DeepHistorySC + + graph = extract(DeepHistorySC) + # Find the history state in the outer compound's children + outer = next(s for s in graph.states if s.id == "outer") + h_state = next(s for s in outer.children if s.type == StateType.HISTORY_DEEP) + assert h_state is not None + + def test_internal_transition_actions_extracted(self): + """Internal transitions with actions are extracted into state actions.""" + from statemachine.contrib.diagram.extract import extract + + from tests.machines.showcase_internal import InternalSC + + graph = extract(InternalSC) + monitoring = next(s for s in graph.states if s.id == "monitoring") + internal_actions = [a for a in monitoring.actions if a.type == ActionType.INTERNAL] + assert len(internal_actions) >= 1 + assert any("check" in a.body for a in internal_actions) + + def test_internal_transition_skipped_in_bidirectional(self): + """Internal transitions are skipped in _collect_bidirectional_compound_ids.""" + from statemachine.contrib.diagram.extract import extract + + class SM(StateChart): + class parent(State.Compound, name="Parent"): + child1 = State(initial=True) + child2 = State(final=True) + + def log(self): ... + + check = child1.to.itself(internal=True, on="log") + go = child1.to(child2) + + start = State(initial=True) + end = State(final=True) + + enter = start.to(parent) + finish = parent.to(end) + + graph = extract(SM) + # parent has both incoming and outgoing, so it should be bidirectional + assert "parent" in graph.bidirectional_compound_ids + + def test_internal_transition_without_action(self): + """Internal transition without on action has no internal action in diagram.""" + from statemachine.contrib.diagram.extract import extract + + class SM(StateChart): + s1 = State(initial=True) + s2 = State(final=True) + + noop = s1.to.itself(internal=True) + go = s1.to(s2) + + graph = extract(SM) + s1 = next(s for s in graph.states if s.id == "s1") + internal_actions = [a for a in s1.actions if a.type == ActionType.INTERNAL] + assert internal_actions == [] + + def test_extract_invalid_type_raises(self): + """extract() raises TypeError for invalid input.""" + from statemachine.contrib.diagram.extract import extract + + with pytest.raises(TypeError, match="Expected a StateChart"): + extract("not a machine") # type: ignore[arg-type] + + def test_resolve_initial_fallback(self): + """When no explicit initial, first candidate gets is_initial=True.""" + from statemachine.contrib.diagram.extract import _resolve_initial_states + from statemachine.contrib.diagram.model import DiagramState + + states = [ + DiagramState(id="a", name="A", type=StateType.REGULAR), + DiagramState(id="b", name="B", type=StateType.REGULAR), + ] + _resolve_initial_states(states) + assert states[0].is_initial is True + + +class TestFormatEventNames: + """Tests for _format_event_names — alias filtering for diagram display.""" + + def test_simple_event_uses_name(self): + """A plain event displays its human-readable name.""" + + class SM(StateChart): + s1 = State(initial=True) + s2 = State(final=True) + go = s1.to(s2) + + t = SM.s1.transitions[0] + assert _format_event_names(t) == "Go" + + def test_done_state_alias_filtered(self): + """done_state_X registers both underscore and dot forms; only underscore is shown.""" + + class SM(StateChart): + class parent(State.Compound): + child = State(initial=True) + done = State(final=True) + finish = child.to(done) + + end = State(final=True) + done_state_parent = parent.to(end) + + t = next(t for t in SM.parent.transitions if t.event and "done_state" in t.event) + result = _format_event_names(t) + assert result == "Done state parent" + assert "done.state" not in result + + def test_done_invoke_alias_filtered(self): + """done_invoke_X alias filtering works the same as done_state_X.""" + + class SM(StateChart): + s1 = State(initial=True) + s2 = State(final=True) + done_invoke_child = s1.to(s2) + + t = SM.s1.transitions[0] + result = _format_event_names(t) + assert result == "Done invoke child" + assert "done.invoke" not in result + + def test_error_alias_filtered(self): + """error_X registers both error_X and error.X; only underscore is shown.""" + + class SM(StateChart): + s1 = State(initial=True) + s2 = State(final=True) + error_execution = s1.to(s2) + + t = SM.s1.transitions[0] + result = _format_event_names(t) + assert result == "Error execution" + assert "error.execution" not in result + + def test_multiple_distinct_events_preserved(self): + """Multiple distinct events on one transition are all preserved.""" + + class SM(StateChart): + s1 = State(initial=True) + s2 = State(final=True) + go = s1.to(s2) + also = s1.to(s2) + + # Add a second event to the first transition + t = SM.s1.transitions[0] + t.add_event("also") + result = _format_event_names(t) + assert "Go" in result + assert "Also" in result + + def test_eventless_transition_returns_empty(self): + """A transition with no events returns an empty string.""" + + class SM(StateChart): + s1 = State(initial=True) + s2 = State(final=True) + s1.to(s2, cond="always_true") + + def always_true(self): + return True + + # Find the eventless transition + t = next(t for t in SM.s1.transitions if not list(t.events)) + assert _format_event_names(t) == "" + + def test_dot_only_event_preserved(self): + """An event whose ID contains dots but has no underscore alias is preserved.""" + + class SM(StateChart): + s1 = State(initial=True) + s2 = State(final=True) + go = s1.to(s2) + + from statemachine.transition import Transition + + t = Transition(source=SM.s1, target=SM.s2, event="custom.event") + assert _format_event_names(t) == "Custom event" + + def test_explicit_event_name_displayed(self): + """An Event with an explicit name= shows the human-readable name.""" + + class SM(StateChart): + active = State(initial=True) + suspended = State(final=True) + + suspend = Event( + active.to(suspended), + name="Human Suspend", + ) + + t = SM.active.transitions[0] + assert _format_event_names(t) == "Human Suspend" + + +class TestDotRendererEdgeCases: + """Tests for dot.py edge cases.""" + + def test_compound_state_with_actions_label(self): + """Compound state with entry/exit actions renders action rows in label.""" + + class SM(StateChart): + class parent(State.Compound, name="Parent"): + child = State(initial=True) + + def on_enter_parent(self): ... + + start = State(initial=True) + enter = start.to(parent) + + dot = DotGraphMachine(SM)().to_string() + # The compound label should contain the entry action + assert "entry" in dot.lower() or "on_enter_parent" in dot + + def test_internal_action_format(self): + """Internal action uses body directly (no 'entry /' prefix).""" + renderer = DotRenderer() + from statemachine.contrib.diagram.model import DiagramAction + + action = DiagramAction(type=ActionType.INTERNAL, body="check / log_status") + result = renderer._format_action(action) + assert result == "check / log_status" + + def test_targetless_transition_self_loop(self): + """Transition with no target falls back to source as destination.""" + from statemachine.contrib.diagram.model import DiagramTransition + + transition = DiagramTransition(source="s1", targets=[], event="tick") + renderer = DotRenderer() + renderer._compound_ids = set() + edges = renderer._create_edges(transition) + assert len(edges) == 1 + # With no targets, target_ids becomes [None], and dst becomes source + assert edges[0].obj_dict["points"][1] == "s1" + + def test_compound_edge_anchor_non_bidirectional(self): + """Non-bidirectional compound state uses generic _anchor node.""" + renderer = DotRenderer() + renderer._compound_bidir_ids = {"other"} + result = renderer._compound_edge_anchor("my_state", "out") + assert result == "my_state_anchor" + + +class TestDiagramMainModule: + """Tests for __main__.py.""" + + def test_main_module_execution(self, tmp_path): + """python -m statemachine.contrib.diagram works.""" + import runpy + + out = tmp_path / "sm.svg" + with mock.patch( + "sys.argv", + [ + "statemachine.contrib.diagram", + "tests.examples.traffic_light_machine.TrafficLightMachine", + str(out), + ], + ): + with pytest.raises(SystemExit) as exc_info: + runpy.run_module( + "statemachine.contrib.diagram", run_name="__main__", alter_sys=True + ) + assert exc_info.value.code is None + assert out.exists() + + +class TestSphinxDirective: + """Unit tests for the statemachine-diagram Sphinx directive.""" + + def test_parse_events(self): + from statemachine.contrib.diagram.sphinx_ext import _parse_events + + assert _parse_events("start, ship") == ["start", "ship"] + assert _parse_events("single") == ["single"] + assert _parse_events(" a , b , c ") == ["a", "b", "c"] + assert _parse_events("") == [] + + def test_import_and_render_class(self, tmp_path): + """Directive logic: import a class and generate SVG.""" + from statemachine.contrib.diagram import DotGraphMachine + from statemachine.contrib.diagram import import_sm + + sm_class = import_sm("tests.examples.order_control_machine.OrderControl") + graph = DotGraphMachine(sm_class).get_graph() + svg_bytes = graph.create_svg() + assert svg_bytes.startswith(b"\n\n' + '' + "" + ) + directive = self._make_directive() + svg_tag, _, _ = directive._prepare_svg(svg_text) + + assert not svg_tag.startswith("" in svg_tag + + def test_extracts_intrinsic_dimensions(self): + svg_text = '' + directive = self._make_directive() + _, w, h = directive._prepare_svg(svg_text) + + assert w == "702pt" + assert h == "170pt" + + def test_removes_fixed_dimensions(self): + svg_text = '' + directive = self._make_directive() + svg_tag, _, _ = directive._prepare_svg(svg_text) + + assert 'width="702pt"' not in svg_tag + assert 'height="170pt"' not in svg_tag + assert "viewBox" in svg_tag + + def test_handles_no_dimensions(self): + svg_text = '' + directive = self._make_directive() + _, w, h = directive._prepare_svg(svg_text) + + assert w == "" + assert h == "" + + def test_handles_px_dimensions(self): + svg_text = '' + directive = self._make_directive() + _, w, h = directive._prepare_svg(svg_text) + + assert w == "200px" + assert h == "100px" + + +class TestBuildSvgStyles: + """Tests for StateMachineDiagram._build_svg_styles.""" + + def _make_directive(self, options=None): + from statemachine.contrib.diagram.sphinx_ext import StateMachineDiagram + + directive = StateMachineDiagram.__new__(StateMachineDiagram) + directive.options = options or {} + return directive + + def test_intrinsic_width_as_max_width(self): + directive = self._make_directive() + result = directive._build_svg_styles("702pt", "170pt") + assert "max-width: 702pt" in result + assert "height: auto" in result + + def test_explicit_width(self): + directive = self._make_directive({"width": "400px"}) + result = directive._build_svg_styles("702pt", "170pt") + assert "width: 400px" in result + assert "max-width" not in result + + def test_explicit_height(self): + directive = self._make_directive({"height": "200px"}) + result = directive._build_svg_styles("702pt", "170pt") + assert "height: 200px" in result + assert "height: auto" not in result + + def test_scale(self): + directive = self._make_directive({"scale": "50%"}) + result = directive._build_svg_styles("702pt", "170pt") + assert "width: 351.0pt" in result + assert "height: 85.0pt" in result + + def test_scale_without_intrinsic(self): + directive = self._make_directive({"scale": "50%"}) + result = directive._build_svg_styles("", "") + # No width/height when no intrinsic dimensions to scale + assert "max-width" not in result + assert "height: auto" in result + + def test_no_dimensions(self): + directive = self._make_directive() + result = directive._build_svg_styles("", "") + assert "height: auto" in result + + def test_explicit_width_overrides_scale(self): + directive = self._make_directive({"width": "300px", "scale": "50%"}) + result = directive._build_svg_styles("702pt", "170pt") + assert "width: 300px" in result + assert "351" not in result + + +class TestBuildWrapperClasses: + """Tests for StateMachineDiagram._build_wrapper_classes.""" + + def _make_directive(self, options=None): + from statemachine.contrib.diagram.sphinx_ext import StateMachineDiagram + + directive = StateMachineDiagram.__new__(StateMachineDiagram) + directive.options = options or {} + return directive + + def test_default_center_align(self): + directive = self._make_directive() + classes = directive._build_wrapper_classes() + assert classes == ["statemachine-diagram", "align-center"] + + def test_custom_align(self): + directive = self._make_directive({"align": "left"}) + classes = directive._build_wrapper_classes() + assert classes == ["statemachine-diagram", "align-left"] + + def test_extra_css_classes(self): + directive = self._make_directive({"class": ["my-class", "another"]}) + classes = directive._build_wrapper_classes() + assert classes == ["statemachine-diagram", "align-center", "my-class", "another"] + + +class TestResolveTarget: + """Tests for StateMachineDiagram._resolve_target.""" + + def _make_directive(self, options=None, tmp_path=None): + from statemachine.contrib.diagram.sphinx_ext import StateMachineDiagram + + directive = StateMachineDiagram.__new__(StateMachineDiagram) + directive.options = options or {} + directive.arguments = ["my.module.MyMachine"] + if tmp_path is not None: + directive.state = mock.MagicMock() + directive.state.document.settings.env.app.outdir = str(tmp_path) + return directive + + def test_no_target_option(self): + directive = self._make_directive() + assert directive._resolve_target("") == "" + + def test_explicit_target_url(self): + directive = self._make_directive({"target": "https://example.com/diagram.svg"}) + assert directive._resolve_target("") == "https://example.com/diagram.svg" + + def test_empty_target_generates_file(self, tmp_path): + directive = self._make_directive({"target": ""}, tmp_path=tmp_path) + svg_data = "" + result = directive._resolve_target(svg_data) + + assert result.startswith("/_images/statemachine-") + assert result.endswith(".svg") + + # Verify the file was written + images_dir = tmp_path / "_images" + svg_files = list(images_dir.glob("statemachine-*.svg")) + assert len(svg_files) == 1 + assert svg_files[0].read_text(encoding="utf-8") == svg_data + + def test_empty_target_deterministic_filename(self, tmp_path): + """Same qualname + events produces the same filename.""" + directive1 = self._make_directive({"target": "", "events": "go"}, tmp_path=tmp_path) + directive2 = self._make_directive({"target": "", "events": "go"}, tmp_path=tmp_path) + result1 = directive1._resolve_target("1") + result2 = directive2._resolve_target("2") + assert result1 == result2 + + def test_different_events_different_filename(self, tmp_path): + """Different events produce different filenames.""" + d1 = self._make_directive({"target": "", "events": "a"}, tmp_path=tmp_path) + d2 = self._make_directive({"target": "", "events": "b"}, tmp_path=tmp_path) + assert d1._resolve_target("") != d2._resolve_target("") + + +class TestDirectiveRun: + """Integration tests for StateMachineDiagram.run().""" + + _QUALNAME = "tests.examples.traffic_light_machine.TrafficLightMachine" + + def _make_directive(self, tmp_path, options=None): + from statemachine.contrib.diagram.sphinx_ext import StateMachineDiagram + + directive = StateMachineDiagram.__new__(StateMachineDiagram) + directive.options = options or {} + directive.lineno = 1 + directive.state_machine = mock.MagicMock() + directive.state = mock.MagicMock() + directive.state.document.settings.env.app.outdir = str(tmp_path) + directive.content_offset = 0 + return directive + + def _run(self, tmp_path, qualname=None, options=None): + directive = self._make_directive(tmp_path, options=options) + directive.arguments = [qualname or self._QUALNAME] + return directive, directive.run() + + def test_render_class_diagram(self, tmp_path): + """Renders a class diagram (no events) as inline SVG.""" + _, result = self._run(tmp_path) + + assert len(result) == 1 + node = result[0] + assert isinstance(node, nodes.raw) + assert node["format"] == "html" + html = node.astext() + assert " element.""" + _, result = self._run(tmp_path, options={"caption": "My caption"}) + + html = result[0].astext() + assert "My caption" in html + + def test_render_with_figclass(self, tmp_path): + """figclass adds extra CSS classes to the figure wrapper.""" + _, result = self._run(tmp_path, options={"caption": "Test", "figclass": ["extra-fig"]}) + + assert "extra-fig" in result[0].astext() + + def test_render_with_alt(self, tmp_path): + """Custom alt text appears in aria-label.""" + _, result = self._run(tmp_path, options={"alt": "Traffic light diagram"}) + + assert 'aria-label="Traffic light diagram"' in result[0].astext() + + def test_render_default_alt(self, tmp_path): + """Default alt text uses the class name from the qualname.""" + _, result = self._run(tmp_path) + + assert 'aria-label="TrafficLightMachine"' in result[0].astext() + + def test_render_with_explicit_target(self, tmp_path): + """Explicit target wraps diagram in a link.""" + _, result = self._run(tmp_path, options={"target": "https://example.com"}) + + html = result[0].astext() + assert 'href="https://example.com"' in html + assert 'target="_blank"' in html + + def test_render_with_empty_target(self, tmp_path): + """Empty target auto-generates a zoom SVG file.""" + _, result = self._run(tmp_path, options={"target": ""}) + + assert 'href="/_images/statemachine-' in result[0].astext() + images_dir = tmp_path / "_images" + assert any(images_dir.glob("statemachine-*.svg")) + + def test_render_with_align(self, tmp_path): + """Align option controls CSS class.""" + _, result = self._run(tmp_path, options={"align": "left"}) + + assert "align-left" in result[0].astext() + + def test_render_with_width(self, tmp_path): + """Width option is applied as inline style.""" + _, result = self._run(tmp_path, options={"width": "400px"}) + + assert "width: 400px" in result[0].astext() + + def test_render_with_name(self, tmp_path): + """Name option calls add_name for cross-referencing.""" + directive = self._make_directive(tmp_path, options={"name": "my-diagram"}) + directive.arguments = [self._QUALNAME] + result = directive.run() + + assert len(result) == 1 + + def test_render_with_class(self, tmp_path): + """Custom CSS classes appear in the wrapper.""" + _, result = self._run(tmp_path, options={"class": ["custom-class"]}) + + assert "custom-class" in result[0].astext() + + def test_invalid_qualname_returns_warning(self, tmp_path): + """Invalid qualname returns a warning node.""" + directive, result = self._run(tmp_path, qualname="nonexistent.module.BadMachine") + + assert len(result) == 1 + directive.state_machine.reporter.warning.assert_called_once() + call_args = directive.state_machine.reporter.warning.call_args + assert "could not import" in call_args[0][0] + + def test_render_failure_returns_warning(self, tmp_path): + """Diagram generation failure returns a warning node.""" + with mock.patch( + "statemachine.contrib.diagram.DotGraphMachine", + side_effect=RuntimeError("render failed"), + ): + directive, result = self._run(tmp_path) + + assert len(result) == 1 + directive.state_machine.reporter.warning.assert_called_once() + call_args = directive.state_machine.reporter.warning.call_args + assert "failed to generate" in call_args[0][0] + + def test_render_without_caption_uses_div(self, tmp_path): + """Without caption, the wrapper is a plain
.""" + _, result = self._run(tmp_path) + + html = result[0].astext() + assert " yellow : Cycle" in result + + def test_format_md_instance(self): + from tests.examples.traffic_light_machine import TrafficLightMachine + + sm = TrafficLightMachine() + result = f"{sm:md}" + assert "| State" in result + assert "Cycle" in result + + def test_format_md_class(self): + from tests.examples.traffic_light_machine import TrafficLightMachine + + result = f"{TrafficLightMachine:md}" + assert "| State" in result + + def test_format_markdown_alias(self): + from tests.examples.traffic_light_machine import TrafficLightMachine + + result = format(TrafficLightMachine, "markdown") + assert "| State" in result + + def test_format_rst_instance(self): + from tests.examples.traffic_light_machine import TrafficLightMachine + + sm = TrafficLightMachine() + result = f"{sm:rst}" + assert "+---" in result + + def test_format_rst_class(self): + from tests.examples.traffic_light_machine import TrafficLightMachine + + result = f"{TrafficLightMachine:rst}" + assert "+---" in result + + def test_format_dot_instance(self): + from tests.examples.traffic_light_machine import TrafficLightMachine + + sm = TrafficLightMachine() + result = f"{sm:dot}" + assert result.startswith("digraph TrafficLightMachine {") + assert "green" in result + + def test_format_dot_class(self): + from tests.examples.traffic_light_machine import TrafficLightMachine + + result = f"{TrafficLightMachine:dot}" + assert result.startswith("digraph TrafficLightMachine {") + + def test_format_empty_falls_back_to_repr(self): + from tests.examples.traffic_light_machine import TrafficLightMachine + + sm = TrafficLightMachine() + result = f"{sm:}" + assert "TrafficLightMachine(" in result + + def test_format_empty_class(self): + from tests.examples.traffic_light_machine import TrafficLightMachine + + result = f"{TrafficLightMachine:}" + assert "TrafficLightMachine" in result + + def test_format_invalid_raises(self): + from tests.examples.traffic_light_machine import TrafficLightMachine + + sm = TrafficLightMachine() + with pytest.raises(ValueError, match="Unsupported format"): + f"{sm:invalid}" + + def test_format_invalid_class_raises(self): + from tests.examples.traffic_light_machine import TrafficLightMachine + + with pytest.raises(ValueError, match="Unsupported format"): + f"{TrafficLightMachine:invalid}" + + +class TestDocstringExpansion: + """Tests for {statechart:FORMAT} placeholder expansion in docstrings.""" + + def test_md_placeholder(self): + from statemachine.state import State + from statemachine.statemachine import StateChart + + class MyMachine(StateChart): + """Machine. + + {statechart:md} + """ + + s1 = State(initial=True) + s2 = State(final=True) + + go = s1.to(s2) + + assert "| State" in MyMachine.__doc__ + assert "{statechart:md}" not in MyMachine.__doc__ + + def test_rst_placeholder(self): + from statemachine.state import State + from statemachine.statemachine import StateChart + + class MyMachine(StateChart): + """Machine. + + {statechart:rst} + """ + + s1 = State(initial=True) + s2 = State(final=True) + + go = s1.to(s2) + + assert "+---" in MyMachine.__doc__ + assert "{statechart:rst}" not in MyMachine.__doc__ + + def test_mermaid_placeholder(self): + from statemachine.state import State + from statemachine.statemachine import StateChart + + class MyMachine(StateChart): + """{statechart:mermaid}""" + + s1 = State(initial=True) + s2 = State(final=True) + + go = s1.to(s2) + + assert "stateDiagram-v2" in MyMachine.__doc__ + + def test_no_placeholder_unchanged(self): + from statemachine.state import State + from statemachine.statemachine import StateChart + + class MyMachine(StateChart): + """Just a plain docstring.""" + + s1 = State(initial=True) + s2 = State(final=True) + + go = s1.to(s2) + + assert MyMachine.__doc__ == "Just a plain docstring." + + def test_no_docstring(self): + from statemachine.state import State + from statemachine.statemachine import StateChart + + class MyMachine(StateChart): + s1 = State(initial=True) + s2 = State(final=True) + + go = s1.to(s2) + + assert MyMachine.__doc__ is None + + def test_indentation_preserved(self): + from statemachine.state import State + from statemachine.statemachine import StateChart + + class MyMachine(StateChart): + __doc__ = "Doc.\n\n Table:\n\n {statechart:md}\n\n End.\n" + + s1 = State(initial=True) + s2 = State(final=True) + + go = s1.to(s2) + + lines = MyMachine.__doc__.split("\n") + table_lines = [line for line in lines if "|" in line] + for line in table_lines: + assert line.startswith(" |") + assert "End." in MyMachine.__doc__ + + def test_multiple_placeholders(self): + from statemachine.state import State + from statemachine.statemachine import StateChart + + class MyMachine(StateChart): + """MD: {statechart:md} + + Mermaid: {statechart:mermaid} + """ + + s1 = State(initial=True) + s2 = State(final=True) + + go = s1.to(s2) + + assert "| State" in MyMachine.__doc__ + assert "stateDiagram-v2" in MyMachine.__doc__ + + +class TestFormatter: + """Tests for the Formatter facade (render, register_format, supported_formats).""" + + def test_render_mermaid(self): + from statemachine.contrib.diagram import formatter + + from tests.examples.traffic_light_machine import TrafficLightMachine + + result = formatter.render(TrafficLightMachine, "mermaid") + assert "stateDiagram-v2" in result + + def test_render_dot(self): + from statemachine.contrib.diagram import formatter + + from tests.examples.traffic_light_machine import TrafficLightMachine + + result = formatter.render(TrafficLightMachine, "dot") + assert result.startswith("digraph TrafficLightMachine {") + + def test_render_svg(self): + from statemachine.contrib.diagram import formatter + + from tests.examples.traffic_light_machine import TrafficLightMachine + + result = formatter.render(TrafficLightMachine, "svg") + assert isinstance(result, str) + assert " s1" in result + assert "s1 --> s2 : go" in result + + def test_initial_and_final(self): + graph = DiagramGraph( + name="InitFinal", + states=[ + DiagramState(id="s1", name="S1", type=StateType.REGULAR, is_initial=True), + DiagramState(id="s2", name="S2", type=StateType.FINAL), + ], + transitions=[ + DiagramTransition(source="s1", targets=["s2"], event="finish"), + ], + ) + result = MermaidRenderer().render(graph) + assert "[*] --> s1" in result + assert "s2 --> [*]" in result + + def test_custom_direction(self): + config = MermaidRendererConfig(direction="TB") + graph = DiagramGraph( + name="TB", + states=[DiagramState(id="a", name="A", type=StateType.REGULAR, is_initial=True)], + ) + result = MermaidRenderer(config=config).render(graph) + assert "direction TB" in result + + def test_state_name_differs_from_id(self): + graph = DiagramGraph( + name="Named", + states=[ + DiagramState( + id="my_state", name="My State", type=StateType.REGULAR, is_initial=True + ), + ], + ) + result = MermaidRenderer().render(graph) + assert 'state "My State" as my_state' in result + + def test_state_name_equals_id_no_declaration(self): + """When name == id, no explicit state declaration is emitted.""" + graph = DiagramGraph( + name="NoDecl", + states=[ + DiagramState(id="s1", name="s1", type=StateType.REGULAR, is_initial=True), + ], + ) + result = MermaidRenderer().render(graph) + assert 'state "s1"' not in result + + +class TestMermaidRendererTransitions: + """Transition rendering tests.""" + + def test_transition_with_guards(self): + graph = DiagramGraph( + name="Guards", + states=[ + DiagramState(id="s1", name="S1", type=StateType.REGULAR, is_initial=True), + DiagramState(id="s2", name="S2", type=StateType.REGULAR), + ], + transitions=[ + DiagramTransition(source="s1", targets=["s2"], event="go", guards=["is_ready"]), + ], + ) + result = MermaidRenderer().render(graph) + assert "s1 --> s2 : go [is_ready]" in result + + def test_eventless_transition(self): + graph = DiagramGraph( + name="Eventless", + states=[ + DiagramState(id="s1", name="S1", type=StateType.REGULAR, is_initial=True), + DiagramState(id="s2", name="S2", type=StateType.REGULAR), + ], + transitions=[ + DiagramTransition(source="s1", targets=["s2"], event=""), + ], + ) + result = MermaidRenderer().render(graph) + assert "s1 --> s2\n" in result + + def test_self_transition(self): + graph = DiagramGraph( + name="SelfLoop", + states=[ + DiagramState(id="s1", name="S1", type=StateType.REGULAR, is_initial=True), + ], + transitions=[ + DiagramTransition(source="s1", targets=["s1"], event="tick"), + ], + ) + result = MermaidRenderer().render(graph) + assert "s1 --> s1 : tick" in result + + def test_targetless_transition(self): + graph = DiagramGraph( + name="Targetless", + states=[ + DiagramState(id="s1", name="S1", type=StateType.REGULAR, is_initial=True), + ], + transitions=[ + DiagramTransition(source="s1", targets=[], event="tick"), + ], + ) + result = MermaidRenderer().render(graph) + assert "s1 --> s1 : tick" in result + + def test_multi_target_transition(self): + graph = DiagramGraph( + name="Multi", + states=[ + DiagramState(id="s1", name="S1", type=StateType.REGULAR, is_initial=True), + DiagramState(id="s2", name="S2", type=StateType.REGULAR), + DiagramState(id="s3", name="S3", type=StateType.REGULAR), + ], + transitions=[ + DiagramTransition(source="s1", targets=["s2", "s3"], event="split"), + ], + ) + result = MermaidRenderer().render(graph) + assert "s1 --> s2 : split" in result + assert "s1 --> s3 : split" in result + + def test_internal_transitions_skipped(self): + graph = DiagramGraph( + name="Internal", + states=[ + DiagramState(id="s1", name="S1", type=StateType.REGULAR, is_initial=True), + ], + transitions=[ + DiagramTransition(source="s1", targets=["s1"], event="check", is_internal=True), + ], + ) + result = MermaidRenderer().render(graph) + assert "s1 --> s1" not in result + + def test_initial_transitions_skipped(self): + graph = DiagramGraph( + name="InitTrans", + states=[ + DiagramState(id="s1", name="S1", type=StateType.REGULAR, is_initial=True), + DiagramState(id="s2", name="S2", type=StateType.REGULAR), + ], + transitions=[ + DiagramTransition(source="s1", targets=["s2"], event="", is_initial=True), + ], + ) + result = MermaidRenderer().render(graph) + # Implicit initial transitions are NOT rendered as edges + assert "s1 --> s2" not in result + + +class TestMermaidRendererActiveState: + """Active state highlighting tests.""" + + def test_active_state_class(self): + graph = DiagramGraph( + name="Active", + states=[ + DiagramState( + id="s1", name="S1", type=StateType.REGULAR, is_initial=True, is_active=True + ), + DiagramState(id="s2", name="S2", type=StateType.REGULAR), + ], + transitions=[ + DiagramTransition(source="s1", targets=["s2"], event="go"), + ], + ) + result = MermaidRenderer().render(graph) + assert "classDef active" in result + assert "s1:::active" in result + assert "s2:::active" not in result + + def test_no_active_state_no_classdef(self): + graph = DiagramGraph( + name="NoActive", + states=[ + DiagramState(id="s1", name="S1", type=StateType.REGULAR, is_initial=True), + ], + ) + result = MermaidRenderer().render(graph) + assert "classDef" not in result + + def test_active_fill_config(self): + config = MermaidRendererConfig(active_fill="#FF0000", active_stroke="#000") + graph = DiagramGraph( + name="CustomActive", + states=[ + DiagramState( + id="s1", name="S1", type=StateType.REGULAR, is_initial=True, is_active=True + ), + ], + ) + result = MermaidRenderer(config=config).render(graph) + assert "fill:#FF0000" in result + assert "stroke:#000" in result + + +class TestMermaidRendererCompound: + """Compound and parallel state tests.""" + + def test_compound_state(self): + class SM(StateChart): + class parent(State.Compound, name="Parent"): + child1 = State(initial=True) + child2 = State(final=True) + go = child1.to(child2) + + start = State(initial=True) + end = State(final=True) + + enter = start.to(parent) + finish = parent.to(end) + + result = MermaidGraphMachine(SM).get_mermaid() + assert 'state "Parent" as parent {' in result + assert "[*] --> child1" in result + assert "child1 --> child2 : Go" in result + assert "child2 --> [*]" in result + assert "start --> parent : Enter" in result + assert "parent --> end : Finish" in result + + def test_compound_no_duplicate_transitions(self): + """Transitions inside compound states must not also appear at top level.""" + + class SM(StateChart): + class parent(State.Compound, name="Parent"): + child1 = State(initial=True) + child2 = State(final=True) + go = child1.to(child2) + + start = State(initial=True) + enter = start.to(parent) + + result = MermaidGraphMachine(SM).get_mermaid() + # "child1 --> child2 : Go" should appear exactly once (inside compound) + assert result.count("child1 --> child2 : Go") == 1 + + def test_parallel_state(self): + class SM(StateChart): + class p(State.Parallel, name="Parallel"): + class r1(State.Compound, name="Region1"): + a = State(initial=True) + a_done = State(final=True) + finish_a = a.to(a_done) + + class r2(State.Compound, name="Region2"): + b = State(initial=True) + b_done = State(final=True) + finish_b = b.to(b_done) + + start = State(initial=True) + begin = start.to(p) + + result = MermaidGraphMachine(SM).get_mermaid() + assert 'state "Parallel" as p {' in result + assert "--" in result # parallel separator + + def test_parallel_redirects_compound_endpoints(self): + """Transitions to/from compound states inside parallel regions are redirected + to the initial child (Mermaid workaround for mermaid-js/mermaid#4052).""" + + class SM(StateChart): + class p(State.Parallel, name="Parallel"): + class region1(State.Compound, name="Region1"): + idle = State(initial=True) + + class inner(State.Compound, name="Inner"): + working = State(initial=True) + + start = idle.to(inner) + + class region2(State.Compound, name="Region2"): + x = State(initial=True) + + begin = State(initial=True) + enter = begin.to(p) + + result = MermaidGraphMachine(SM).get_mermaid() + # Inside parallel: compound endpoint redirected to initial child + assert "idle --> working : Start" in result + assert "idle --> inner" not in result + + def test_compound_outside_parallel_not_redirected(self): + """Compound states outside parallel regions keep direct transitions.""" + + class SM(StateChart): + class parent(State.Compound, name="Parent"): + child = State(initial=True) + + start = State(initial=True) + end = State(final=True) + enter = start.to(parent) + leave = parent.to(end) + + result = MermaidGraphMachine(SM).get_mermaid() + assert "start --> parent : Enter" in result + assert "parent --> end : Leave" in result + + def test_nested_compound(self): + class SM(StateChart): + class outer(State.Compound, name="Outer"): + class inner(State.Compound, name="Inner"): + deep = State(initial=True) + deep_final = State(final=True) + go_deep = deep.to(deep_final) + + start_inner = State(initial=True) + to_inner = start_inner.to(inner) + + begin = State(initial=True) + enter = begin.to(outer) + + result = MermaidGraphMachine(SM).get_mermaid() + assert 'state "Outer" as outer {' in result + assert 'state "Inner" as inner {' in result + + +class TestMermaidRendererPseudoStates: + """Pseudo-state rendering tests.""" + + def test_history_shallow(self): + graph = DiagramGraph( + name="History", + states=[ + DiagramState( + id="comp", + name="Comp", + type=StateType.REGULAR, + is_initial=True, + children=[ + DiagramState(id="h", name="H", type=StateType.HISTORY_SHALLOW), + DiagramState(id="c1", name="C1", type=StateType.REGULAR, is_initial=True), + ], + ), + ], + compound_state_ids={"comp"}, + ) + result = MermaidRenderer().render(graph) + assert 'state "H" as h' in result + + def test_history_deep(self): + graph = DiagramGraph( + name="DeepHistory", + states=[ + DiagramState( + id="comp", + name="Comp", + type=StateType.REGULAR, + is_initial=True, + children=[ + DiagramState(id="h", name="H*", type=StateType.HISTORY_DEEP), + DiagramState(id="c1", name="C1", type=StateType.REGULAR, is_initial=True), + ], + ), + ], + compound_state_ids={"comp"}, + ) + result = MermaidRenderer().render(graph) + assert 'state "H*" as h' in result + + def test_choice_state(self): + graph = DiagramGraph( + name="Choice", + states=[ + DiagramState(id="ch", name="ch", type=StateType.CHOICE, is_initial=True), + ], + ) + result = MermaidRenderer().render(graph) + assert "state ch <>" in result + + def test_fork_state(self): + graph = DiagramGraph( + name="Fork", + states=[ + DiagramState(id="fk", name="fk", type=StateType.FORK, is_initial=True), + ], + ) + result = MermaidRenderer().render(graph) + assert "state fk <>" in result + + def test_join_state(self): + graph = DiagramGraph( + name="Join", + states=[ + DiagramState(id="jn", name="jn", type=StateType.JOIN, is_initial=True), + ], + ) + result = MermaidRenderer().render(graph) + assert "state jn <>" in result + + +class TestMermaidRendererActions: + """State action rendering tests.""" + + def test_entry_exit_actions(self): + graph = DiagramGraph( + name="Actions", + states=[ + DiagramState( + id="s1", + name="S1", + type=StateType.REGULAR, + is_initial=True, + actions=[ + DiagramAction(type=ActionType.ENTRY, body="setup"), + DiagramAction(type=ActionType.EXIT, body="cleanup"), + ], + ), + ], + ) + result = MermaidRenderer().render(graph) + assert "s1 : entry / setup" in result + assert "s1 : exit / cleanup" in result + + def test_internal_action(self): + graph = DiagramGraph( + name="InternalAction", + states=[ + DiagramState( + id="s1", + name="S1", + type=StateType.REGULAR, + is_initial=True, + actions=[ + DiagramAction(type=ActionType.INTERNAL, body="tick / handle"), + ], + ), + ], + ) + result = MermaidRenderer().render(graph) + assert "s1 : tick / handle" in result + + def test_empty_internal_action_skipped(self): + graph = DiagramGraph( + name="EmptyInternal", + states=[ + DiagramState( + id="s1", + name="S1", + type=StateType.REGULAR, + is_initial=True, + actions=[ + DiagramAction(type=ActionType.INTERNAL, body=""), + ], + ), + ], + ) + result = MermaidRenderer().render(graph) + assert "s1 : " not in result + + +class TestMermaidGraphMachine: + """Tests for the MermaidGraphMachine facade.""" + + def test_facade_returns_string(self): + from tests.examples.traffic_light_machine import TrafficLightMachine + + result = MermaidGraphMachine(TrafficLightMachine).get_mermaid() + assert isinstance(result, str) + assert "stateDiagram-v2" in result + + def test_facade_callable(self): + from tests.examples.traffic_light_machine import TrafficLightMachine + + facade = MermaidGraphMachine(TrafficLightMachine) + assert facade() == facade.get_mermaid() + + def test_facade_with_instance(self): + from tests.examples.traffic_light_machine import TrafficLightMachine + + sm = TrafficLightMachine() + result = MermaidGraphMachine(sm).get_mermaid() + assert "green:::active" in result + + def test_facade_custom_config(self): + from tests.examples.traffic_light_machine import TrafficLightMachine + + class Custom(MermaidGraphMachine): + direction = "TB" + active_fill = "#FF0000" + + sm = TrafficLightMachine() + result = Custom(sm).get_mermaid() + assert "direction TB" in result + assert "fill:#FF0000" in result + + +class TestMermaidRendererEdgeCases: + """Edge case tests for coverage.""" + + def test_compound_state_name_equals_id(self): + """Compound state where name == id uses unquoted declaration.""" + graph = DiagramGraph( + name="NameId", + states=[ + DiagramState( + id="comp", + name="comp", + type=StateType.REGULAR, + is_initial=True, + children=[ + DiagramState(id="c1", name="C1", type=StateType.REGULAR, is_initial=True), + ], + ), + ], + compound_state_ids={"comp"}, + ) + result = MermaidRenderer().render(graph) + assert "state comp {" in result + assert '"comp"' not in result + + def test_active_compound_state(self): + """Compound state that is active gets classDef.""" + graph = DiagramGraph( + name="ActiveComp", + states=[ + DiagramState( + id="comp", + name="Comp", + type=StateType.REGULAR, + is_initial=True, + is_active=True, + children=[ + DiagramState(id="c1", name="C1", type=StateType.REGULAR, is_initial=True), + ], + ), + ], + compound_state_ids={"comp"}, + ) + result = MermaidRenderer().render(graph) + assert "comp:::active" in result + + def test_cross_scope_transition_rendered_at_parent(self): + """Transition crossing compound boundaries is rendered at the parent scope.""" + graph = DiagramGraph( + name="CrossScope", + states=[ + DiagramState( + id="comp", + name="Comp", + type=StateType.REGULAR, + is_initial=True, + children=[ + DiagramState(id="c1", name="C1", type=StateType.REGULAR, is_initial=True), + ], + ), + DiagramState(id="outside", name="Outside", type=StateType.REGULAR), + ], + transitions=[ + DiagramTransition(source="c1", targets=["outside"], event="leave"), + ], + compound_state_ids={"comp"}, + ) + result = MermaidRenderer().render(graph) + # c1 is inside comp, outside is at top level — the transition + # crosses the compound boundary and is rendered at the top scope. + assert "c1 --> outside : leave" in result + # It should NOT appear inside the compound block + lines = result.split("\n") + for line in lines: + if "c1 --> outside" in line: + # Should be at indent level 1 (top scope), not deeper + assert line.startswith(" c1"), f"Expected top-level indent, got: {line!r}" + + def test_cross_scope_to_history_state(self): + """Transition from outside a compound to a history state inside it is rendered.""" + graph = DiagramGraph( + name="HistoryCross", + states=[ + DiagramState( + id="process", + name="Process", + type=StateType.REGULAR, + children=[ + DiagramState( + id="step1", name="Step1", type=StateType.REGULAR, is_initial=True + ), + DiagramState(id="step2", name="Step2", type=StateType.REGULAR), + DiagramState(id="h", name="H", type=StateType.HISTORY_SHALLOW), + ], + ), + DiagramState(id="paused", name="Paused", type=StateType.REGULAR, is_initial=True), + ], + transitions=[ + DiagramTransition(source="step1", targets=["step2"], event="advance"), + DiagramTransition(source="process", targets=["paused"], event="pause"), + DiagramTransition(source="paused", targets=["h"], event="resume"), + DiagramTransition(source="paused", targets=["process"], event="begin"), + ], + compound_state_ids={"process"}, + ) + result = MermaidRenderer().render(graph) + # The resume transition crosses the compound boundary + assert "paused --> h : resume" in result + # advance stays inside the compound + assert "step1 --> step2 : advance" in result + # pause and begin are at top level (both endpoints are top-level) + assert "process --> paused : pause" in result + assert "paused --> process : begin" in result + + def test_no_initial_state(self): + """Graph with no initial state omits [*] arrow.""" + graph = DiagramGraph( + name="NoInitial", + states=[ + DiagramState(id="s1", name="S1", type=StateType.REGULAR), + ], + ) + result = MermaidRenderer().render(graph) + assert "[*]" not in result + + def test_duplicate_transition_rendered_once(self): + """Duplicate transitions in the IR are rendered only once.""" + graph = DiagramGraph( + name="Dedup", + states=[ + DiagramState(id="s1", name="S1", type=StateType.REGULAR, is_initial=True), + DiagramState(id="s2", name="S2", type=StateType.REGULAR), + ], + transitions=[ + DiagramTransition(source="s1", targets=["s2"], event="go"), + DiagramTransition(source="s1", targets=["s2"], event="go"), + ], + ) + result = MermaidRenderer().render(graph) + assert result.count("s1 --> s2 : go") == 1 + + def test_compound_no_initial_child(self): + """Compound state with no initial child omits internal [*] arrow.""" + graph = DiagramGraph( + name="NoInitChild", + states=[ + DiagramState( + id="comp", + name="Comp", + type=StateType.REGULAR, + is_initial=True, + children=[ + DiagramState(id="c1", name="C1", type=StateType.REGULAR), + ], + ), + ], + compound_state_ids={"comp"}, + ) + result = MermaidRenderer().render(graph) + # No [*] --> c1 inside the compound + lines = result.strip().split("\n") + inner_initial = [ln for ln in lines if "[*] --> c1" in ln] + assert len(inner_initial) == 0 + + +class TestMermaidRendererIntegration: + """Integration tests with real state machines.""" + + def test_traffic_light(self): + from tests.examples.traffic_light_machine import TrafficLightMachine + + result = MermaidGraphMachine(TrafficLightMachine).get_mermaid() + assert "green --> yellow : Cycle" in result + assert "yellow --> red : Cycle" in result + assert "red --> green : Cycle" in result + + def test_traffic_light_with_events(self): + from tests.examples.traffic_light_machine import TrafficLightMachine + + sm = TrafficLightMachine() + sm.send("cycle") + result = MermaidGraphMachine(sm).get_mermaid() + assert "yellow:::active" in result diff --git a/tests/test_mixins.py b/tests/test_mixins.py index 938c710e..f1ae83b9 100644 --- a/tests/test_mixins.py +++ b/tests/test_mixins.py @@ -5,7 +5,7 @@ class MyMixedModel(MyModel, MachineMixin): - state_machine_name = "tests.conftest.CampaignMachine" + state_machine_name = "tests.machines.workflow.campaign_machine.CampaignMachine" def test_mixin_should_instantiate_a_machine(campaign_machine): diff --git a/tests/test_multiple_destinations.py b/tests/test_multiple_destinations.py index 59f38494..96e63953 100644 --- a/tests/test_multiple_destinations.py +++ b/tests/test_multiple_destinations.py @@ -163,7 +163,7 @@ def on_validate(self, previous_configuration): assert machine.is_terminated - with pytest.raises(exceptions.TransitionNotAllowed, match="Can't validate when in Completed."): + with pytest.raises(exceptions.TransitionNotAllowed, match="Can't Validate when in Completed."): assert machine.validate() diff --git a/tests/test_profiling.py b/tests/test_profiling.py index 7da292ff..43863ebd 100644 --- a/tests/test_profiling.py +++ b/tests/test_profiling.py @@ -2,10 +2,16 @@ import pytest +from statemachine import HistoryState from statemachine import State from statemachine import StateChart +# --------------------------------------------------------------------------- +# Machines under test +# --------------------------------------------------------------------------- + +# 1. Flat machine with model, guards, and listener callbacks (v1-style) class OrderControl(StateChart): allow_event_without_transition = False catch_errors_as_events = False @@ -45,6 +51,111 @@ def after_receive_payment(self): self.payment_received = True +# 2. Compound (nested) states +class CompoundSC(StateChart): + class active(State.Compound, name="Active"): + idle = State(initial=True) + working = State() + begin = idle.to(working) + + off = State(initial=True) + done = State(final=True) + + turn_on = off.to(active) + turn_off = active.to(done) + + +# 3. Parallel regions +class ParallelSC(StateChart): + class both(State.Parallel, name="Both"): + class left(State.Compound, name="Left"): + l1 = State(initial=True) + l2 = State() + go_l = l1.to(l2) + back_l = l2.to(l1) + + class right(State.Compound, name="Right"): + r1 = State(initial=True) + r2 = State() + go_r = r1.to(r2) + back_r = r2.to(r1) + + start = State(initial=True) + enter = start.to(both) + + +# 4. Guards with boolean expressions +class GuardedSC(StateChart): + s1 = State(initial=True) + s2 = State() + s3 = State(final=True) + + def check_a(self): + return True + + def check_b(self): + return False + + go = s1.to(s2, cond="check_a") | s1.to(s3, cond="check_b") + back = s2.to(s1) + + +# 5. History states (shallow) +class HistoryShallowSC(StateChart): + class process(State.Compound, name="Process"): + step1 = State(initial=True) + step2 = State() + advance = step1.to(step2) + h = HistoryState() + + paused = State(initial=True) + + pause = process.to(paused) + resume = paused.to(process.h) + begin = paused.to(process) + + +# 6. Deep history with nested compound states +class DeepHistorySC(StateChart): + class outer(State.Compound, name="Outer"): + class inner(State.Compound, name="Inner"): + a = State(initial=True) + b = State() + go = a.to(b) + back = b.to(a) + + start = State(initial=True) + enter_inner = start.to(inner) + h = HistoryState(type="deep") + + away = State(initial=True) + + dive = away.to(outer) + leave = outer.to(away) + restore = away.to(outer.h) + + +# 7. Many-transition stress machine (wide, not deep) +class ManyTransitionsSC(StateChart): + s1 = State(initial=True) + s2 = State() + s3 = State() + s4 = State() + s5 = State() + + go_12 = s1.to(s2) + go_23 = s2.to(s3) + go_34 = s3.to(s4) + go_45 = s4.to(s5) + go_51 = s5.to(s1) + reset = s2.to(s1) | s3.to(s1) | s4.to(s1) | s5.to(s1) + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + def create_order(): order = Order() assert order.state_machine.waiting_for_payment.is_active @@ -54,12 +165,129 @@ def add_to_order(sm, amount): sm.add_to_order(amount) +# --------------------------------------------------------------------------- +# Benchmark: instance creation +# --------------------------------------------------------------------------- + + @pytest.mark.slow() -def test_setup_performance(benchmark): - benchmark.pedantic(create_order, rounds=10, iterations=1000) +class TestSetupPerformance: + """Benchmark the cost of creating and activating state machine instances.""" + + def test_flat_machine(self, benchmark): + benchmark.pedantic(create_order, rounds=10, iterations=1000) + + def test_compound_machine(self, benchmark): + benchmark.pedantic(lambda: CompoundSC(), rounds=10, iterations=1000) + + def test_parallel_machine(self, benchmark): + benchmark.pedantic(lambda: ParallelSC(), rounds=10, iterations=1000) + + def test_guarded_machine(self, benchmark): + benchmark.pedantic(lambda: GuardedSC(), rounds=10, iterations=1000) + + def test_history_machine(self, benchmark): + benchmark.pedantic(lambda: HistoryShallowSC(), rounds=10, iterations=1000) + + def test_deep_history_machine(self, benchmark): + benchmark.pedantic(lambda: DeepHistorySC(), rounds=10, iterations=1000) + + +# --------------------------------------------------------------------------- +# Benchmark: event throughput +# --------------------------------------------------------------------------- @pytest.mark.slow() -def test_event_performance(benchmark): - order = Order() - benchmark.pedantic(add_to_order, args=(order.state_machine, 1), rounds=10, iterations=1000) +class TestEventPerformance: + """Benchmark event processing (self-transitions and state changes).""" + + def test_flat_self_transition(self, benchmark): + """Self-transition on a flat machine with model/listener.""" + order = Order() + sm = order.state_machine + benchmark.pedantic(add_to_order, args=(sm, 1), rounds=10, iterations=1000) + + def test_compound_enter_exit(self, benchmark): + """Enter and exit a compound state repeatedly.""" + + def cycle(): + sm = CompoundSC() + sm.turn_on() + sm.begin() + sm.turn_off() + + benchmark.pedantic(cycle, rounds=10, iterations=500) + + def test_parallel_region_events(self, benchmark): + """Send events within parallel regions.""" + sm = ParallelSC() + sm.enter() + + def cycle(): + sm.go_l() + sm.go_r() + sm.back_l() + sm.back_r() + + benchmark.pedantic(cycle, rounds=10, iterations=500) + + def test_guarded_transitions(self, benchmark): + """Guard evaluation + transition selection.""" + sm = GuardedSC() + + def cycle(): + sm.go() + sm.back() + + benchmark.pedantic(cycle, rounds=10, iterations=1000) + + def test_history_pause_resume(self, benchmark): + """Shallow history: pause and resume compound state.""" + sm = HistoryShallowSC() + sm.begin() + sm.advance() + + def cycle(): + sm.pause() + sm.resume() + + benchmark.pedantic(cycle, rounds=10, iterations=500) + + def test_deep_history_cycle(self, benchmark): + """Deep history: leave and restore nested compound state.""" + sm = DeepHistorySC() + sm.dive() + sm.enter_inner() + sm.go() + + def cycle(): + sm.leave() + sm.restore() + + benchmark.pedantic(cycle, rounds=10, iterations=500) + + def test_many_transitions_full_cycle(self, benchmark): + """Traverse a 5-state ring (s1→s2→s3→s4→s5→s1).""" + sm = ManyTransitionsSC() + + def cycle(): + sm.go_12() + sm.go_23() + sm.go_34() + sm.go_45() + sm.go_51() + + benchmark.pedantic(cycle, rounds=10, iterations=500) + + def test_many_transitions_reset(self, benchmark): + """Composite event (|) selecting among multiple source states.""" + sm = ManyTransitionsSC() + + def cycle(): + sm.go_12() + sm.go_23() + sm.go_34() + sm.reset() + + benchmark.pedantic(cycle, rounds=10, iterations=500) diff --git a/tests/test_statechart_compound.py b/tests/test_statechart_compound.py index 49757ae9..334b4f28 100644 --- a/tests/test_statechart_compound.py +++ b/tests/test_statechart_compound.py @@ -11,39 +11,26 @@ from statemachine import State from statemachine import StateChart +from tests.machines.compound.middle_earth_journey import MiddleEarthJourney +from tests.machines.compound.middle_earth_journey_two_compounds import ( + MiddleEarthJourneyTwoCompounds, +) +from tests.machines.compound.middle_earth_journey_with_finals import MiddleEarthJourneyWithFinals +from tests.machines.compound.moria_expedition import MoriaExpedition +from tests.machines.compound.moria_expedition_with_escape import MoriaExpeditionWithEscape +from tests.machines.compound.quest_for_erebor import QuestForErebor +from tests.machines.compound.shire_to_rivendell import ShireToRivendell @pytest.mark.timeout(5) class TestCompoundStates: async def test_enter_compound_activates_initial_child(self, sm_runner): """Entering a compound activates both parent and the initial child.""" - - class ShireToRivendell(StateChart): - class shire(State.Compound): - bag_end = State(initial=True) - green_dragon = State() - - visit_pub = bag_end.to(green_dragon) - - road = State(final=True) - depart = shire.to(road) - sm = await sm_runner.start(ShireToRivendell) assert {"shire", "bag_end"} == set(sm.configuration_values) async def test_transition_within_compound(self, sm_runner): """Inner state changes while parent stays active.""" - - class ShireToRivendell(StateChart): - class shire(State.Compound): - bag_end = State(initial=True) - green_dragon = State() - - visit_pub = bag_end.to(green_dragon) - - road = State(final=True) - depart = shire.to(road) - sm = await sm_runner.start(ShireToRivendell) await sm_runner.send(sm, "visit_pub") assert "shire" in sm.configuration_values @@ -52,86 +39,23 @@ class shire(State.Compound): async def test_exit_compound_removes_all_descendants(self, sm_runner): """Leaving a compound removes the parent and all children.""" - - class ShireToRivendell(StateChart): - class shire(State.Compound): - bag_end = State(initial=True) - green_dragon = State() - - visit_pub = bag_end.to(green_dragon) - - road = State(final=True) - depart = shire.to(road) - sm = await sm_runner.start(ShireToRivendell) await sm_runner.send(sm, "depart") assert {"road"} == set(sm.configuration_values) async def test_nested_compound_two_levels(self, sm_runner): """Three-level nesting: outer > middle > leaf.""" - - class MoriaExpedition(StateChart): - class moria(State.Compound): - class upper_halls(State.Compound): - entrance = State(initial=True) - bridge = State(final=True) - - cross = entrance.to(bridge) - - assert isinstance(upper_halls, State) - depths = State(final=True) - descend = upper_halls.to(depths) - sm = await sm_runner.start(MoriaExpedition) assert {"moria", "upper_halls", "entrance"} == set(sm.configuration_values) async def test_transition_from_inner_to_outer(self, sm_runner): """A deep child can transition to an outer state.""" - - class MoriaExpedition(StateChart): - class moria(State.Compound): - class upper_halls(State.Compound): - entrance = State(initial=True) - bridge = State() - - cross = entrance.to(bridge) - - assert isinstance(upper_halls, State) - depths = State(final=True) - descend = upper_halls.to(depths) - - daylight = State(final=True) - escape = moria.to(daylight) - - sm = await sm_runner.start(MoriaExpedition) + sm = await sm_runner.start(MoriaExpeditionWithEscape) await sm_runner.send(sm, "escape") assert {"daylight"} == set(sm.configuration_values) async def test_cross_compound_transition(self, sm_runner): """Transition from one compound to another removes old children.""" - - class MiddleEarthJourney(StateChart): - class rivendell(State.Compound): - council = State(initial=True) - preparing = State() - - get_ready = council.to(preparing) - - class moria(State.Compound): - gates = State(initial=True) - bridge = State(final=True) - - cross = gates.to(bridge) - - class lothlorien(State.Compound): - mirror = State(initial=True) - departure = State(final=True) - - leave = mirror.to(departure) - - march_to_moria = rivendell.to(moria) - march_to_lorien = moria.to(lothlorien) - sm = await sm_runner.start(MiddleEarthJourney) assert "rivendell" in sm.configuration_values assert "council" in sm.configuration_values @@ -144,40 +68,13 @@ class lothlorien(State.Compound): async def test_enter_compound_lands_on_initial(self, sm_runner): """Entering a compound from outside lands on the initial child.""" - - class MiddleEarthJourney(StateChart): - class rivendell(State.Compound): - council = State(initial=True) - preparing = State() - - get_ready = council.to(preparing) - - class moria(State.Compound): - gates = State(initial=True) - bridge = State(final=True) - - cross = gates.to(bridge) - - march_to_moria = rivendell.to(moria) - - sm = await sm_runner.start(MiddleEarthJourney) + sm = await sm_runner.start(MiddleEarthJourneyTwoCompounds) await sm_runner.send(sm, "march_to_moria") assert "gates" in sm.configuration_values assert "moria" in sm.configuration_values async def test_final_child_fires_done_state(self, sm_runner): """Reaching a final child triggers done.state.{parent_id}.""" - - class QuestForErebor(StateChart): - class lonely_mountain(State.Compound): - approach = State(initial=True) - inside = State(final=True) - - enter_mountain = approach.to(inside) - - victory = State(final=True) - done_state_lonely_mountain = lonely_mountain.to(victory) - sm = await sm_runner.start(QuestForErebor) assert "approach" in sm.configuration_values @@ -186,30 +83,7 @@ class lonely_mountain(State.Compound): async def test_multiple_compound_sequential_traversal(self, sm_runner): """Traverse all three compounds sequentially.""" - - class MiddleEarthJourney(StateChart): - class rivendell(State.Compound): - council = State(initial=True) - preparing = State(final=True) - - get_ready = council.to(preparing) - - class moria(State.Compound): - gates = State(initial=True) - bridge = State(final=True) - - cross = gates.to(bridge) - - class lothlorien(State.Compound): - mirror = State(initial=True) - departure = State(final=True) - - leave = mirror.to(departure) - - march_to_moria = rivendell.to(moria) - march_to_lorien = moria.to(lothlorien) - - sm = await sm_runner.start(MiddleEarthJourney) + sm = await sm_runner.start(MiddleEarthJourneyWithFinals) await sm_runner.send(sm, "march_to_moria") assert "moria" in sm.configuration_values diff --git a/tests/test_statechart_donedata.py b/tests/test_statechart_donedata.py index ca191361..60acd02a 100644 --- a/tests/test_statechart_donedata.py +++ b/tests/test_statechart_donedata.py @@ -13,77 +13,32 @@ from statemachine import Event from statemachine import State from statemachine import StateChart +from tests.machines.donedata.destroy_the_ring import DestroyTheRing +from tests.machines.donedata.destroy_the_ring_simple import DestroyTheRingSimple +from tests.machines.donedata.nested_quest_donedata import NestedQuestDoneData +from tests.machines.donedata.quest_for_erebor_done_convention import QuestForEreborDoneConvention +from tests.machines.donedata.quest_for_erebor_explicit_id import QuestForEreborExplicitId +from tests.machines.donedata.quest_for_erebor_multi_word import QuestForEreborMultiWord +from tests.machines.donedata.quest_for_erebor_with_event import QuestForEreborWithEvent @pytest.mark.timeout(5) class TestDoneData: async def test_donedata_callable_returns_dict(self, sm_runner): """Handler receives donedata as kwargs.""" - received = {} - - class DestroyTheRing(StateChart): - class quest(State.Compound): - traveling = State(initial=True) - completed = State(final=True, donedata="get_quest_result") - - finish = traveling.to(completed) - - def get_quest_result(self): - return {"ring_destroyed": True, "hero": "frodo"} - - epilogue = State(final=True) - done_state_quest = Event(quest.to(epilogue, on="capture_result")) - - def capture_result(self, ring_destroyed=None, hero=None, **kwargs): - received["ring_destroyed"] = ring_destroyed - received["hero"] = hero - sm = await sm_runner.start(DestroyTheRing) await sm_runner.send(sm, "finish") - assert received["ring_destroyed"] is True - assert received["hero"] == "frodo" + assert sm.received["ring_destroyed"] is True + assert sm.received["hero"] == "frodo" async def test_donedata_fires_done_state_with_data(self, sm_runner): """done.state event fires and triggers a transition.""" - - class DestroyTheRing(StateChart): - class quest(State.Compound): - traveling = State(initial=True) - completed = State(final=True, donedata="get_result") - - finish = traveling.to(completed) - - def get_result(self): - return {"outcome": "victory"} - - celebration = State(final=True) - done_state_quest = Event(quest.to(celebration)) - - sm = await sm_runner.start(DestroyTheRing) + sm = await sm_runner.start(DestroyTheRingSimple) await sm_runner.send(sm, "finish") assert {"celebration"} == set(sm.configuration_values) async def test_donedata_in_nested_compound(self, sm_runner): """Inner done.state propagates up through nesting.""" - - class NestedQuestDoneData(StateChart): - class outer(State.Compound): - class inner(State.Compound): - start = State(initial=True) - end = State(final=True, donedata="inner_result") - - go = start.to(end) - - def inner_result(self): - return {"level": "inner"} - - assert isinstance(inner, State) - after_inner = State(final=True) - done_state_inner = Event(inner.to(after_inner)) - - final = State(final=True) - done_state_outer = Event(outer.to(final)) - sm = await sm_runner.start(NestedQuestDoneData) await sm_runner.send(sm, "go") # inner finishes -> done.state.inner -> after_inner (final) @@ -108,7 +63,7 @@ class QuestListener: def on_enter_celebration(self, ring_destroyed=None, **kwargs): captured["ring_destroyed"] = ring_destroyed - class DestroyTheRing(StateChart): + class DestroyTheRingWithListener(StateChart): class quest(State.Compound): traveling = State(initial=True) completed = State(final=True, donedata="get_result") @@ -122,7 +77,7 @@ def get_result(self): done_state_quest = Event(quest.to(celebration)) listener = QuestListener() - sm = await sm_runner.start(DestroyTheRing, listeners=[listener]) + sm = await sm_runner.start(DestroyTheRingWithListener, listeners=[listener]) await sm_runner.send(sm, "finish") assert {"celebration"} == set(sm.configuration_values) @@ -131,68 +86,24 @@ def get_result(self): class TestDoneStateConvention: async def test_done_state_convention_with_transition_list(self, sm_runner): """Bare TransitionList with done_state_ name auto-registers done.state.X.""" - - class QuestForErebor(StateChart): - class quest(State.Compound): - traveling = State(initial=True) - arrived = State(final=True) - - finish = traveling.to(arrived) - - celebration = State(final=True) - done_state_quest = quest.to(celebration) - - sm = await sm_runner.start(QuestForErebor) + sm = await sm_runner.start(QuestForEreborDoneConvention) await sm_runner.send(sm, "finish") assert {"celebration"} == set(sm.configuration_values) async def test_done_state_convention_with_event_no_explicit_id(self, sm_runner): """Event() wrapper without explicit id= applies the convention.""" - - class QuestForErebor(StateChart): - class quest(State.Compound): - traveling = State(initial=True) - arrived = State(final=True) - - finish = traveling.to(arrived) - - celebration = State(final=True) - done_state_quest = Event(quest.to(celebration)) - - sm = await sm_runner.start(QuestForErebor) + sm = await sm_runner.start(QuestForEreborWithEvent) await sm_runner.send(sm, "finish") assert {"celebration"} == set(sm.configuration_values) async def test_done_state_convention_preserves_explicit_id(self, sm_runner): """Explicit id= takes precedence over the convention.""" - - class QuestForErebor(StateChart): - class quest(State.Compound): - traveling = State(initial=True) - arrived = State(final=True) - - finish = traveling.to(arrived) - - celebration = State(final=True) - done_state_quest = Event(quest.to(celebration), id="done.state.quest") - - sm = await sm_runner.start(QuestForErebor) + sm = await sm_runner.start(QuestForEreborExplicitId) await sm_runner.send(sm, "finish") assert {"celebration"} == set(sm.configuration_values) async def test_done_state_convention_with_multi_word_state(self, sm_runner): """done_state_lonely_mountain maps to done.state.lonely_mountain.""" - - class QuestForErebor(StateChart): - class lonely_mountain(State.Compound): - approach = State(initial=True) - inside = State(final=True) - - enter_mountain = approach.to(inside) - - victory = State(final=True) - done_state_lonely_mountain = lonely_mountain.to(victory) - - sm = await sm_runner.start(QuestForErebor) + sm = await sm_runner.start(QuestForEreborMultiWord) await sm_runner.send(sm, "enter_mountain") assert {"victory"} == set(sm.configuration_values) diff --git a/tests/test_statechart_eventless.py b/tests/test_statechart_eventless.py index 4e69eb68..ab75f37e 100644 --- a/tests/test_statechart_eventless.py +++ b/tests/test_statechart_eventless.py @@ -9,30 +9,19 @@ import pytest -from statemachine import State -from statemachine import StateChart +from tests.machines.eventless.auto_advance import AutoAdvance +from tests.machines.eventless.beacon_chain import BeaconChain +from tests.machines.eventless.beacon_chain_lighting import BeaconChainLighting +from tests.machines.eventless.coordinated_advance import CoordinatedAdvance +from tests.machines.eventless.ring_corruption import RingCorruption +from tests.machines.eventless.ring_corruption_with_bear_ring import RingCorruptionWithBearRing +from tests.machines.eventless.ring_corruption_with_tick import RingCorruptionWithTick @pytest.mark.timeout(5) class TestEventlessTransitions: async def test_eventless_fires_when_condition_met(self, sm_runner): """Eventless transition fires when guard is True.""" - - class RingCorruption(StateChart): - resisting = State(initial=True) - corrupted = State(final=True) - - # eventless: no event name - resisting.to(corrupted, cond="is_corrupted") - - ring_power = 0 - - def is_corrupted(self): - return self.ring_power > 5 - - def increase_power(self): - self.ring_power += 3 - sm = await sm_runner.start(RingCorruption) assert "resisting" in sm.configuration_values @@ -43,65 +32,20 @@ def increase_power(self): async def test_eventless_does_not_fire_when_condition_false(self, sm_runner): """Eventless transition stays when guard is False.""" - - class RingCorruption(StateChart): - resisting = State(initial=True) - corrupted = State(final=True) - - resisting.to(corrupted, cond="is_corrupted") - tick = resisting.to.itself(internal=True) - - ring_power = 0 - - def is_corrupted(self): - return self.ring_power > 5 - - sm = await sm_runner.start(RingCorruption) + sm = await sm_runner.start(RingCorruptionWithTick) sm.ring_power = 2 await sm_runner.send(sm, "tick") assert "resisting" in sm.configuration_values async def test_eventless_chain_cascades(self, sm_runner): """All beacons light in a single macrostep via unconditional eventless chain.""" - - class BeaconChainLighting(StateChart): - class chain(State.Compound): - amon_din = State(initial=True) - eilenach = State() - nardol = State() - halifirien = State(final=True) - - # Eventless chain: each fires immediately - amon_din.to(eilenach) - eilenach.to(nardol) - nardol.to(halifirien) - - all_lit = State(final=True) - done_state_chain = chain.to(all_lit) - sm = await sm_runner.start(BeaconChainLighting) # The chain should cascade through all states in a single macrostep assert {"all_lit"} == set(sm.configuration_values) async def test_eventless_gradual_condition(self, sm_runner): """Multiple events needed before the condition threshold is met.""" - - class RingCorruption(StateChart): - resisting = State(initial=True) - corrupted = State(final=True) - - resisting.to(corrupted, cond="is_corrupted") - bear_ring = resisting.to.itself(internal=True, on="increase_power") - - ring_power = 0 - - def is_corrupted(self): - return self.ring_power > 5 - - def increase_power(self): - self.ring_power += 2 - - sm = await sm_runner.start(RingCorruption) + sm = await sm_runner.start(RingCorruptionWithBearRing) await sm_runner.send(sm, "bear_ring") # power = 2 assert "resisting" in sm.configuration_values @@ -113,41 +57,12 @@ def increase_power(self): async def test_eventless_in_compound_state(self, sm_runner): """Eventless transition between compound children.""" - - class AutoAdvance(StateChart): - class journey(State.Compound): - step1 = State(initial=True) - step2 = State() - step3 = State(final=True) - - step1.to(step2) - step2.to(step3) - - done = State(final=True) - done_state_journey = journey.to(done) - sm = await sm_runner.start(AutoAdvance) # Eventless chain cascades through all children assert {"done"} == set(sm.configuration_values) async def test_eventless_with_in_condition(self, sm_runner): """Eventless transition guarded by In('state_id').""" - - class CoordinatedAdvance(StateChart): - class forces(State.Parallel): - class vanguard(State.Compound): - waiting = State(initial=True) - advanced = State(final=True) - - move_forward = waiting.to(advanced) - - class rearguard(State.Compound): - holding = State(initial=True) - moved_up = State(final=True) - - # Eventless: advance only when vanguard has advanced - holding.to(moved_up, cond="In('advanced')") - sm = await sm_runner.start(CoordinatedAdvance) assert "waiting" in sm.configuration_values @@ -159,16 +74,5 @@ class rearguard(State.Compound): async def test_eventless_chain_with_final_triggers_done(self, sm_runner): """Eventless chain reaches final state -> done.state fires.""" - - class BeaconChain(StateChart): - class beacons(State.Compound): - first = State(initial=True) - last = State(final=True) - - first.to(last) - - signal_received = State(final=True) - done_state_beacons = beacons.to(signal_received) - sm = await sm_runner.start(BeaconChain) assert {"signal_received"} == set(sm.configuration_values) diff --git a/tests/test_statechart_history.py b/tests/test_statechart_history.py index 3ed35fe0..2e1d1923 100644 --- a/tests/test_statechart_history.py +++ b/tests/test_statechart_history.py @@ -9,29 +9,17 @@ import pytest -from statemachine import HistoryState -from statemachine import State -from statemachine import StateChart +from tests.machines.history.deep_memory_of_moria import DeepMemoryOfMoria +from tests.machines.history.gollum_personality import GollumPersonality +from tests.machines.history.gollum_personality_default_gollum import GollumPersonalityDefaultGollum +from tests.machines.history.gollum_personality_with_default import GollumPersonalityWithDefault +from tests.machines.history.shallow_moria import ShallowMoria @pytest.mark.timeout(5) class TestHistoryStates: async def test_shallow_history_remembers_last_child(self, sm_runner): """Exit compound, re-enter via history -> restores last active child.""" - - class GollumPersonality(StateChart): - class personality(State.Compound): - smeagol = State(initial=True) - gollum = State() - h = HistoryState() - - dark_side = smeagol.to(gollum) - light_side = gollum.to(smeagol) - - outside = State() - leave = personality.to(outside) - return_via_history = outside.to(personality.h) - sm = await sm_runner.start(GollumPersonality) await sm_runner.send(sm, "dark_side") assert "gollum" in sm.configuration_values @@ -45,21 +33,7 @@ class personality(State.Compound): async def test_shallow_history_default_on_first_visit(self, sm_runner): """No prior visit -> history uses default transition target.""" - - class GollumPersonality(StateChart): - class personality(State.Compound): - smeagol = State(initial=True) - gollum = State() - h = HistoryState() - - dark_side = smeagol.to(gollum) - _ = h.to(smeagol) # default: smeagol - - outside = State(initial=True) - enter_via_history = outside.to(personality.h) - leave = personality.to(outside) - - sm = await sm_runner.start(GollumPersonality) + sm = await sm_runner.start(GollumPersonalityWithDefault) assert {"outside"} == set(sm.configuration_values) await sm_runner.send(sm, "enter_via_history") @@ -67,24 +41,6 @@ class personality(State.Compound): async def test_deep_history_remembers_full_descendant(self, sm_runner): """Deep history restores the exact leaf in a nested compound.""" - - class DeepMemoryOfMoria(StateChart): - class moria(State.Compound): - class halls(State.Compound): - entrance = State(initial=True) - chamber = State() - - explore = entrance.to(chamber) - - assert isinstance(halls, State) - h = HistoryState(type="deep") - bridge = State(final=True) - flee = halls.to(bridge) - - outside = State() - escape = moria.to(outside) - return_deep = outside.to(moria.h) - sm = await sm_runner.start(DeepMemoryOfMoria) await sm_runner.send(sm, "explore") assert "chamber" in sm.configuration_values @@ -99,20 +55,6 @@ class halls(State.Compound): async def test_multiple_exits_and_reentries(self, sm_runner): """History updates each time we exit the compound.""" - - class GollumPersonality(StateChart): - class personality(State.Compound): - smeagol = State(initial=True) - gollum = State() - h = HistoryState() - - dark_side = smeagol.to(gollum) - light_side = gollum.to(smeagol) - - outside = State() - leave = personality.to(outside) - return_via_history = outside.to(personality.h) - sm = await sm_runner.start(GollumPersonality) await sm_runner.send(sm, "leave") await sm_runner.send(sm, "return_via_history") @@ -130,19 +72,6 @@ class personality(State.Compound): async def test_history_after_state_change(self, sm_runner): """Change state within compound, exit, re-enter -> new state restored.""" - - class GollumPersonality(StateChart): - class personality(State.Compound): - smeagol = State(initial=True) - gollum = State() - h = HistoryState() - - dark_side = smeagol.to(gollum) - - outside = State() - leave = personality.to(outside) - return_via_history = outside.to(personality.h) - sm = await sm_runner.start(GollumPersonality) await sm_runner.send(sm, "dark_side") await sm_runner.send(sm, "leave") @@ -151,24 +80,6 @@ class personality(State.Compound): async def test_shallow_only_remembers_immediate_child(self, sm_runner): """Shallow history in nested compound restores direct child, not grandchild.""" - - class ShallowMoria(StateChart): - class moria(State.Compound): - class halls(State.Compound): - entrance = State(initial=True) - chamber = State() - - explore = entrance.to(chamber) - - assert isinstance(halls, State) - h = HistoryState() - bridge = State(final=True) - flee = halls.to(bridge) - - outside = State() - escape = moria.to(outside) - return_shallow = outside.to(moria.h) - sm = await sm_runner.start(ShallowMoria) await sm_runner.send(sm, "explore") assert "chamber" in sm.configuration_values @@ -182,19 +93,6 @@ class halls(State.Compound): async def test_history_values_dict_populated(self, sm_runner): """sm.history_values[history_id] has saved states after exit.""" - - class GollumPersonality(StateChart): - class personality(State.Compound): - smeagol = State(initial=True) - gollum = State() - h = HistoryState() - - dark_side = smeagol.to(gollum) - - outside = State() - leave = personality.to(outside) - return_via_history = outside.to(personality.h) - sm = await sm_runner.start(GollumPersonality) await sm_runner.send(sm, "dark_side") await sm_runner.send(sm, "leave") @@ -205,20 +103,6 @@ class personality(State.Compound): async def test_history_with_default_transition(self, sm_runner): """HistoryState with explicit default .to() transition.""" - - class GollumPersonality(StateChart): - class personality(State.Compound): - smeagol = State(initial=True) - gollum = State() - h = HistoryState() - - dark_side = smeagol.to(gollum) - _ = h.to(gollum) # default: gollum (not the initial smeagol) - - outside = State(initial=True) - enter_via_history = outside.to(personality.h) - leave = personality.to(outside) - - sm = await sm_runner.start(GollumPersonality) + sm = await sm_runner.start(GollumPersonalityDefaultGollum) await sm_runner.send(sm, "enter_via_history") assert "gollum" in sm.configuration_values diff --git a/tests/test_statechart_in_condition.py b/tests/test_statechart_in_condition.py index 593ea6c4..f9dba574 100644 --- a/tests/test_statechart_in_condition.py +++ b/tests/test_statechart_in_condition.py @@ -9,30 +9,18 @@ import pytest -from statemachine import State -from statemachine import StateChart +from tests.machines.in_condition.combined_guard import CombinedGuard +from tests.machines.in_condition.descendant_check import DescendantCheck +from tests.machines.in_condition.eventless_in import EventlessIn +from tests.machines.in_condition.fellowship import Fellowship +from tests.machines.in_condition.fellowship_coordination import FellowshipCoordination +from tests.machines.in_condition.gate_of_moria import GateOfMoria @pytest.mark.timeout(5) class TestInCondition: async def test_in_condition_true_enables_transition(self, sm_runner): """In('state_id') when state is active -> transition fires.""" - - class Fellowship(StateChart): - class positions(State.Parallel): - class frodo(State.Compound): - shire_f = State(initial=True) - mordor_f = State(final=True) - - journey = shire_f.to(mordor_f) - - class sam(State.Compound): - shire_s = State(initial=True) - mordor_s = State(final=True) - - # Sam follows Frodo: eventless, guarded by In('mordor_f') - shire_s.to(mordor_s, cond="In('mordor_f')") - sm = await sm_runner.start(Fellowship) await sm_runner.send(sm, "journey") vals = set(sm.configuration_values) @@ -41,39 +29,12 @@ class sam(State.Compound): async def test_in_condition_false_blocks_transition(self, sm_runner): """In('state_id') when state is not active -> transition blocked.""" - - class GateOfMoria(StateChart): - outside = State(initial=True) - at_gate = State() - inside = State(final=True) - - approach = outside.to(at_gate) - # Can only enter if we are at the gate - enter_gate = outside.to(inside, cond="In('at_gate')") - speak_friend = at_gate.to(inside) - sm = await sm_runner.start(GateOfMoria) await sm_runner.send(sm, "enter_gate") assert "outside" in sm.configuration_values async def test_in_with_parallel_regions(self, sm_runner): """Cross-region In() evaluation in parallel states.""" - - class FellowshipCoordination(StateChart): - class mission(State.Parallel): - class scouts(State.Compound): - scouting = State(initial=True) - reported = State(final=True) - - report = scouting.to(reported) - - class army(State.Compound): - waiting = State(initial=True) - marching = State(final=True) - - # Army marches only after scouts report - waiting.to(marching, cond="In('reported')") - sm = await sm_runner.start(FellowshipCoordination) vals = set(sm.configuration_values) assert "waiting" in vals @@ -86,19 +47,6 @@ class army(State.Compound): async def test_in_with_compound_descendant(self, sm_runner): """In('child') when child is an active descendant.""" - - class DescendantCheck(StateChart): - class realm(State.Compound): - village = State(initial=True) - castle = State() - - ascend = village.to(castle) - - conquered = State(final=True) - # Guarded by being inside the castle - conquer = realm.to(conquered, cond="In('castle')") - explore = realm.to.itself(internal=True) - sm = await sm_runner.start(DescendantCheck) await sm_runner.send(sm, "conquer") assert "realm" in sm.configuration_values @@ -111,22 +59,6 @@ class realm(State.Compound): async def test_in_combined_with_event(self, sm_runner): """Event + In() guard together.""" - - class CombinedGuard(StateChart): - class positions(State.Parallel): - class scout(State.Compound): - out = State(initial=True) - back = State(final=True) - - return_scout = out.to(back) - - class warrior(State.Compound): - idle = State(initial=True) - attacking = State(final=True) - - # Only attacks when scout is back - charge = idle.to(attacking, cond="In('back')") - sm = await sm_runner.start(CombinedGuard) await sm_runner.send(sm, "charge") assert "idle" in sm.configuration_values @@ -137,22 +69,6 @@ class warrior(State.Compound): async def test_in_with_eventless_transition(self, sm_runner): """Eventless + In() guard.""" - - class EventlessIn(StateChart): - class coordination(State.Parallel): - class leader(State.Compound): - planning = State(initial=True) - ready = State(final=True) - - get_ready = planning.to(ready) - - class follower(State.Compound): - waiting = State(initial=True) - moving = State(final=True) - - # Eventless: move when leader is ready - waiting.to(moving, cond="In('ready')") - sm = await sm_runner.start(EventlessIn) assert "waiting" in sm.configuration_values diff --git a/tests/test_statechart_parallel.py b/tests/test_statechart_parallel.py index 6e87d42e..619d1b36 100644 --- a/tests/test_statechart_parallel.py +++ b/tests/test_statechart_parallel.py @@ -9,41 +9,18 @@ import pytest -from statemachine import State -from statemachine import StateChart +from tests.machines.parallel.session import Session +from tests.machines.parallel.session_with_done_state import SessionWithDoneState +from tests.machines.parallel.two_towers import TwoTowers +from tests.machines.parallel.war_of_the_ring import WarOfTheRing +from tests.machines.parallel.war_with_exit import WarWithExit @pytest.mark.timeout(5) class TestParallelStates: - @pytest.fixture() - def war_of_the_ring_cls(self): - class WarOfTheRing(StateChart): - class war(State.Parallel): - class frodos_quest(State.Compound): - shire = State(initial=True) - mordor = State() - mount_doom = State(final=True) - - journey = shire.to(mordor) - destroy_ring = mordor.to(mount_doom) - - class aragorns_path(State.Compound): - ranger = State(initial=True) - king = State(final=True) - - coronation = ranger.to(king) - - class gandalfs_defense(State.Compound): - rohan = State(initial=True) - gondor = State(final=True) - - ride_to_gondor = rohan.to(gondor) - - return WarOfTheRing - - async def test_parallel_activates_all_regions(self, sm_runner, war_of_the_ring_cls): + async def test_parallel_activates_all_regions(self, sm_runner): """Entering a parallel state activates the initial child of every region.""" - sm = await sm_runner.start(war_of_the_ring_cls) + sm = await sm_runner.start(WarOfTheRing) vals = set(sm.configuration_values) assert "war" in vals assert "frodos_quest" in vals @@ -53,18 +30,18 @@ async def test_parallel_activates_all_regions(self, sm_runner, war_of_the_ring_c assert "gandalfs_defense" in vals assert "rohan" in vals - async def test_independent_transitions_in_regions(self, sm_runner, war_of_the_ring_cls): + async def test_independent_transitions_in_regions(self, sm_runner): """An event in one region does not affect others.""" - sm = await sm_runner.start(war_of_the_ring_cls) + sm = await sm_runner.start(WarOfTheRing) await sm_runner.send(sm, "journey") vals = set(sm.configuration_values) assert "mordor" in vals assert "ranger" in vals # unchanged assert "rohan" in vals # unchanged - async def test_configuration_includes_all_active_states(self, sm_runner, war_of_the_ring_cls): + async def test_configuration_includes_all_active_states(self, sm_runner): """Configuration set includes all active states across regions.""" - sm = await sm_runner.start(war_of_the_ring_cls) + sm = await sm_runner.start(WarOfTheRing) config_ids = {s.id for s in sm.configuration} assert config_ids == { "war", @@ -78,48 +55,30 @@ async def test_configuration_includes_all_active_states(self, sm_runner, war_of_ async def test_exit_parallel_exits_all_regions(self, sm_runner): """Transition out of a parallel clears everything.""" - - class WarWithExit(StateChart): - class war(State.Parallel): - class front_a(State.Compound): - fighting = State(initial=True) - won = State(final=True) - - win_a = fighting.to(won) - - class front_b(State.Compound): - holding = State(initial=True) - held = State(final=True) - - hold_b = holding.to(held) - - peace = State(final=True) - truce = war.to(peace) - sm = await sm_runner.start(WarWithExit) assert "war" in sm.configuration_values await sm_runner.send(sm, "truce") assert {"peace"} == set(sm.configuration_values) - async def test_event_in_one_region_no_effect_on_others(self, sm_runner, war_of_the_ring_cls): + async def test_event_in_one_region_no_effect_on_others(self, sm_runner): """Region isolation: events affect only the targeted region.""" - sm = await sm_runner.start(war_of_the_ring_cls) + sm = await sm_runner.start(WarOfTheRing) await sm_runner.send(sm, "coronation") vals = set(sm.configuration_values) assert "king" in vals assert "shire" in vals # Frodo's region unchanged assert "rohan" in vals # Gandalf's region unchanged - async def test_parallel_with_compound_children(self, sm_runner, war_of_the_ring_cls): + async def test_parallel_with_compound_children(self, sm_runner): """Mixed hierarchy: parallel with compound regions verified.""" - sm = await sm_runner.start(war_of_the_ring_cls) + sm = await sm_runner.start(WarOfTheRing) assert "shire" in sm.configuration_values assert "ranger" in sm.configuration_values assert "rohan" in sm.configuration_values - async def test_current_state_value_set_comparison(self, sm_runner, war_of_the_ring_cls): + async def test_current_state_value_set_comparison(self, sm_runner): """configuration_values supports set comparison for parallel states.""" - sm = await sm_runner.start(war_of_the_ring_cls) + sm = await sm_runner.start(WarOfTheRing) vals = set(sm.configuration_values) expected = { "war", @@ -134,24 +93,6 @@ async def test_current_state_value_set_comparison(self, sm_runner, war_of_the_ri async def test_parallel_done_when_all_regions_final(self, sm_runner): """done.state fires when ALL regions reach a final state.""" - - class TwoTowers(StateChart): - class battle(State.Parallel): - class helms_deep(State.Compound): - fighting = State(initial=True) - victory = State(final=True) - - win = fighting.to(victory) - - class isengard(State.Compound): - besieging = State(initial=True) - flooded = State(final=True) - - flood = besieging.to(flooded) - - aftermath = State(final=True) - done_state_battle = battle.to(aftermath) - sm = await sm_runner.start(TwoTowers) await sm_runner.send(sm, "win") # Only one region is final, battle continues @@ -163,35 +104,15 @@ class isengard(State.Compound): async def test_parallel_not_done_when_one_region_final(self, sm_runner): """Parallel not done when only one region reaches final.""" - - class TwoTowers(StateChart): - class battle(State.Parallel): - class helms_deep(State.Compound): - fighting = State(initial=True) - victory = State(final=True) - - win = fighting.to(victory) - - class isengard(State.Compound): - besieging = State(initial=True) - flooded = State(final=True) - - flood = besieging.to(flooded) - - aftermath = State(final=True) - done_state_battle = battle.to(aftermath) - sm = await sm_runner.start(TwoTowers) await sm_runner.send(sm, "win") assert "battle" in sm.configuration_values assert "victory" in sm.configuration_values assert "besieging" in sm.configuration_values - async def test_transition_within_compound_inside_parallel( - self, sm_runner, war_of_the_ring_cls - ): + async def test_transition_within_compound_inside_parallel(self, sm_runner): """Deep transition within a compound region of a parallel state.""" - sm = await sm_runner.start(war_of_the_ring_cls) + sm = await sm_runner.start(WarOfTheRing) await sm_runner.send(sm, "journey") await sm_runner.send(sm, "destroy_ring") vals = set(sm.configuration_values) @@ -200,21 +121,6 @@ async def test_transition_within_compound_inside_parallel( async def test_top_level_parallel_terminates_when_all_children_final(self, sm_runner): """A root parallel terminates when all regions reach final states.""" - - class Session(StateChart): - class session(State.Parallel): - class ui(State.Compound): - active = State(initial=True) - closed = State(final=True) - - close_ui = active.to(closed) - - class backend(State.Compound): - running = State(initial=True) - stopped = State(final=True) - - stop_backend = running.to(stopped) - sm = await sm_runner.start(Session) assert sm.is_terminated is False @@ -226,25 +132,7 @@ class backend(State.Compound): async def test_top_level_parallel_done_state_fires_before_termination(self, sm_runner): """done.state fires and transitions before root-final check terminates.""" - - class Session(StateChart): - class session(State.Parallel): - class ui(State.Compound): - active = State(initial=True) - closed = State(final=True) - - close_ui = active.to(closed) - - class backend(State.Compound): - running = State(initial=True) - stopped = State(final=True) - - stop_backend = running.to(stopped) - - finished = State(final=True) - done_state_session = session.to(finished) - - sm = await sm_runner.start(Session) + sm = await sm_runner.start(SessionWithDoneState) await sm_runner.send(sm, "close_ui") await sm_runner.send(sm, "stop_backend") # done.state.session fires, transitions to finished, then terminates @@ -253,21 +141,6 @@ class backend(State.Compound): async def test_top_level_parallel_not_terminated_when_one_region_pending(self, sm_runner): """Machine keeps running when only one region reaches final.""" - - class Session(StateChart): - class session(State.Parallel): - class ui(State.Compound): - active = State(initial=True) - closed = State(final=True) - - close_ui = active.to(closed) - - class backend(State.Compound): - running = State(initial=True) - stopped = State(final=True) - - stop_backend = running.to(stopped) - sm = await sm_runner.start(Session) await sm_runner.send(sm, "close_ui") assert sm.is_terminated is False diff --git a/tests/test_statemachine.py b/tests/test_statemachine.py index 5bc7f85c..1946a083 100644 --- a/tests/test_statemachine.py +++ b/tests/test_statemachine.py @@ -27,9 +27,9 @@ def test_machine_should_be_at_start_state(campaign_machine): "closed", ] assert [t.name for t in campaign_machine.events] == [ - "add_job", - "produce", - "deliver", + "Add job", + "Produce", + "Deliver", ] assert model.state == "draft" @@ -160,11 +160,11 @@ def test_machine_should_list_allowed_events_in_the_current_state(campaign_machin machine = campaign_machine(model) assert model.state == "draft" - assert [t.name for t in machine.allowed_events] == ["add_job", "produce"] + assert [t.name for t in machine.allowed_events] == ["Add job", "Produce"] machine.produce() assert model.state == "producing" - assert [t.name for t in machine.allowed_events] == ["add_job", "deliver"] + assert [t.name for t in machine.allowed_events] == ["Add job", "Deliver"] deliver = machine.allowed_events[1] diff --git a/tests/test_statemachine_compat.py b/tests/test_statemachine_compat.py index edfda0b1..b163886e 100644 --- a/tests/test_statemachine_compat.py +++ b/tests/test_statemachine_compat.py @@ -356,19 +356,3 @@ class SM(StateMachine): sm = SM() with pytest.warns(DeprecationWarning, match="current_state"): _ = sm.current_state # noqa: F841 - - def test_current_state_with_list_value(self): - """current_state handles list current_state_value (backward compat).""" - - class SM(StateMachine): - s1 = State(initial=True) - s2 = State(final=True) - - go = s1.to(s2) - - sm = SM() - setattr(sm.model, sm.state_field, [sm.s1.value]) - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - config = sm.current_state - assert sm.s1 in config diff --git a/tests/test_threading.py b/tests/test_threading.py index 5f6721a7..b2d305e8 100644 --- a/tests/test_threading.py +++ b/tests/test_threading.py @@ -1,6 +1,8 @@ import threading import time +from collections import Counter +import pytest from statemachine.state import State from statemachine.statemachine import StateChart @@ -115,6 +117,184 @@ def __init__(self, name): assert c3.fsm.statuses_history == ["c3.green", "c3.green", "c3.green", "c3.yellow"] +class TestThreadSafety: + """Stress tests for concurrent access to a single state machine instance. + + These tests exercise real contention: multiple threads sending events to the + same SM simultaneously, synchronized via barriers to maximize overlap. + """ + + @pytest.fixture() + def cycling_machine(self): + class CyclingMachine(StateChart): + s1 = State(initial=True) + s2 = State() + s3 = State() + cycle = s1.to(s2) | s2.to(s3) | s3.to(s1) + + return CyclingMachine() + + @pytest.mark.parametrize("num_threads", [4, 8]) + def test_concurrent_sends_no_lost_events(self, cycling_machine, num_threads): + """All events sent concurrently must be processed — none lost.""" + events_per_thread = 300 + total_events = num_threads * events_per_thread + barrier = threading.Barrier(num_threads) + errors = [] + + def sender(): + try: + barrier.wait(timeout=5) + for _ in range(events_per_thread): + cycling_machine.send("cycle") + except Exception as e: + errors.append(e) + + threads = [threading.Thread(target=sender) for _ in range(num_threads)] + for t in threads: + t.start() + for t in threads: + t.join(timeout=30) + + assert not errors, f"Thread errors: {errors}" + + # The machine cycles s1→s2→s3→s1. After N total cycle events starting + # from s1, the state is determined by (N % 3). + expected_states = {0: "s1", 1: "s2", 2: "s3"} + expected = expected_states[total_events % 3] + assert cycling_machine.current_state_value == expected + + def test_concurrent_sends_state_consistency(self, cycling_machine): + """State must always be one of the valid states, never corrupted.""" + valid_values = {"s1", "s2", "s3"} + num_threads = 6 + events_per_thread = 500 + barrier = threading.Barrier(num_threads + 1) # +1 for observer + stop_event = threading.Event() + observed_values = [] + errors = [] + + def sender(): + try: + barrier.wait(timeout=5) + for _ in range(events_per_thread): + cycling_machine.send("cycle") + except Exception as e: + errors.append(e) + + def observer(): + barrier.wait(timeout=5) + while not stop_event.is_set(): + val = cycling_machine.current_state_value + observed_values.append(val) + + threads = [threading.Thread(target=sender) for _ in range(num_threads)] + obs_thread = threading.Thread(target=observer) + + for t in threads: + t.start() + obs_thread.start() + + for t in threads: + t.join(timeout=30) + + stop_event.set() + obs_thread.join(timeout=5) + + assert not errors, f"Thread errors: {errors}" + # None may appear transiently during configuration updates — that's expected. + invalid = [v for v in observed_values if v not in valid_values and v is not None] + assert not invalid, f"Observed invalid state values: {set(invalid)}" + assert len(observed_values) > 100, "Observer didn't collect enough samples" + + def test_concurrent_sends_with_callbacks(self): + """Callbacks must execute exactly once per transition under contention.""" + call_log = [] + lock = threading.Lock() + + class CallbackMachine(StateChart): + s1 = State(initial=True) + s2 = State() + go = s1.to(s2) | s2.to(s1) + + def on_enter_s2(self): + with lock: + call_log.append("enter_s2") + + def on_enter_s1(self): + with lock: + call_log.append("enter_s1") + + sm = CallbackMachine() + num_threads = 4 + events_per_thread = 200 + total_events = num_threads * events_per_thread + barrier = threading.Barrier(num_threads) + errors = [] + + def sender(): + try: + barrier.wait(timeout=5) + for _ in range(events_per_thread): + sm.send("go") + except Exception as e: + errors.append(e) + + threads = [threading.Thread(target=sender) for _ in range(num_threads)] + for t in threads: + t.start() + for t in threads: + t.join(timeout=30) + + assert not errors, f"Thread errors: {errors}" + + # Each transition fires exactly one on_enter callback. + # +1 because initial activation also fires on_enter_s1. + counts = Counter(call_log) + total_callbacks = counts["enter_s1"] + counts["enter_s2"] + assert total_callbacks == total_events + 1 + + def test_concurrent_send_and_read_configuration(self, cycling_machine): + """Reading configuration while events are being processed must not raise.""" + num_senders = 4 + events_per_sender = 300 + barrier = threading.Barrier(num_senders + 1) + stop_event = threading.Event() + errors = [] + + def sender(): + try: + barrier.wait(timeout=5) + for _ in range(events_per_sender): + cycling_machine.send("cycle") + except Exception as e: + errors.append(e) + + def reader(): + barrier.wait(timeout=5) + while not stop_event.is_set(): + try: + _ = cycling_machine.configuration + _ = cycling_machine.current_state_value + _ = list(cycling_machine.configuration) + except Exception as e: + errors.append(e) + + threads = [threading.Thread(target=sender) for _ in range(num_senders)] + reader_thread = threading.Thread(target=reader) + + for t in threads: + t.start() + reader_thread.start() + + for t in threads: + t.join(timeout=30) + stop_event.set() + reader_thread.join(timeout=5) + + assert not errors, f"Thread errors: {errors}" + + async def test_regression_443_with_modifications_for_async_engine(): """ Test for https://github.com/fgmacedo/python-statemachine/issues/443 diff --git a/tests/test_transition_table.py b/tests/test_transition_table.py new file mode 100644 index 00000000..fadf363e --- /dev/null +++ b/tests/test_transition_table.py @@ -0,0 +1,201 @@ +from statemachine.contrib.diagram.extract import extract +from statemachine.contrib.diagram.model import DiagramGraph +from statemachine.contrib.diagram.model import DiagramState +from statemachine.contrib.diagram.model import DiagramTransition +from statemachine.contrib.diagram.model import StateType +from statemachine.contrib.diagram.renderers.table import TransitionTableRenderer + +from statemachine import State +from statemachine import StateChart + + +class TestTransitionTableMarkdown: + """Markdown transition table tests.""" + + def test_simple_table(self): + graph = DiagramGraph( + name="Simple", + states=[ + DiagramState(id="s1", name="S1", type=StateType.REGULAR, is_initial=True), + DiagramState(id="s2", name="S2", type=StateType.REGULAR), + ], + transitions=[ + DiagramTransition(source="s1", targets=["s2"], event="go"), + ], + ) + result = TransitionTableRenderer().render(graph, fmt="md") + assert "| State" in result + assert "| Event" in result + assert "| Guard" in result + assert "| Target" in result + assert "| S1" in result + assert "go" in result + assert "| S2" in result + + def test_with_guards(self): + graph = DiagramGraph( + name="Guards", + states=[ + DiagramState(id="s1", name="S1", type=StateType.REGULAR, is_initial=True), + DiagramState(id="s2", name="S2", type=StateType.REGULAR), + ], + transitions=[ + DiagramTransition(source="s1", targets=["s2"], event="go", guards=["is_ready"]), + ], + ) + result = TransitionTableRenderer().render(graph, fmt="md") + assert "is_ready" in result + + def test_multiple_targets(self): + graph = DiagramGraph( + name="Multi", + states=[ + DiagramState(id="s1", name="S1", type=StateType.REGULAR, is_initial=True), + DiagramState(id="s2", name="S2", type=StateType.REGULAR), + DiagramState(id="s3", name="S3", type=StateType.REGULAR), + ], + transitions=[ + DiagramTransition(source="s1", targets=["s2", "s3"], event="split"), + ], + ) + result = TransitionTableRenderer().render(graph, fmt="md") + lines = result.strip().split("\n") + # Header + separator + 2 data rows + assert len(lines) == 4 + + def test_skips_initial_transitions(self): + graph = DiagramGraph( + name="SkipInit", + states=[ + DiagramState(id="s1", name="S1", type=StateType.REGULAR, is_initial=True), + DiagramState(id="s2", name="S2", type=StateType.REGULAR), + ], + transitions=[ + DiagramTransition(source="s1", targets=["s2"], event="", is_initial=True), + DiagramTransition(source="s1", targets=["s2"], event="go"), + ], + ) + result = TransitionTableRenderer().render(graph, fmt="md") + lines = result.strip().split("\n") + # Header + separator + 1 data row (initial skipped) + assert len(lines) == 3 + + def test_skips_internal_transitions(self): + graph = DiagramGraph( + name="SkipInternal", + states=[ + DiagramState(id="s1", name="S1", type=StateType.REGULAR, is_initial=True), + ], + transitions=[ + DiagramTransition(source="s1", targets=["s1"], event="check", is_internal=True), + ], + ) + result = TransitionTableRenderer().render(graph, fmt="md") + lines = result.strip().split("\n") + # Header + separator only (no data rows) + assert len(lines) == 2 + + def test_targetless_transition(self): + graph = DiagramGraph( + name="Targetless", + states=[ + DiagramState(id="s1", name="S1", type=StateType.REGULAR, is_initial=True), + ], + transitions=[ + DiagramTransition(source="s1", targets=[], event="tick"), + ], + ) + result = TransitionTableRenderer().render(graph, fmt="md") + assert "tick" in result + # Target falls back to source name + assert "S1" in result + + +class TestTransitionTableRST: + """RST grid table tests.""" + + def test_rst_format(self): + graph = DiagramGraph( + name="RST", + states=[ + DiagramState(id="s1", name="S1", type=StateType.REGULAR, is_initial=True), + DiagramState(id="s2", name="S2", type=StateType.REGULAR), + ], + transitions=[ + DiagramTransition(source="s1", targets=["s2"], event="go"), + ], + ) + result = TransitionTableRenderer().render(graph, fmt="rst") + assert "+---" in result + assert "|" in result + assert "====" in result # header separator + assert "go" in result + + def test_rst_with_guards(self): + graph = DiagramGraph( + name="RSTGuards", + states=[ + DiagramState(id="s1", name="S1", type=StateType.REGULAR, is_initial=True), + DiagramState(id="s2", name="S2", type=StateType.REGULAR), + ], + transitions=[ + DiagramTransition(source="s1", targets=["s2"], event="go", guards=["is_ready"]), + ], + ) + result = TransitionTableRenderer().render(graph, fmt="rst") + assert "is_ready" in result + + +class TestTransitionTableIntegration: + """Integration tests with real state machines.""" + + def test_traffic_light_md(self): + from tests.examples.traffic_light_machine import TrafficLightMachine + + ir = extract(TrafficLightMachine) + result = TransitionTableRenderer().render(ir, fmt="md") + assert "Green" in result + assert "Yellow" in result + assert "Red" in result + assert "Cycle" in result + + def test_traffic_light_rst(self): + from tests.examples.traffic_light_machine import TrafficLightMachine + + ir = extract(TrafficLightMachine) + result = TransitionTableRenderer().render(ir, fmt="rst") + assert "Green" in result + assert "Cycle" in result + assert "+---" in result + + def test_compound_state_names(self): + """Child state names are properly resolved.""" + + class SM(StateChart): + class parent(State.Compound, name="Parent"): + child1 = State(initial=True) + child2 = State(final=True) + go = child1.to(child2) + + start = State(initial=True) + enter = start.to(parent) + + ir = extract(SM) + result = TransitionTableRenderer().render(ir, fmt="md") + assert "Child1" in result + assert "Child2" in result + + def test_default_format_is_md(self): + """render() without fmt defaults to markdown.""" + graph = DiagramGraph( + name="Default", + states=[ + DiagramState(id="s1", name="S1", type=StateType.REGULAR, is_initial=True), + DiagramState(id="s2", name="S2", type=StateType.REGULAR), + ], + transitions=[ + DiagramTransition(source="s1", targets=["s2"], event="go"), + ], + ) + result = TransitionTableRenderer().render(graph) + assert "| State" in result # markdown uses pipes diff --git a/tests/test_transitions.py b/tests/test_transitions.py index 4f4f47e4..b71497f0 100644 --- a/tests/test_transitions.py +++ b/tests/test_transitions.py @@ -19,7 +19,7 @@ def test_transition_representation(campaign_machine): def test_list_machine_events(classic_traffic_light_machine): machine = classic_traffic_light_machine() transitions = [t.name for t in machine.events] - assert transitions == ["slowdown", "stop", "go"] + assert transitions == ["Slowdown", "Stop", "Go"] def test_list_state_transitions(classic_traffic_light_machine): diff --git a/tests/test_validators.py b/tests/test_validators.py index 2910538b..ca07afcd 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -12,122 +12,12 @@ import pytest -from statemachine import State -from statemachine import StateChart - -# --------------------------------------------------------------------------- -# State machine definitions used across tests -# --------------------------------------------------------------------------- - - -class OrderValidation(StateChart): - """StateChart with catch_errors_as_events=True (the default).""" - - pending = State(initial=True) - confirmed = State() - cancelled = State(final=True) - - confirm = pending.to(confirmed, validators="check_stock") - cancel = confirmed.to(cancelled) - - def check_stock(self, quantity=0, **kwargs): - if quantity <= 0: - raise ValueError("Quantity must be positive") - - -class OrderValidationNoErrorEvents(StateChart): - """Same machine but with catch_errors_as_events=False.""" - - catch_errors_as_events = False - - pending = State(initial=True) - confirmed = State() - cancelled = State(final=True) - - confirm = pending.to(confirmed, validators="check_stock") - cancel = confirmed.to(cancelled) - - def check_stock(self, quantity=0, **kwargs): - if quantity <= 0: - raise ValueError("Quantity must be positive") - - -class MultiValidator(StateChart): - """Machine with multiple validators — first failure stops the chain.""" - - idle = State(initial=True) - active = State(final=True) - - start = idle.to(active, validators=["check_a", "check_b"]) - - def check_a(self, **kwargs): - if not kwargs.get("a_ok"): - raise ValueError("A failed") - - def check_b(self, **kwargs): - if not kwargs.get("b_ok"): - raise ValueError("B failed") - - -class ValidatorWithCond(StateChart): - """Machine that combines validators and conditions on the same transition.""" - - idle = State(initial=True) - active = State(final=True) - - start = idle.to(active, validators="check_auth", cond="has_permission") - - has_permission = False - - def check_auth(self, token=None, **kwargs): - if token != "valid": - raise PermissionError("Invalid token") - - -class ValidatorWithErrorTransition(StateChart): - """Machine with both a validator and an error.execution transition. - - The error.execution transition should NOT be triggered by validator - rejection — only by actual execution errors in actions. - """ - - idle = State(initial=True) - active = State() - error_state = State(final=True) - - start = idle.to(active, validators="check_input") - do_work = active.to.itself(on="risky_action") - error_execution = active.to(error_state) - - def check_input(self, value=None, **kwargs): - if value is None: - raise ValueError("Input required") - - def risky_action(self, **kwargs): - raise RuntimeError("Boom") - - -class ValidatorFallthrough(StateChart): - """Machine with multiple transitions for the same event. - - When the first transition's validator rejects, the exception propagates - immediately — the engine does NOT fall through to the next transition. - """ - - idle = State(initial=True) - path_a = State(final=True) - path_b = State(final=True) - - go = idle.to(path_a, validators="must_be_premium") | idle.to(path_b) - - def must_be_premium(self, **kwargs): - if not kwargs.get("premium"): - raise PermissionError("Premium required") - - -# --------------------------------------------------------------------------- -# Tests -# --------------------------------------------------------------------------- +from tests.machines.validators.multi_validator import MultiValidator +from tests.machines.validators.order_validation import OrderValidation +from tests.machines.validators.order_validation_no_error_events import OrderValidationNoErrorEvents +from tests.machines.validators.validator_fallthrough import ValidatorFallthrough +from tests.machines.validators.validator_with_cond import ValidatorWithCond +from tests.machines.validators.validator_with_error_transition import ValidatorWithErrorTransition class TestValidatorPropagation: diff --git a/uv.lock b/uv.lock index 496ec66d..8ccdb666 100644 --- a/uv.lock +++ b/uv.lock @@ -1,4 +1,5 @@ version = 1 +revision = 3 requires-python = ">=3.9" resolution-markers = [ "python_full_version >= '3.10'", @@ -9,9 +10,9 @@ resolution-markers = [ name = "alabaster" version = "0.7.16" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c9/3e/13dd8e5ed9094e734ac430b5d0eb4f2bb001708a8b7856cbf8e084e001ba/alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65", size = 23776 } +sdist = { url = "https://files.pythonhosted.org/packages/c9/3e/13dd8e5ed9094e734ac430b5d0eb4f2bb001708a8b7856cbf8e084e001ba/alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65", size = 23776, upload-time = "2024-01-10T00:56:10.189Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/34/d4e1c02d3bee589efb5dfa17f88ea08bdb3e3eac12bc475462aec52ed223/alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92", size = 13511 }, + { url = "https://files.pythonhosted.org/packages/32/34/d4e1c02d3bee589efb5dfa17f88ea08bdb3e3eac12bc475462aec52ed223/alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92", size = 13511, upload-time = "2024-01-10T00:56:08.388Z" }, ] [[package]] @@ -24,9 +25,9 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9f/09/45b9b7a6d4e45c6bcb5bf61d19e3ab87df68e0601fa8c5293de3542546cc/anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c", size = 173422 } +sdist = { url = "https://files.pythonhosted.org/packages/9f/09/45b9b7a6d4e45c6bcb5bf61d19e3ab87df68e0601fa8c5293de3542546cc/anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c", size = 173422, upload-time = "2024-10-14T14:31:44.021Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/f5/f2b75d2fc6f1a260f340f0e7c6a060f4dd2961cc16884ed851b0d18da06a/anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d", size = 90377 }, + { url = "https://files.pythonhosted.org/packages/e4/f5/f2b75d2fc6f1a260f340f0e7c6a060f4dd2961cc16884ed851b0d18da06a/anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d", size = 90377, upload-time = "2024-10-14T14:31:42.623Z" }, ] [[package]] @@ -36,27 +37,27 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/29/38/b3395cc9ad1b56d2ddac9970bc8f4141312dbaec28bc7c218b0dfafd0f42/asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590", size = 35186 } +sdist = { url = "https://files.pythonhosted.org/packages/29/38/b3395cc9ad1b56d2ddac9970bc8f4141312dbaec28bc7c218b0dfafd0f42/asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590", size = 35186, upload-time = "2024-03-22T14:39:36.863Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", size = 23828 }, + { url = "https://files.pythonhosted.org/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", size = 23828, upload-time = "2024-03-22T14:39:34.521Z" }, ] [[package]] name = "babel" version = "2.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2a/74/f1bc80f23eeba13393b7222b11d95ca3af2c1e28edca18af487137eefed9/babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316", size = 9348104 } +sdist = { url = "https://files.pythonhosted.org/packages/2a/74/f1bc80f23eeba13393b7222b11d95ca3af2c1e28edca18af487137eefed9/babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316", size = 9348104, upload-time = "2024-08-08T14:25:45.459Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/20/bc79bc575ba2e2a7f70e8a1155618bb1301eaa5132a8271373a6903f73f8/babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b", size = 9587599 }, + { url = "https://files.pythonhosted.org/packages/ed/20/bc79bc575ba2e2a7f70e8a1155618bb1301eaa5132a8271373a6903f73f8/babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b", size = 9587599, upload-time = "2024-08-08T14:25:42.686Z" }, ] [[package]] name = "backports-asyncio-runner" version = "1.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893 } +sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313 }, + { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, ] [[package]] @@ -66,111 +67,111 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "soupsieve" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b3/ca/824b1195773ce6166d388573fc106ce56d4a805bd7427b624e063596ec58/beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051", size = 581181 } +sdist = { url = "https://files.pythonhosted.org/packages/b3/ca/824b1195773ce6166d388573fc106ce56d4a805bd7427b624e063596ec58/beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051", size = 581181, upload-time = "2024-01-17T16:53:17.902Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/fe/e8c672695b37eecc5cbf43e1d0638d88d66ba3a44c4d321c796f4e59167f/beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed", size = 147925 }, + { url = "https://files.pythonhosted.org/packages/b1/fe/e8c672695b37eecc5cbf43e1d0638d88d66ba3a44c4d321c796f4e59167f/beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed", size = 147925, upload-time = "2024-01-17T16:53:12.779Z" }, ] [[package]] name = "certifi" version = "2024.8.30" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 } +sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507, upload-time = "2024-08-30T01:55:04.365Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 }, + { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321, upload-time = "2024-08-30T01:55:02.591Z" }, ] [[package]] name = "cfgv" version = "3.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c4/bf/d0d622b660d414a47dc7f0d303791a627663f554345b21250e39e7acb48b/cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736", size = 7864 } +sdist = { url = "https://files.pythonhosted.org/packages/c4/bf/d0d622b660d414a47dc7f0d303791a627663f554345b21250e39e7acb48b/cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736", size = 7864, upload-time = "2021-08-25T14:18:49.905Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/82/0a0ebd35bae9981dea55c06f8e6aaf44a49171ad798795c72c6f64cba4c2/cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426", size = 7312 }, + { url = "https://files.pythonhosted.org/packages/6d/82/0a0ebd35bae9981dea55c06f8e6aaf44a49171ad798795c72c6f64cba4c2/cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426", size = 7312, upload-time = "2021-08-25T14:18:47.77Z" }, ] [[package]] name = "charset-normalizer" version = "3.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/8b/825cc84cf13a28bfbcba7c416ec22bf85a9584971be15b21dd8300c65b7f/charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6", size = 196363 }, - { url = "https://files.pythonhosted.org/packages/23/81/d7eef6a99e42c77f444fdd7bc894b0ceca6c3a95c51239e74a722039521c/charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b", size = 125639 }, - { url = "https://files.pythonhosted.org/packages/21/67/b4564d81f48042f520c948abac7079356e94b30cb8ffb22e747532cf469d/charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99", size = 120451 }, - { url = "https://files.pythonhosted.org/packages/c2/72/12a7f0943dd71fb5b4e7b55c41327ac0a1663046a868ee4d0d8e9c369b85/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca", size = 140041 }, - { url = "https://files.pythonhosted.org/packages/67/56/fa28c2c3e31217c4c52158537a2cf5d98a6c1e89d31faf476c89391cd16b/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d", size = 150333 }, - { url = "https://files.pythonhosted.org/packages/f9/d2/466a9be1f32d89eb1554cf84073a5ed9262047acee1ab39cbaefc19635d2/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7", size = 142921 }, - { url = "https://files.pythonhosted.org/packages/f8/01/344ec40cf5d85c1da3c1f57566c59e0c9b56bcc5566c08804a95a6cc8257/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3", size = 144785 }, - { url = "https://files.pythonhosted.org/packages/73/8b/2102692cb6d7e9f03b9a33a710e0164cadfce312872e3efc7cfe22ed26b4/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907", size = 146631 }, - { url = "https://files.pythonhosted.org/packages/d8/96/cc2c1b5d994119ce9f088a9a0c3ebd489d360a2eb058e2c8049f27092847/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b", size = 140867 }, - { url = "https://files.pythonhosted.org/packages/c9/27/cde291783715b8ec30a61c810d0120411844bc4c23b50189b81188b273db/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912", size = 149273 }, - { url = "https://files.pythonhosted.org/packages/3a/a4/8633b0fc1a2d1834d5393dafecce4a1cc56727bfd82b4dc18fc92f0d3cc3/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95", size = 152437 }, - { url = "https://files.pythonhosted.org/packages/64/ea/69af161062166b5975ccbb0961fd2384853190c70786f288684490913bf5/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e", size = 150087 }, - { url = "https://files.pythonhosted.org/packages/3b/fd/e60a9d9fd967f4ad5a92810138192f825d77b4fa2a557990fd575a47695b/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe", size = 145142 }, - { url = "https://files.pythonhosted.org/packages/6d/02/8cb0988a1e49ac9ce2eed1e07b77ff118f2923e9ebd0ede41ba85f2dcb04/charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc", size = 94701 }, - { url = "https://files.pythonhosted.org/packages/d6/20/f1d4670a8a723c46be695dff449d86d6092916f9e99c53051954ee33a1bc/charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749", size = 102191 }, - { url = "https://files.pythonhosted.org/packages/9c/61/73589dcc7a719582bf56aae309b6103d2762b526bffe189d635a7fcfd998/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", size = 193339 }, - { url = "https://files.pythonhosted.org/packages/77/d5/8c982d58144de49f59571f940e329ad6e8615e1e82ef84584c5eeb5e1d72/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", size = 124366 }, - { url = "https://files.pythonhosted.org/packages/bf/19/411a64f01ee971bed3231111b69eb56f9331a769072de479eae7de52296d/charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", size = 118874 }, - { url = "https://files.pythonhosted.org/packages/4c/92/97509850f0d00e9f14a46bc751daabd0ad7765cff29cdfb66c68b6dad57f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", size = 138243 }, - { url = "https://files.pythonhosted.org/packages/e2/29/d227805bff72ed6d6cb1ce08eec707f7cfbd9868044893617eb331f16295/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", size = 148676 }, - { url = "https://files.pythonhosted.org/packages/13/bc/87c2c9f2c144bedfa62f894c3007cd4530ba4b5351acb10dc786428a50f0/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", size = 141289 }, - { url = "https://files.pythonhosted.org/packages/eb/5b/6f10bad0f6461fa272bfbbdf5d0023b5fb9bc6217c92bf068fa5a99820f5/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", size = 142585 }, - { url = "https://files.pythonhosted.org/packages/3b/a0/a68980ab8a1f45a36d9745d35049c1af57d27255eff8c907e3add84cf68f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", size = 144408 }, - { url = "https://files.pythonhosted.org/packages/d7/a1/493919799446464ed0299c8eef3c3fad0daf1c3cd48bff9263c731b0d9e2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", size = 139076 }, - { url = "https://files.pythonhosted.org/packages/fb/9d/9c13753a5a6e0db4a0a6edb1cef7aee39859177b64e1a1e748a6e3ba62c2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", size = 146874 }, - { url = "https://files.pythonhosted.org/packages/75/d2/0ab54463d3410709c09266dfb416d032a08f97fd7d60e94b8c6ef54ae14b/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", size = 150871 }, - { url = "https://files.pythonhosted.org/packages/8d/c9/27e41d481557be53d51e60750b85aa40eaf52b841946b3cdeff363105737/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", size = 148546 }, - { url = "https://files.pythonhosted.org/packages/ee/44/4f62042ca8cdc0cabf87c0fc00ae27cd8b53ab68be3605ba6d071f742ad3/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", size = 143048 }, - { url = "https://files.pythonhosted.org/packages/01/f8/38842422988b795220eb8038745d27a675ce066e2ada79516c118f291f07/charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", size = 94389 }, - { url = "https://files.pythonhosted.org/packages/0b/6e/b13bd47fa9023b3699e94abf565b5a2f0b0be6e9ddac9812182596ee62e4/charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", size = 101752 }, - { url = "https://files.pythonhosted.org/packages/d3/0b/4b7a70987abf9b8196845806198975b6aab4ce016632f817ad758a5aa056/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", size = 194445 }, - { url = "https://files.pythonhosted.org/packages/50/89/354cc56cf4dd2449715bc9a0f54f3aef3dc700d2d62d1fa5bbea53b13426/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", size = 125275 }, - { url = "https://files.pythonhosted.org/packages/fa/44/b730e2a2580110ced837ac083d8ad222343c96bb6b66e9e4e706e4d0b6df/charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", size = 119020 }, - { url = "https://files.pythonhosted.org/packages/9d/e4/9263b8240ed9472a2ae7ddc3e516e71ef46617fe40eaa51221ccd4ad9a27/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", size = 139128 }, - { url = "https://files.pythonhosted.org/packages/6b/e3/9f73e779315a54334240353eaea75854a9a690f3f580e4bd85d977cb2204/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", size = 149277 }, - { url = "https://files.pythonhosted.org/packages/1a/cf/f1f50c2f295312edb8a548d3fa56a5c923b146cd3f24114d5adb7e7be558/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", size = 142174 }, - { url = "https://files.pythonhosted.org/packages/16/92/92a76dc2ff3a12e69ba94e7e05168d37d0345fa08c87e1fe24d0c2a42223/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", size = 143838 }, - { url = "https://files.pythonhosted.org/packages/a4/01/2117ff2b1dfc61695daf2babe4a874bca328489afa85952440b59819e9d7/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", size = 146149 }, - { url = "https://files.pythonhosted.org/packages/f6/9b/93a332b8d25b347f6839ca0a61b7f0287b0930216994e8bf67a75d050255/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", size = 140043 }, - { url = "https://files.pythonhosted.org/packages/ab/f6/7ac4a01adcdecbc7a7587767c776d53d369b8b971382b91211489535acf0/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", size = 148229 }, - { url = "https://files.pythonhosted.org/packages/9d/be/5708ad18161dee7dc6a0f7e6cf3a88ea6279c3e8484844c0590e50e803ef/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", size = 151556 }, - { url = "https://files.pythonhosted.org/packages/5a/bb/3d8bc22bacb9eb89785e83e6723f9888265f3a0de3b9ce724d66bd49884e/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", size = 149772 }, - { url = "https://files.pythonhosted.org/packages/f7/fa/d3fc622de05a86f30beea5fc4e9ac46aead4731e73fd9055496732bcc0a4/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", size = 144800 }, - { url = "https://files.pythonhosted.org/packages/9a/65/bdb9bc496d7d190d725e96816e20e2ae3a6fa42a5cac99c3c3d6ff884118/charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", size = 94836 }, - { url = "https://files.pythonhosted.org/packages/3e/67/7b72b69d25b89c0b3cea583ee372c43aa24df15f0e0f8d3982c57804984b/charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", size = 102187 }, - { url = "https://files.pythonhosted.org/packages/f3/89/68a4c86f1a0002810a27f12e9a7b22feb198c59b2f05231349fbce5c06f4/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", size = 194617 }, - { url = "https://files.pythonhosted.org/packages/4f/cd/8947fe425e2ab0aa57aceb7807af13a0e4162cd21eee42ef5b053447edf5/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", size = 125310 }, - { url = "https://files.pythonhosted.org/packages/5b/f0/b5263e8668a4ee9becc2b451ed909e9c27058337fda5b8c49588183c267a/charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", size = 119126 }, - { url = "https://files.pythonhosted.org/packages/ff/6e/e445afe4f7fda27a533f3234b627b3e515a1b9429bc981c9a5e2aa5d97b6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", size = 139342 }, - { url = "https://files.pythonhosted.org/packages/a1/b2/4af9993b532d93270538ad4926c8e37dc29f2111c36f9c629840c57cd9b3/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", size = 149383 }, - { url = "https://files.pythonhosted.org/packages/fb/6f/4e78c3b97686b871db9be6f31d64e9264e889f8c9d7ab33c771f847f79b7/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", size = 142214 }, - { url = "https://files.pythonhosted.org/packages/2b/c9/1c8fe3ce05d30c87eff498592c89015b19fade13df42850aafae09e94f35/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", size = 144104 }, - { url = "https://files.pythonhosted.org/packages/ee/68/efad5dcb306bf37db7db338338e7bb8ebd8cf38ee5bbd5ceaaaa46f257e6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", size = 146255 }, - { url = "https://files.pythonhosted.org/packages/0c/75/1ed813c3ffd200b1f3e71121c95da3f79e6d2a96120163443b3ad1057505/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", size = 140251 }, - { url = "https://files.pythonhosted.org/packages/7d/0d/6f32255c1979653b448d3c709583557a4d24ff97ac4f3a5be156b2e6a210/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", size = 148474 }, - { url = "https://files.pythonhosted.org/packages/ac/a0/c1b5298de4670d997101fef95b97ac440e8c8d8b4efa5a4d1ef44af82f0d/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", size = 151849 }, - { url = "https://files.pythonhosted.org/packages/04/4f/b3961ba0c664989ba63e30595a3ed0875d6790ff26671e2aae2fdc28a399/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", size = 149781 }, - { url = "https://files.pythonhosted.org/packages/d8/90/6af4cd042066a4adad58ae25648a12c09c879efa4849c705719ba1b23d8c/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482", size = 144970 }, - { url = "https://files.pythonhosted.org/packages/cc/67/e5e7e0cbfefc4ca79025238b43cdf8a2037854195b37d6417f3d0895c4c2/charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", size = 94973 }, - { url = "https://files.pythonhosted.org/packages/65/97/fc9bbc54ee13d33dc54a7fcf17b26368b18505500fc01e228c27b5222d80/charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", size = 102308 }, - { url = "https://files.pythonhosted.org/packages/54/2f/28659eee7f5d003e0f5a3b572765bf76d6e0fe6601ab1f1b1dd4cba7e4f1/charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa", size = 196326 }, - { url = "https://files.pythonhosted.org/packages/d1/18/92869d5c0057baa973a3ee2af71573be7b084b3c3d428fe6463ce71167f8/charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a", size = 125614 }, - { url = "https://files.pythonhosted.org/packages/d6/27/327904c5a54a7796bb9f36810ec4173d2df5d88b401d2b95ef53111d214e/charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0", size = 120450 }, - { url = "https://files.pythonhosted.org/packages/a4/23/65af317914a0308495133b2d654cf67b11bbd6ca16637c4e8a38f80a5a69/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a", size = 140135 }, - { url = "https://files.pythonhosted.org/packages/f2/41/6190102ad521a8aa888519bb014a74251ac4586cde9b38e790901684f9ab/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242", size = 150413 }, - { url = "https://files.pythonhosted.org/packages/7b/ab/f47b0159a69eab9bd915591106859f49670c75f9a19082505ff16f50efc0/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b", size = 142992 }, - { url = "https://files.pythonhosted.org/packages/28/89/60f51ad71f63aaaa7e51a2a2ad37919985a341a1d267070f212cdf6c2d22/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62", size = 144871 }, - { url = "https://files.pythonhosted.org/packages/0c/48/0050550275fea585a6e24460b42465020b53375017d8596c96be57bfabca/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0", size = 146756 }, - { url = "https://files.pythonhosted.org/packages/dc/b5/47f8ee91455946f745e6c9ddbb0f8f50314d2416dd922b213e7d5551ad09/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd", size = 141034 }, - { url = "https://files.pythonhosted.org/packages/84/79/5c731059ebab43e80bf61fa51666b9b18167974b82004f18c76378ed31a3/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be", size = 149434 }, - { url = "https://files.pythonhosted.org/packages/ca/f3/0719cd09fc4dc42066f239cb3c48ced17fc3316afca3e2a30a4756fe49ab/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d", size = 152443 }, - { url = "https://files.pythonhosted.org/packages/f7/0e/c6357297f1157c8e8227ff337e93fd0a90e498e3d6ab96b2782204ecae48/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3", size = 150294 }, - { url = "https://files.pythonhosted.org/packages/54/9a/acfa96dc4ea8c928040b15822b59d0863d6e1757fba8bd7de3dc4f761c13/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742", size = 145314 }, - { url = "https://files.pythonhosted.org/packages/73/1c/b10a63032eaebb8d7bcb8544f12f063f41f5f463778ac61da15d9985e8b6/charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2", size = 94724 }, - { url = "https://files.pythonhosted.org/packages/c5/77/3a78bf28bfaa0863f9cfef278dbeadf55efe064eafff8c7c424ae3c4c1bf/charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca", size = 102159 }, - { url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 }, +sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620, upload-time = "2024-10-09T07:40:20.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/8b/825cc84cf13a28bfbcba7c416ec22bf85a9584971be15b21dd8300c65b7f/charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6", size = 196363, upload-time = "2024-10-09T07:38:02.622Z" }, + { url = "https://files.pythonhosted.org/packages/23/81/d7eef6a99e42c77f444fdd7bc894b0ceca6c3a95c51239e74a722039521c/charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b", size = 125639, upload-time = "2024-10-09T07:38:04.044Z" }, + { url = "https://files.pythonhosted.org/packages/21/67/b4564d81f48042f520c948abac7079356e94b30cb8ffb22e747532cf469d/charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99", size = 120451, upload-time = "2024-10-09T07:38:04.997Z" }, + { url = "https://files.pythonhosted.org/packages/c2/72/12a7f0943dd71fb5b4e7b55c41327ac0a1663046a868ee4d0d8e9c369b85/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca", size = 140041, upload-time = "2024-10-09T07:38:06.676Z" }, + { url = "https://files.pythonhosted.org/packages/67/56/fa28c2c3e31217c4c52158537a2cf5d98a6c1e89d31faf476c89391cd16b/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d", size = 150333, upload-time = "2024-10-09T07:38:08.626Z" }, + { url = "https://files.pythonhosted.org/packages/f9/d2/466a9be1f32d89eb1554cf84073a5ed9262047acee1ab39cbaefc19635d2/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7", size = 142921, upload-time = "2024-10-09T07:38:10.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/01/344ec40cf5d85c1da3c1f57566c59e0c9b56bcc5566c08804a95a6cc8257/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3", size = 144785, upload-time = "2024-10-09T07:38:12.019Z" }, + { url = "https://files.pythonhosted.org/packages/73/8b/2102692cb6d7e9f03b9a33a710e0164cadfce312872e3efc7cfe22ed26b4/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907", size = 146631, upload-time = "2024-10-09T07:38:13.701Z" }, + { url = "https://files.pythonhosted.org/packages/d8/96/cc2c1b5d994119ce9f088a9a0c3ebd489d360a2eb058e2c8049f27092847/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b", size = 140867, upload-time = "2024-10-09T07:38:15.403Z" }, + { url = "https://files.pythonhosted.org/packages/c9/27/cde291783715b8ec30a61c810d0120411844bc4c23b50189b81188b273db/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912", size = 149273, upload-time = "2024-10-09T07:38:16.433Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a4/8633b0fc1a2d1834d5393dafecce4a1cc56727bfd82b4dc18fc92f0d3cc3/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95", size = 152437, upload-time = "2024-10-09T07:38:18.013Z" }, + { url = "https://files.pythonhosted.org/packages/64/ea/69af161062166b5975ccbb0961fd2384853190c70786f288684490913bf5/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e", size = 150087, upload-time = "2024-10-09T07:38:19.089Z" }, + { url = "https://files.pythonhosted.org/packages/3b/fd/e60a9d9fd967f4ad5a92810138192f825d77b4fa2a557990fd575a47695b/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe", size = 145142, upload-time = "2024-10-09T07:38:20.78Z" }, + { url = "https://files.pythonhosted.org/packages/6d/02/8cb0988a1e49ac9ce2eed1e07b77ff118f2923e9ebd0ede41ba85f2dcb04/charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc", size = 94701, upload-time = "2024-10-09T07:38:21.851Z" }, + { url = "https://files.pythonhosted.org/packages/d6/20/f1d4670a8a723c46be695dff449d86d6092916f9e99c53051954ee33a1bc/charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749", size = 102191, upload-time = "2024-10-09T07:38:23.467Z" }, + { url = "https://files.pythonhosted.org/packages/9c/61/73589dcc7a719582bf56aae309b6103d2762b526bffe189d635a7fcfd998/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", size = 193339, upload-time = "2024-10-09T07:38:24.527Z" }, + { url = "https://files.pythonhosted.org/packages/77/d5/8c982d58144de49f59571f940e329ad6e8615e1e82ef84584c5eeb5e1d72/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", size = 124366, upload-time = "2024-10-09T07:38:26.488Z" }, + { url = "https://files.pythonhosted.org/packages/bf/19/411a64f01ee971bed3231111b69eb56f9331a769072de479eae7de52296d/charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", size = 118874, upload-time = "2024-10-09T07:38:28.115Z" }, + { url = "https://files.pythonhosted.org/packages/4c/92/97509850f0d00e9f14a46bc751daabd0ad7765cff29cdfb66c68b6dad57f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", size = 138243, upload-time = "2024-10-09T07:38:29.822Z" }, + { url = "https://files.pythonhosted.org/packages/e2/29/d227805bff72ed6d6cb1ce08eec707f7cfbd9868044893617eb331f16295/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", size = 148676, upload-time = "2024-10-09T07:38:30.869Z" }, + { url = "https://files.pythonhosted.org/packages/13/bc/87c2c9f2c144bedfa62f894c3007cd4530ba4b5351acb10dc786428a50f0/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", size = 141289, upload-time = "2024-10-09T07:38:32.557Z" }, + { url = "https://files.pythonhosted.org/packages/eb/5b/6f10bad0f6461fa272bfbbdf5d0023b5fb9bc6217c92bf068fa5a99820f5/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", size = 142585, upload-time = "2024-10-09T07:38:33.649Z" }, + { url = "https://files.pythonhosted.org/packages/3b/a0/a68980ab8a1f45a36d9745d35049c1af57d27255eff8c907e3add84cf68f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", size = 144408, upload-time = "2024-10-09T07:38:34.687Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a1/493919799446464ed0299c8eef3c3fad0daf1c3cd48bff9263c731b0d9e2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", size = 139076, upload-time = "2024-10-09T07:38:36.417Z" }, + { url = "https://files.pythonhosted.org/packages/fb/9d/9c13753a5a6e0db4a0a6edb1cef7aee39859177b64e1a1e748a6e3ba62c2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", size = 146874, upload-time = "2024-10-09T07:38:37.59Z" }, + { url = "https://files.pythonhosted.org/packages/75/d2/0ab54463d3410709c09266dfb416d032a08f97fd7d60e94b8c6ef54ae14b/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", size = 150871, upload-time = "2024-10-09T07:38:38.666Z" }, + { url = "https://files.pythonhosted.org/packages/8d/c9/27e41d481557be53d51e60750b85aa40eaf52b841946b3cdeff363105737/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", size = 148546, upload-time = "2024-10-09T07:38:40.459Z" }, + { url = "https://files.pythonhosted.org/packages/ee/44/4f62042ca8cdc0cabf87c0fc00ae27cd8b53ab68be3605ba6d071f742ad3/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", size = 143048, upload-time = "2024-10-09T07:38:42.178Z" }, + { url = "https://files.pythonhosted.org/packages/01/f8/38842422988b795220eb8038745d27a675ce066e2ada79516c118f291f07/charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", size = 94389, upload-time = "2024-10-09T07:38:43.339Z" }, + { url = "https://files.pythonhosted.org/packages/0b/6e/b13bd47fa9023b3699e94abf565b5a2f0b0be6e9ddac9812182596ee62e4/charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", size = 101752, upload-time = "2024-10-09T07:38:44.276Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0b/4b7a70987abf9b8196845806198975b6aab4ce016632f817ad758a5aa056/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", size = 194445, upload-time = "2024-10-09T07:38:45.275Z" }, + { url = "https://files.pythonhosted.org/packages/50/89/354cc56cf4dd2449715bc9a0f54f3aef3dc700d2d62d1fa5bbea53b13426/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", size = 125275, upload-time = "2024-10-09T07:38:46.449Z" }, + { url = "https://files.pythonhosted.org/packages/fa/44/b730e2a2580110ced837ac083d8ad222343c96bb6b66e9e4e706e4d0b6df/charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", size = 119020, upload-time = "2024-10-09T07:38:48.88Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e4/9263b8240ed9472a2ae7ddc3e516e71ef46617fe40eaa51221ccd4ad9a27/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", size = 139128, upload-time = "2024-10-09T07:38:49.86Z" }, + { url = "https://files.pythonhosted.org/packages/6b/e3/9f73e779315a54334240353eaea75854a9a690f3f580e4bd85d977cb2204/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", size = 149277, upload-time = "2024-10-09T07:38:52.306Z" }, + { url = "https://files.pythonhosted.org/packages/1a/cf/f1f50c2f295312edb8a548d3fa56a5c923b146cd3f24114d5adb7e7be558/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", size = 142174, upload-time = "2024-10-09T07:38:53.458Z" }, + { url = "https://files.pythonhosted.org/packages/16/92/92a76dc2ff3a12e69ba94e7e05168d37d0345fa08c87e1fe24d0c2a42223/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", size = 143838, upload-time = "2024-10-09T07:38:54.691Z" }, + { url = "https://files.pythonhosted.org/packages/a4/01/2117ff2b1dfc61695daf2babe4a874bca328489afa85952440b59819e9d7/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", size = 146149, upload-time = "2024-10-09T07:38:55.737Z" }, + { url = "https://files.pythonhosted.org/packages/f6/9b/93a332b8d25b347f6839ca0a61b7f0287b0930216994e8bf67a75d050255/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", size = 140043, upload-time = "2024-10-09T07:38:57.44Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f6/7ac4a01adcdecbc7a7587767c776d53d369b8b971382b91211489535acf0/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", size = 148229, upload-time = "2024-10-09T07:38:58.782Z" }, + { url = "https://files.pythonhosted.org/packages/9d/be/5708ad18161dee7dc6a0f7e6cf3a88ea6279c3e8484844c0590e50e803ef/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", size = 151556, upload-time = "2024-10-09T07:39:00.467Z" }, + { url = "https://files.pythonhosted.org/packages/5a/bb/3d8bc22bacb9eb89785e83e6723f9888265f3a0de3b9ce724d66bd49884e/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", size = 149772, upload-time = "2024-10-09T07:39:01.5Z" }, + { url = "https://files.pythonhosted.org/packages/f7/fa/d3fc622de05a86f30beea5fc4e9ac46aead4731e73fd9055496732bcc0a4/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", size = 144800, upload-time = "2024-10-09T07:39:02.491Z" }, + { url = "https://files.pythonhosted.org/packages/9a/65/bdb9bc496d7d190d725e96816e20e2ae3a6fa42a5cac99c3c3d6ff884118/charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", size = 94836, upload-time = "2024-10-09T07:39:04.607Z" }, + { url = "https://files.pythonhosted.org/packages/3e/67/7b72b69d25b89c0b3cea583ee372c43aa24df15f0e0f8d3982c57804984b/charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", size = 102187, upload-time = "2024-10-09T07:39:06.247Z" }, + { url = "https://files.pythonhosted.org/packages/f3/89/68a4c86f1a0002810a27f12e9a7b22feb198c59b2f05231349fbce5c06f4/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", size = 194617, upload-time = "2024-10-09T07:39:07.317Z" }, + { url = "https://files.pythonhosted.org/packages/4f/cd/8947fe425e2ab0aa57aceb7807af13a0e4162cd21eee42ef5b053447edf5/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", size = 125310, upload-time = "2024-10-09T07:39:08.353Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f0/b5263e8668a4ee9becc2b451ed909e9c27058337fda5b8c49588183c267a/charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", size = 119126, upload-time = "2024-10-09T07:39:09.327Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6e/e445afe4f7fda27a533f3234b627b3e515a1b9429bc981c9a5e2aa5d97b6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", size = 139342, upload-time = "2024-10-09T07:39:10.322Z" }, + { url = "https://files.pythonhosted.org/packages/a1/b2/4af9993b532d93270538ad4926c8e37dc29f2111c36f9c629840c57cd9b3/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", size = 149383, upload-time = "2024-10-09T07:39:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/fb/6f/4e78c3b97686b871db9be6f31d64e9264e889f8c9d7ab33c771f847f79b7/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", size = 142214, upload-time = "2024-10-09T07:39:13.059Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c9/1c8fe3ce05d30c87eff498592c89015b19fade13df42850aafae09e94f35/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", size = 144104, upload-time = "2024-10-09T07:39:14.815Z" }, + { url = "https://files.pythonhosted.org/packages/ee/68/efad5dcb306bf37db7db338338e7bb8ebd8cf38ee5bbd5ceaaaa46f257e6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", size = 146255, upload-time = "2024-10-09T07:39:15.868Z" }, + { url = "https://files.pythonhosted.org/packages/0c/75/1ed813c3ffd200b1f3e71121c95da3f79e6d2a96120163443b3ad1057505/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", size = 140251, upload-time = "2024-10-09T07:39:16.995Z" }, + { url = "https://files.pythonhosted.org/packages/7d/0d/6f32255c1979653b448d3c709583557a4d24ff97ac4f3a5be156b2e6a210/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", size = 148474, upload-time = "2024-10-09T07:39:18.021Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a0/c1b5298de4670d997101fef95b97ac440e8c8d8b4efa5a4d1ef44af82f0d/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", size = 151849, upload-time = "2024-10-09T07:39:19.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/4f/b3961ba0c664989ba63e30595a3ed0875d6790ff26671e2aae2fdc28a399/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", size = 149781, upload-time = "2024-10-09T07:39:20.397Z" }, + { url = "https://files.pythonhosted.org/packages/d8/90/6af4cd042066a4adad58ae25648a12c09c879efa4849c705719ba1b23d8c/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482", size = 144970, upload-time = "2024-10-09T07:39:21.452Z" }, + { url = "https://files.pythonhosted.org/packages/cc/67/e5e7e0cbfefc4ca79025238b43cdf8a2037854195b37d6417f3d0895c4c2/charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", size = 94973, upload-time = "2024-10-09T07:39:22.509Z" }, + { url = "https://files.pythonhosted.org/packages/65/97/fc9bbc54ee13d33dc54a7fcf17b26368b18505500fc01e228c27b5222d80/charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", size = 102308, upload-time = "2024-10-09T07:39:23.524Z" }, + { url = "https://files.pythonhosted.org/packages/54/2f/28659eee7f5d003e0f5a3b572765bf76d6e0fe6601ab1f1b1dd4cba7e4f1/charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa", size = 196326, upload-time = "2024-10-09T07:39:59.619Z" }, + { url = "https://files.pythonhosted.org/packages/d1/18/92869d5c0057baa973a3ee2af71573be7b084b3c3d428fe6463ce71167f8/charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a", size = 125614, upload-time = "2024-10-09T07:40:00.776Z" }, + { url = "https://files.pythonhosted.org/packages/d6/27/327904c5a54a7796bb9f36810ec4173d2df5d88b401d2b95ef53111d214e/charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0", size = 120450, upload-time = "2024-10-09T07:40:02.621Z" }, + { url = "https://files.pythonhosted.org/packages/a4/23/65af317914a0308495133b2d654cf67b11bbd6ca16637c4e8a38f80a5a69/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a", size = 140135, upload-time = "2024-10-09T07:40:05.719Z" }, + { url = "https://files.pythonhosted.org/packages/f2/41/6190102ad521a8aa888519bb014a74251ac4586cde9b38e790901684f9ab/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242", size = 150413, upload-time = "2024-10-09T07:40:06.777Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ab/f47b0159a69eab9bd915591106859f49670c75f9a19082505ff16f50efc0/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b", size = 142992, upload-time = "2024-10-09T07:40:07.921Z" }, + { url = "https://files.pythonhosted.org/packages/28/89/60f51ad71f63aaaa7e51a2a2ad37919985a341a1d267070f212cdf6c2d22/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62", size = 144871, upload-time = "2024-10-09T07:40:09.035Z" }, + { url = "https://files.pythonhosted.org/packages/0c/48/0050550275fea585a6e24460b42465020b53375017d8596c96be57bfabca/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0", size = 146756, upload-time = "2024-10-09T07:40:10.186Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b5/47f8ee91455946f745e6c9ddbb0f8f50314d2416dd922b213e7d5551ad09/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd", size = 141034, upload-time = "2024-10-09T07:40:11.386Z" }, + { url = "https://files.pythonhosted.org/packages/84/79/5c731059ebab43e80bf61fa51666b9b18167974b82004f18c76378ed31a3/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be", size = 149434, upload-time = "2024-10-09T07:40:12.513Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f3/0719cd09fc4dc42066f239cb3c48ced17fc3316afca3e2a30a4756fe49ab/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d", size = 152443, upload-time = "2024-10-09T07:40:13.655Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0e/c6357297f1157c8e8227ff337e93fd0a90e498e3d6ab96b2782204ecae48/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3", size = 150294, upload-time = "2024-10-09T07:40:14.883Z" }, + { url = "https://files.pythonhosted.org/packages/54/9a/acfa96dc4ea8c928040b15822b59d0863d6e1757fba8bd7de3dc4f761c13/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742", size = 145314, upload-time = "2024-10-09T07:40:16.043Z" }, + { url = "https://files.pythonhosted.org/packages/73/1c/b10a63032eaebb8d7bcb8544f12f063f41f5f463778ac61da15d9985e8b6/charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2", size = 94724, upload-time = "2024-10-09T07:40:17.199Z" }, + { url = "https://files.pythonhosted.org/packages/c5/77/3a78bf28bfaa0863f9cfef278dbeadf55efe064eafff8c7c424ae3c4c1bf/charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca", size = 102159, upload-time = "2024-10-09T07:40:18.264Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446, upload-time = "2024-10-09T07:40:19.383Z" }, ] [[package]] @@ -180,129 +181,129 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } +sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121, upload-time = "2023-08-17T17:29:11.868Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, + { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941, upload-time = "2023-08-17T17:29:10.08Z" }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] name = "coverage" version = "7.10.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/6c/3a3f7a46888e69d18abe3ccc6fe4cb16cccb1e6a2f99698931dafca489e6/coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a", size = 217987 }, - { url = "https://files.pythonhosted.org/packages/03/94/952d30f180b1a916c11a56f5c22d3535e943aa22430e9e3322447e520e1c/coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5", size = 218388 }, - { url = "https://files.pythonhosted.org/packages/50/2b/9e0cf8ded1e114bcd8b2fd42792b57f1c4e9e4ea1824cde2af93a67305be/coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17", size = 245148 }, - { url = "https://files.pythonhosted.org/packages/19/20/d0384ac06a6f908783d9b6aa6135e41b093971499ec488e47279f5b846e6/coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b", size = 246958 }, - { url = "https://files.pythonhosted.org/packages/60/83/5c283cff3d41285f8eab897651585db908a909c572bdc014bcfaf8a8b6ae/coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87", size = 248819 }, - { url = "https://files.pythonhosted.org/packages/60/22/02eb98fdc5ff79f423e990d877693e5310ae1eab6cb20ae0b0b9ac45b23b/coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e", size = 245754 }, - { url = "https://files.pythonhosted.org/packages/b4/bc/25c83bcf3ad141b32cd7dc45485ef3c01a776ca3aa8ef0a93e77e8b5bc43/coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e", size = 246860 }, - { url = "https://files.pythonhosted.org/packages/3c/b7/95574702888b58c0928a6e982038c596f9c34d52c5e5107f1eef729399b5/coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df", size = 244877 }, - { url = "https://files.pythonhosted.org/packages/47/b6/40095c185f235e085df0e0b158f6bd68cc6e1d80ba6c7721dc81d97ec318/coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0", size = 245108 }, - { url = "https://files.pythonhosted.org/packages/c8/50/4aea0556da7a4b93ec9168420d170b55e2eb50ae21b25062513d020c6861/coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13", size = 245752 }, - { url = "https://files.pythonhosted.org/packages/6a/28/ea1a84a60828177ae3b100cb6723838523369a44ec5742313ed7db3da160/coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b", size = 220497 }, - { url = "https://files.pythonhosted.org/packages/fc/1a/a81d46bbeb3c3fd97b9602ebaa411e076219a150489bcc2c025f151bd52d/coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807", size = 221392 }, - { url = "https://files.pythonhosted.org/packages/d2/5d/c1a17867b0456f2e9ce2d8d4708a4c3a089947d0bec9c66cdf60c9e7739f/coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59", size = 218102 }, - { url = "https://files.pythonhosted.org/packages/54/f0/514dcf4b4e3698b9a9077f084429681bf3aad2b4a72578f89d7f643eb506/coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a", size = 218505 }, - { url = "https://files.pythonhosted.org/packages/20/f6/9626b81d17e2a4b25c63ac1b425ff307ecdeef03d67c9a147673ae40dc36/coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699", size = 248898 }, - { url = "https://files.pythonhosted.org/packages/b0/ef/bd8e719c2f7417ba03239052e099b76ea1130ac0cbb183ee1fcaa58aaff3/coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d", size = 250831 }, - { url = "https://files.pythonhosted.org/packages/a5/b6/bf054de41ec948b151ae2b79a55c107f5760979538f5fb80c195f2517718/coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e", size = 252937 }, - { url = "https://files.pythonhosted.org/packages/0f/e5/3860756aa6f9318227443c6ce4ed7bf9e70bb7f1447a0353f45ac5c7974b/coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23", size = 249021 }, - { url = "https://files.pythonhosted.org/packages/26/0f/bd08bd042854f7fd07b45808927ebcce99a7ed0f2f412d11629883517ac2/coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab", size = 250626 }, - { url = "https://files.pythonhosted.org/packages/8e/a7/4777b14de4abcc2e80c6b1d430f5d51eb18ed1d75fca56cbce5f2db9b36e/coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82", size = 248682 }, - { url = "https://files.pythonhosted.org/packages/34/72/17d082b00b53cd45679bad682fac058b87f011fd8b9fe31d77f5f8d3a4e4/coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2", size = 248402 }, - { url = "https://files.pythonhosted.org/packages/81/7a/92367572eb5bdd6a84bfa278cc7e97db192f9f45b28c94a9ca1a921c3577/coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61", size = 249320 }, - { url = "https://files.pythonhosted.org/packages/2f/88/a23cc185f6a805dfc4fdf14a94016835eeb85e22ac3a0e66d5e89acd6462/coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14", size = 220536 }, - { url = "https://files.pythonhosted.org/packages/fe/ef/0b510a399dfca17cec7bc2f05ad8bd78cf55f15c8bc9a73ab20c5c913c2e/coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2", size = 221425 }, - { url = "https://files.pythonhosted.org/packages/51/7f/023657f301a276e4ba1850f82749bc136f5a7e8768060c2e5d9744a22951/coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a", size = 220103 }, - { url = "https://files.pythonhosted.org/packages/13/e4/eb12450f71b542a53972d19117ea5a5cea1cab3ac9e31b0b5d498df1bd5a/coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417", size = 218290 }, - { url = "https://files.pythonhosted.org/packages/37/66/593f9be12fc19fb36711f19a5371af79a718537204d16ea1d36f16bd78d2/coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973", size = 218515 }, - { url = "https://files.pythonhosted.org/packages/66/80/4c49f7ae09cafdacc73fbc30949ffe77359635c168f4e9ff33c9ebb07838/coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c", size = 250020 }, - { url = "https://files.pythonhosted.org/packages/a6/90/a64aaacab3b37a17aaedd83e8000142561a29eb262cede42d94a67f7556b/coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7", size = 252769 }, - { url = "https://files.pythonhosted.org/packages/98/2e/2dda59afd6103b342e096f246ebc5f87a3363b5412609946c120f4e7750d/coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6", size = 253901 }, - { url = "https://files.pythonhosted.org/packages/53/dc/8d8119c9051d50f3119bb4a75f29f1e4a6ab9415cd1fa8bf22fcc3fb3b5f/coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59", size = 250413 }, - { url = "https://files.pythonhosted.org/packages/98/b3/edaff9c5d79ee4d4b6d3fe046f2b1d799850425695b789d491a64225d493/coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b", size = 251820 }, - { url = "https://files.pythonhosted.org/packages/11/25/9a0728564bb05863f7e513e5a594fe5ffef091b325437f5430e8cfb0d530/coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a", size = 249941 }, - { url = "https://files.pythonhosted.org/packages/e0/fd/ca2650443bfbef5b0e74373aac4df67b08180d2f184b482c41499668e258/coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb", size = 249519 }, - { url = "https://files.pythonhosted.org/packages/24/79/f692f125fb4299b6f963b0745124998ebb8e73ecdfce4ceceb06a8c6bec5/coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1", size = 251375 }, - { url = "https://files.pythonhosted.org/packages/5e/75/61b9bbd6c7d24d896bfeec57acba78e0f8deac68e6baf2d4804f7aae1f88/coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256", size = 220699 }, - { url = "https://files.pythonhosted.org/packages/ca/f3/3bf7905288b45b075918d372498f1cf845b5b579b723c8fd17168018d5f5/coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba", size = 221512 }, - { url = "https://files.pythonhosted.org/packages/5c/44/3e32dbe933979d05cf2dac5e697c8599cfe038aaf51223ab901e208d5a62/coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf", size = 220147 }, - { url = "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d", size = 218320 }, - { url = "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b", size = 218575 }, - { url = "https://files.pythonhosted.org/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e", size = 249568 }, - { url = "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b", size = 252174 }, - { url = "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49", size = 253447 }, - { url = "https://files.pythonhosted.org/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911", size = 249779 }, - { url = "https://files.pythonhosted.org/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0", size = 251604 }, - { url = "https://files.pythonhosted.org/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f", size = 249497 }, - { url = "https://files.pythonhosted.org/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c", size = 249350 }, - { url = "https://files.pythonhosted.org/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f", size = 251111 }, - { url = "https://files.pythonhosted.org/packages/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698", size = 220746 }, - { url = "https://files.pythonhosted.org/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843", size = 221541 }, - { url = "https://files.pythonhosted.org/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546", size = 220170 }, - { url = "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c", size = 219029 }, - { url = "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15", size = 219259 }, - { url = "https://files.pythonhosted.org/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4", size = 260592 }, - { url = "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0", size = 262768 }, - { url = "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0", size = 264995 }, - { url = "https://files.pythonhosted.org/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65", size = 259546 }, - { url = "https://files.pythonhosted.org/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541", size = 262544 }, - { url = "https://files.pythonhosted.org/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6", size = 260308 }, - { url = "https://files.pythonhosted.org/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999", size = 258920 }, - { url = "https://files.pythonhosted.org/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2", size = 261434 }, - { url = "https://files.pythonhosted.org/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a", size = 221403 }, - { url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb", size = 222469 }, - { url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb", size = 220731 }, - { url = "https://files.pythonhosted.org/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520", size = 218302 }, - { url = "https://files.pythonhosted.org/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32", size = 218578 }, - { url = "https://files.pythonhosted.org/packages/05/e8/261cae479e85232828fb17ad536765c88dd818c8470aca690b0ac6feeaa3/coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f", size = 249629 }, - { url = "https://files.pythonhosted.org/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a", size = 252162 }, - { url = "https://files.pythonhosted.org/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360", size = 253517 }, - { url = "https://files.pythonhosted.org/packages/a2/59/c5201c62dbf165dfbc91460f6dbbaa85a8b82cfa6131ac45d6c1bfb52deb/coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69", size = 249632 }, - { url = "https://files.pythonhosted.org/packages/07/ae/5920097195291a51fb00b3a70b9bbd2edbfe3c84876a1762bd1ef1565ebc/coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14", size = 251520 }, - { url = "https://files.pythonhosted.org/packages/b9/3c/a815dde77a2981f5743a60b63df31cb322c944843e57dbd579326625a413/coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe", size = 249455 }, - { url = "https://files.pythonhosted.org/packages/aa/99/f5cdd8421ea656abefb6c0ce92556709db2265c41e8f9fc6c8ae0f7824c9/coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e", size = 249287 }, - { url = "https://files.pythonhosted.org/packages/c3/7a/e9a2da6a1fc5d007dd51fca083a663ab930a8c4d149c087732a5dbaa0029/coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd", size = 250946 }, - { url = "https://files.pythonhosted.org/packages/ef/5b/0b5799aa30380a949005a353715095d6d1da81927d6dbed5def2200a4e25/coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2", size = 221009 }, - { url = "https://files.pythonhosted.org/packages/da/b0/e802fbb6eb746de006490abc9bb554b708918b6774b722bb3a0e6aa1b7de/coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681", size = 221804 }, - { url = "https://files.pythonhosted.org/packages/9e/e8/71d0c8e374e31f39e3389bb0bd19e527d46f00ea8571ec7ec8fd261d8b44/coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880", size = 220384 }, - { url = "https://files.pythonhosted.org/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63", size = 219047 }, - { url = "https://files.pythonhosted.org/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2", size = 219266 }, - { url = "https://files.pythonhosted.org/packages/74/5c/183ffc817ba68e0b443b8c934c8795553eb0c14573813415bd59941ee165/coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d", size = 260767 }, - { url = "https://files.pythonhosted.org/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0", size = 262931 }, - { url = "https://files.pythonhosted.org/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699", size = 265186 }, - { url = "https://files.pythonhosted.org/packages/b1/8f/74ecc30607dd95ad50e3034221113ccb1c6d4e8085cc761134782995daae/coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9", size = 259470 }, - { url = "https://files.pythonhosted.org/packages/0f/55/79ff53a769f20d71b07023ea115c9167c0bb56f281320520cf64c5298a96/coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f", size = 262626 }, - { url = "https://files.pythonhosted.org/packages/88/e2/dac66c140009b61ac3fc13af673a574b00c16efdf04f9b5c740703e953c0/coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1", size = 260386 }, - { url = "https://files.pythonhosted.org/packages/a2/f1/f48f645e3f33bb9ca8a496bc4a9671b52f2f353146233ebd7c1df6160440/coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0", size = 258852 }, - { url = "https://files.pythonhosted.org/packages/bb/3b/8442618972c51a7affeead957995cfa8323c0c9bcf8fa5a027421f720ff4/coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399", size = 261534 }, - { url = "https://files.pythonhosted.org/packages/b2/dc/101f3fa3a45146db0cb03f5b4376e24c0aac818309da23e2de0c75295a91/coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235", size = 221784 }, - { url = "https://files.pythonhosted.org/packages/4c/a1/74c51803fc70a8a40d7346660379e144be772bab4ac7bb6e6b905152345c/coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d", size = 222905 }, - { url = "https://files.pythonhosted.org/packages/12/65/f116a6d2127df30bcafbceef0302d8a64ba87488bf6f73a6d8eebf060873/coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a", size = 220922 }, - { url = "https://files.pythonhosted.org/packages/a3/ad/d1c25053764b4c42eb294aae92ab617d2e4f803397f9c7c8295caa77a260/coverage-7.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3", size = 217978 }, - { url = "https://files.pythonhosted.org/packages/52/2f/b9f9daa39b80ece0b9548bbb723381e29bc664822d9a12c2135f8922c22b/coverage-7.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c", size = 218370 }, - { url = "https://files.pythonhosted.org/packages/dd/6e/30d006c3b469e58449650642383dddf1c8fb63d44fdf92994bfd46570695/coverage-7.10.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396", size = 244802 }, - { url = "https://files.pythonhosted.org/packages/b0/49/8a070782ce7e6b94ff6a0b6d7c65ba6bc3091d92a92cef4cd4eb0767965c/coverage-7.10.7-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40", size = 246625 }, - { url = "https://files.pythonhosted.org/packages/6a/92/1c1c5a9e8677ce56d42b97bdaca337b2d4d9ebe703d8c174ede52dbabd5f/coverage-7.10.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594", size = 248399 }, - { url = "https://files.pythonhosted.org/packages/c0/54/b140edee7257e815de7426d5d9846b58505dffc29795fff2dfb7f8a1c5a0/coverage-7.10.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a", size = 245142 }, - { url = "https://files.pythonhosted.org/packages/e4/9e/6d6b8295940b118e8b7083b29226c71f6154f7ff41e9ca431f03de2eac0d/coverage-7.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b", size = 246284 }, - { url = "https://files.pythonhosted.org/packages/db/e5/5e957ca747d43dbe4d9714358375c7546cb3cb533007b6813fc20fce37ad/coverage-7.10.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3", size = 244353 }, - { url = "https://files.pythonhosted.org/packages/9a/45/540fc5cc92536a1b783b7ef99450bd55a4b3af234aae35a18a339973ce30/coverage-7.10.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0", size = 244430 }, - { url = "https://files.pythonhosted.org/packages/75/0b/8287b2e5b38c8fe15d7e3398849bb58d382aedc0864ea0fa1820e8630491/coverage-7.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f", size = 245311 }, - { url = "https://files.pythonhosted.org/packages/0c/1d/29724999984740f0c86d03e6420b942439bf5bd7f54d4382cae386a9d1e9/coverage-7.10.7-cp39-cp39-win32.whl", hash = "sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431", size = 220500 }, - { url = "https://files.pythonhosted.org/packages/43/11/4b1e6b129943f905ca54c339f343877b55b365ae2558806c1be4f7476ed5/coverage-7.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07", size = 221408 }, - { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952 }, +sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/6c/3a3f7a46888e69d18abe3ccc6fe4cb16cccb1e6a2f99698931dafca489e6/coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a", size = 217987, upload-time = "2025-09-21T20:00:57.218Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/952d30f180b1a916c11a56f5c22d3535e943aa22430e9e3322447e520e1c/coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5", size = 218388, upload-time = "2025-09-21T20:01:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/50/2b/9e0cf8ded1e114bcd8b2fd42792b57f1c4e9e4ea1824cde2af93a67305be/coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17", size = 245148, upload-time = "2025-09-21T20:01:01.768Z" }, + { url = "https://files.pythonhosted.org/packages/19/20/d0384ac06a6f908783d9b6aa6135e41b093971499ec488e47279f5b846e6/coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b", size = 246958, upload-time = "2025-09-21T20:01:03.355Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/5c283cff3d41285f8eab897651585db908a909c572bdc014bcfaf8a8b6ae/coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87", size = 248819, upload-time = "2025-09-21T20:01:04.968Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/02eb98fdc5ff79f423e990d877693e5310ae1eab6cb20ae0b0b9ac45b23b/coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e", size = 245754, upload-time = "2025-09-21T20:01:06.321Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bc/25c83bcf3ad141b32cd7dc45485ef3c01a776ca3aa8ef0a93e77e8b5bc43/coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e", size = 246860, upload-time = "2025-09-21T20:01:07.605Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b7/95574702888b58c0928a6e982038c596f9c34d52c5e5107f1eef729399b5/coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df", size = 244877, upload-time = "2025-09-21T20:01:08.829Z" }, + { url = "https://files.pythonhosted.org/packages/47/b6/40095c185f235e085df0e0b158f6bd68cc6e1d80ba6c7721dc81d97ec318/coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0", size = 245108, upload-time = "2025-09-21T20:01:10.527Z" }, + { url = "https://files.pythonhosted.org/packages/c8/50/4aea0556da7a4b93ec9168420d170b55e2eb50ae21b25062513d020c6861/coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13", size = 245752, upload-time = "2025-09-21T20:01:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/6a/28/ea1a84a60828177ae3b100cb6723838523369a44ec5742313ed7db3da160/coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b", size = 220497, upload-time = "2025-09-21T20:01:13.459Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1a/a81d46bbeb3c3fd97b9602ebaa411e076219a150489bcc2c025f151bd52d/coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807", size = 221392, upload-time = "2025-09-21T20:01:14.722Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5d/c1a17867b0456f2e9ce2d8d4708a4c3a089947d0bec9c66cdf60c9e7739f/coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59", size = 218102, upload-time = "2025-09-21T20:01:16.089Z" }, + { url = "https://files.pythonhosted.org/packages/54/f0/514dcf4b4e3698b9a9077f084429681bf3aad2b4a72578f89d7f643eb506/coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a", size = 218505, upload-time = "2025-09-21T20:01:17.788Z" }, + { url = "https://files.pythonhosted.org/packages/20/f6/9626b81d17e2a4b25c63ac1b425ff307ecdeef03d67c9a147673ae40dc36/coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699", size = 248898, upload-time = "2025-09-21T20:01:19.488Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ef/bd8e719c2f7417ba03239052e099b76ea1130ac0cbb183ee1fcaa58aaff3/coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d", size = 250831, upload-time = "2025-09-21T20:01:20.817Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b6/bf054de41ec948b151ae2b79a55c107f5760979538f5fb80c195f2517718/coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e", size = 252937, upload-time = "2025-09-21T20:01:22.171Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e5/3860756aa6f9318227443c6ce4ed7bf9e70bb7f1447a0353f45ac5c7974b/coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23", size = 249021, upload-time = "2025-09-21T20:01:23.907Z" }, + { url = "https://files.pythonhosted.org/packages/26/0f/bd08bd042854f7fd07b45808927ebcce99a7ed0f2f412d11629883517ac2/coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab", size = 250626, upload-time = "2025-09-21T20:01:25.721Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a7/4777b14de4abcc2e80c6b1d430f5d51eb18ed1d75fca56cbce5f2db9b36e/coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82", size = 248682, upload-time = "2025-09-21T20:01:27.105Z" }, + { url = "https://files.pythonhosted.org/packages/34/72/17d082b00b53cd45679bad682fac058b87f011fd8b9fe31d77f5f8d3a4e4/coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2", size = 248402, upload-time = "2025-09-21T20:01:28.629Z" }, + { url = "https://files.pythonhosted.org/packages/81/7a/92367572eb5bdd6a84bfa278cc7e97db192f9f45b28c94a9ca1a921c3577/coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61", size = 249320, upload-time = "2025-09-21T20:01:30.004Z" }, + { url = "https://files.pythonhosted.org/packages/2f/88/a23cc185f6a805dfc4fdf14a94016835eeb85e22ac3a0e66d5e89acd6462/coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14", size = 220536, upload-time = "2025-09-21T20:01:32.184Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ef/0b510a399dfca17cec7bc2f05ad8bd78cf55f15c8bc9a73ab20c5c913c2e/coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2", size = 221425, upload-time = "2025-09-21T20:01:33.557Z" }, + { url = "https://files.pythonhosted.org/packages/51/7f/023657f301a276e4ba1850f82749bc136f5a7e8768060c2e5d9744a22951/coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a", size = 220103, upload-time = "2025-09-21T20:01:34.929Z" }, + { url = "https://files.pythonhosted.org/packages/13/e4/eb12450f71b542a53972d19117ea5a5cea1cab3ac9e31b0b5d498df1bd5a/coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417", size = 218290, upload-time = "2025-09-21T20:01:36.455Z" }, + { url = "https://files.pythonhosted.org/packages/37/66/593f9be12fc19fb36711f19a5371af79a718537204d16ea1d36f16bd78d2/coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973", size = 218515, upload-time = "2025-09-21T20:01:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/66/80/4c49f7ae09cafdacc73fbc30949ffe77359635c168f4e9ff33c9ebb07838/coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c", size = 250020, upload-time = "2025-09-21T20:01:39.617Z" }, + { url = "https://files.pythonhosted.org/packages/a6/90/a64aaacab3b37a17aaedd83e8000142561a29eb262cede42d94a67f7556b/coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7", size = 252769, upload-time = "2025-09-21T20:01:41.341Z" }, + { url = "https://files.pythonhosted.org/packages/98/2e/2dda59afd6103b342e096f246ebc5f87a3363b5412609946c120f4e7750d/coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6", size = 253901, upload-time = "2025-09-21T20:01:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/dc/8d8119c9051d50f3119bb4a75f29f1e4a6ab9415cd1fa8bf22fcc3fb3b5f/coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59", size = 250413, upload-time = "2025-09-21T20:01:44.469Z" }, + { url = "https://files.pythonhosted.org/packages/98/b3/edaff9c5d79ee4d4b6d3fe046f2b1d799850425695b789d491a64225d493/coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b", size = 251820, upload-time = "2025-09-21T20:01:45.915Z" }, + { url = "https://files.pythonhosted.org/packages/11/25/9a0728564bb05863f7e513e5a594fe5ffef091b325437f5430e8cfb0d530/coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a", size = 249941, upload-time = "2025-09-21T20:01:47.296Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fd/ca2650443bfbef5b0e74373aac4df67b08180d2f184b482c41499668e258/coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb", size = 249519, upload-time = "2025-09-21T20:01:48.73Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/f692f125fb4299b6f963b0745124998ebb8e73ecdfce4ceceb06a8c6bec5/coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1", size = 251375, upload-time = "2025-09-21T20:01:50.529Z" }, + { url = "https://files.pythonhosted.org/packages/5e/75/61b9bbd6c7d24d896bfeec57acba78e0f8deac68e6baf2d4804f7aae1f88/coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256", size = 220699, upload-time = "2025-09-21T20:01:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f3/3bf7905288b45b075918d372498f1cf845b5b579b723c8fd17168018d5f5/coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba", size = 221512, upload-time = "2025-09-21T20:01:53.481Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/3e32dbe933979d05cf2dac5e697c8599cfe038aaf51223ab901e208d5a62/coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf", size = 220147, upload-time = "2025-09-21T20:01:55.2Z" }, + { url = "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d", size = 218320, upload-time = "2025-09-21T20:01:56.629Z" }, + { url = "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b", size = 218575, upload-time = "2025-09-21T20:01:58.203Z" }, + { url = "https://files.pythonhosted.org/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e", size = 249568, upload-time = "2025-09-21T20:01:59.748Z" }, + { url = "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b", size = 252174, upload-time = "2025-09-21T20:02:01.192Z" }, + { url = "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49", size = 253447, upload-time = "2025-09-21T20:02:02.701Z" }, + { url = "https://files.pythonhosted.org/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911", size = 249779, upload-time = "2025-09-21T20:02:04.185Z" }, + { url = "https://files.pythonhosted.org/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0", size = 251604, upload-time = "2025-09-21T20:02:06.034Z" }, + { url = "https://files.pythonhosted.org/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f", size = 249497, upload-time = "2025-09-21T20:02:07.619Z" }, + { url = "https://files.pythonhosted.org/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c", size = 249350, upload-time = "2025-09-21T20:02:10.34Z" }, + { url = "https://files.pythonhosted.org/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f", size = 251111, upload-time = "2025-09-21T20:02:12.122Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698", size = 220746, upload-time = "2025-09-21T20:02:13.919Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843", size = 221541, upload-time = "2025-09-21T20:02:15.57Z" }, + { url = "https://files.pythonhosted.org/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546", size = 220170, upload-time = "2025-09-21T20:02:17.395Z" }, + { url = "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c", size = 219029, upload-time = "2025-09-21T20:02:18.936Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15", size = 219259, upload-time = "2025-09-21T20:02:20.44Z" }, + { url = "https://files.pythonhosted.org/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4", size = 260592, upload-time = "2025-09-21T20:02:22.313Z" }, + { url = "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0", size = 262768, upload-time = "2025-09-21T20:02:24.287Z" }, + { url = "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0", size = 264995, upload-time = "2025-09-21T20:02:26.133Z" }, + { url = "https://files.pythonhosted.org/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65", size = 259546, upload-time = "2025-09-21T20:02:27.716Z" }, + { url = "https://files.pythonhosted.org/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541", size = 262544, upload-time = "2025-09-21T20:02:29.216Z" }, + { url = "https://files.pythonhosted.org/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6", size = 260308, upload-time = "2025-09-21T20:02:31.226Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999", size = 258920, upload-time = "2025-09-21T20:02:32.823Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2", size = 261434, upload-time = "2025-09-21T20:02:34.86Z" }, + { url = "https://files.pythonhosted.org/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a", size = 221403, upload-time = "2025-09-21T20:02:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb", size = 222469, upload-time = "2025-09-21T20:02:39.011Z" }, + { url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb", size = 220731, upload-time = "2025-09-21T20:02:40.939Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520", size = 218302, upload-time = "2025-09-21T20:02:42.527Z" }, + { url = "https://files.pythonhosted.org/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32", size = 218578, upload-time = "2025-09-21T20:02:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/05/e8/261cae479e85232828fb17ad536765c88dd818c8470aca690b0ac6feeaa3/coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f", size = 249629, upload-time = "2025-09-21T20:02:46.503Z" }, + { url = "https://files.pythonhosted.org/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a", size = 252162, upload-time = "2025-09-21T20:02:48.689Z" }, + { url = "https://files.pythonhosted.org/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360", size = 253517, upload-time = "2025-09-21T20:02:50.31Z" }, + { url = "https://files.pythonhosted.org/packages/a2/59/c5201c62dbf165dfbc91460f6dbbaa85a8b82cfa6131ac45d6c1bfb52deb/coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69", size = 249632, upload-time = "2025-09-21T20:02:51.971Z" }, + { url = "https://files.pythonhosted.org/packages/07/ae/5920097195291a51fb00b3a70b9bbd2edbfe3c84876a1762bd1ef1565ebc/coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14", size = 251520, upload-time = "2025-09-21T20:02:53.858Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3c/a815dde77a2981f5743a60b63df31cb322c944843e57dbd579326625a413/coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe", size = 249455, upload-time = "2025-09-21T20:02:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/aa/99/f5cdd8421ea656abefb6c0ce92556709db2265c41e8f9fc6c8ae0f7824c9/coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e", size = 249287, upload-time = "2025-09-21T20:02:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/c3/7a/e9a2da6a1fc5d007dd51fca083a663ab930a8c4d149c087732a5dbaa0029/coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd", size = 250946, upload-time = "2025-09-21T20:02:59.431Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5b/0b5799aa30380a949005a353715095d6d1da81927d6dbed5def2200a4e25/coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2", size = 221009, upload-time = "2025-09-21T20:03:01.324Z" }, + { url = "https://files.pythonhosted.org/packages/da/b0/e802fbb6eb746de006490abc9bb554b708918b6774b722bb3a0e6aa1b7de/coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681", size = 221804, upload-time = "2025-09-21T20:03:03.4Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e8/71d0c8e374e31f39e3389bb0bd19e527d46f00ea8571ec7ec8fd261d8b44/coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880", size = 220384, upload-time = "2025-09-21T20:03:05.111Z" }, + { url = "https://files.pythonhosted.org/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63", size = 219047, upload-time = "2025-09-21T20:03:06.795Z" }, + { url = "https://files.pythonhosted.org/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2", size = 219266, upload-time = "2025-09-21T20:03:08.495Z" }, + { url = "https://files.pythonhosted.org/packages/74/5c/183ffc817ba68e0b443b8c934c8795553eb0c14573813415bd59941ee165/coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d", size = 260767, upload-time = "2025-09-21T20:03:10.172Z" }, + { url = "https://files.pythonhosted.org/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0", size = 262931, upload-time = "2025-09-21T20:03:11.861Z" }, + { url = "https://files.pythonhosted.org/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699", size = 265186, upload-time = "2025-09-21T20:03:13.539Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8f/74ecc30607dd95ad50e3034221113ccb1c6d4e8085cc761134782995daae/coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9", size = 259470, upload-time = "2025-09-21T20:03:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/0f/55/79ff53a769f20d71b07023ea115c9167c0bb56f281320520cf64c5298a96/coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f", size = 262626, upload-time = "2025-09-21T20:03:17.673Z" }, + { url = "https://files.pythonhosted.org/packages/88/e2/dac66c140009b61ac3fc13af673a574b00c16efdf04f9b5c740703e953c0/coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1", size = 260386, upload-time = "2025-09-21T20:03:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f1/f48f645e3f33bb9ca8a496bc4a9671b52f2f353146233ebd7c1df6160440/coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0", size = 258852, upload-time = "2025-09-21T20:03:21.007Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3b/8442618972c51a7affeead957995cfa8323c0c9bcf8fa5a027421f720ff4/coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399", size = 261534, upload-time = "2025-09-21T20:03:23.12Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dc/101f3fa3a45146db0cb03f5b4376e24c0aac818309da23e2de0c75295a91/coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235", size = 221784, upload-time = "2025-09-21T20:03:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a1/74c51803fc70a8a40d7346660379e144be772bab4ac7bb6e6b905152345c/coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d", size = 222905, upload-time = "2025-09-21T20:03:26.93Z" }, + { url = "https://files.pythonhosted.org/packages/12/65/f116a6d2127df30bcafbceef0302d8a64ba87488bf6f73a6d8eebf060873/coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a", size = 220922, upload-time = "2025-09-21T20:03:28.672Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/d1c25053764b4c42eb294aae92ab617d2e4f803397f9c7c8295caa77a260/coverage-7.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3", size = 217978, upload-time = "2025-09-21T20:03:30.362Z" }, + { url = "https://files.pythonhosted.org/packages/52/2f/b9f9daa39b80ece0b9548bbb723381e29bc664822d9a12c2135f8922c22b/coverage-7.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c", size = 218370, upload-time = "2025-09-21T20:03:32.147Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6e/30d006c3b469e58449650642383dddf1c8fb63d44fdf92994bfd46570695/coverage-7.10.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396", size = 244802, upload-time = "2025-09-21T20:03:33.919Z" }, + { url = "https://files.pythonhosted.org/packages/b0/49/8a070782ce7e6b94ff6a0b6d7c65ba6bc3091d92a92cef4cd4eb0767965c/coverage-7.10.7-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40", size = 246625, upload-time = "2025-09-21T20:03:36.09Z" }, + { url = "https://files.pythonhosted.org/packages/6a/92/1c1c5a9e8677ce56d42b97bdaca337b2d4d9ebe703d8c174ede52dbabd5f/coverage-7.10.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594", size = 248399, upload-time = "2025-09-21T20:03:38.342Z" }, + { url = "https://files.pythonhosted.org/packages/c0/54/b140edee7257e815de7426d5d9846b58505dffc29795fff2dfb7f8a1c5a0/coverage-7.10.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a", size = 245142, upload-time = "2025-09-21T20:03:40.591Z" }, + { url = "https://files.pythonhosted.org/packages/e4/9e/6d6b8295940b118e8b7083b29226c71f6154f7ff41e9ca431f03de2eac0d/coverage-7.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b", size = 246284, upload-time = "2025-09-21T20:03:42.355Z" }, + { url = "https://files.pythonhosted.org/packages/db/e5/5e957ca747d43dbe4d9714358375c7546cb3cb533007b6813fc20fce37ad/coverage-7.10.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3", size = 244353, upload-time = "2025-09-21T20:03:44.218Z" }, + { url = "https://files.pythonhosted.org/packages/9a/45/540fc5cc92536a1b783b7ef99450bd55a4b3af234aae35a18a339973ce30/coverage-7.10.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0", size = 244430, upload-time = "2025-09-21T20:03:46.065Z" }, + { url = "https://files.pythonhosted.org/packages/75/0b/8287b2e5b38c8fe15d7e3398849bb58d382aedc0864ea0fa1820e8630491/coverage-7.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f", size = 245311, upload-time = "2025-09-21T20:03:48.19Z" }, + { url = "https://files.pythonhosted.org/packages/0c/1d/29724999984740f0c86d03e6420b942439bf5bd7f54d4382cae386a9d1e9/coverage-7.10.7-cp39-cp39-win32.whl", hash = "sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431", size = 220500, upload-time = "2025-09-21T20:03:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/43/11/4b1e6b129943f905ca54c339f343877b55b365ae2558806c1be4f7476ed5/coverage-7.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07", size = 221408, upload-time = "2025-09-21T20:03:51.803Z" }, + { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" }, ] [package.optional-dependencies] @@ -314,9 +315,9 @@ toml = [ name = "distlib" version = "0.3.9" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 } +sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923, upload-time = "2024-10-09T18:35:47.551Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, + { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973, upload-time = "2024-10-09T18:35:44.272Z" }, ] [[package]] @@ -328,36 +329,36 @@ dependencies = [ { name = "sqlparse", marker = "python_full_version >= '3.10'" }, { name = "tzdata", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/17/f2/3e57ef696b95067e05ae206171e47a8e53b9c84eec56198671ef9eaa51a6/django-5.2.11.tar.gz", hash = "sha256:7f2d292ad8b9ee35e405d965fbbad293758b858c34bbf7f3df551aeeac6f02d3", size = 10885017 } +sdist = { url = "https://files.pythonhosted.org/packages/17/f2/3e57ef696b95067e05ae206171e47a8e53b9c84eec56198671ef9eaa51a6/django-5.2.11.tar.gz", hash = "sha256:7f2d292ad8b9ee35e405d965fbbad293758b858c34bbf7f3df551aeeac6f02d3", size = 10885017, upload-time = "2026-02-03T13:52:50.554Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/91/a7/2b112ab430575bf3135b8304ac372248500d99c352f777485f53fdb9537e/django-5.2.11-py3-none-any.whl", hash = "sha256:e7130df33ada9ab5e5e929bc19346a20fe383f5454acb2cc004508f242ee92c0", size = 8291375 }, + { url = "https://files.pythonhosted.org/packages/91/a7/2b112ab430575bf3135b8304ac372248500d99c352f777485f53fdb9537e/django-5.2.11-py3-none-any.whl", hash = "sha256:e7130df33ada9ab5e5e929bc19346a20fe383f5454acb2cc004508f242ee92c0", size = 8291375, upload-time = "2026-02-03T13:52:42.47Z" }, ] [[package]] name = "docutils" version = "0.21.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444 } +sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408 }, + { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, ] [[package]] name = "exceptiongroup" version = "1.2.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883, upload-time = "2024-07-12T22:26:00.161Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453, upload-time = "2024-07-12T22:25:58.476Z" }, ] [[package]] name = "execnet" version = "2.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622 } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708 }, + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, ] [[package]] @@ -367,9 +368,9 @@ source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.10'", ] -sdist = { url = "https://files.pythonhosted.org/packages/00/0b/c506e9e44e4c4b6c89fcecda23dc115bf8e7ff7eb127e0cb9c114cbc9a15/filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81", size = 12441 } +sdist = { url = "https://files.pythonhosted.org/packages/00/0b/c506e9e44e4c4b6c89fcecda23dc115bf8e7ff7eb127e0cb9c114cbc9a15/filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81", size = 12441, upload-time = "2023-06-12T22:02:09.617Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/45/ec3407adf6f6b5bf867a4462b2b0af27597a26bd3cd6e2534cb6ab029938/filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec", size = 10923 }, + { url = "https://files.pythonhosted.org/packages/00/45/ec3407adf6f6b5bf867a4462b2b0af27597a26bd3cd6e2534cb6ab029938/filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec", size = 10923, upload-time = "2023-06-12T22:02:08.03Z" }, ] [[package]] @@ -379,9 +380,9 @@ source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.10'", ] -sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485 } +sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701 }, + { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, ] [[package]] @@ -394,45 +395,45 @@ dependencies = [ { name = "sphinx" }, { name = "sphinx-basic-ng" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a0/e2/d351d69a9a9e4badb4a5be062c2d0e87bd9e6c23b5e57337fef14bef34c8/furo-2024.8.6.tar.gz", hash = "sha256:b63e4cee8abfc3136d3bc03a3d45a76a850bada4d6374d24c1716b0e01394a01", size = 1661506 } +sdist = { url = "https://files.pythonhosted.org/packages/a0/e2/d351d69a9a9e4badb4a5be062c2d0e87bd9e6c23b5e57337fef14bef34c8/furo-2024.8.6.tar.gz", hash = "sha256:b63e4cee8abfc3136d3bc03a3d45a76a850bada4d6374d24c1716b0e01394a01", size = 1661506, upload-time = "2024-08-06T08:07:57.567Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/48/e791a7ed487dbb9729ef32bb5d1af16693d8925f4366befef54119b2e576/furo-2024.8.6-py3-none-any.whl", hash = "sha256:6cd97c58b47813d3619e63e9081169880fbe331f0ca883c871ff1f3f11814f5c", size = 341333 }, + { url = "https://files.pythonhosted.org/packages/27/48/e791a7ed487dbb9729ef32bb5d1af16693d8925f4366befef54119b2e576/furo-2024.8.6-py3-none-any.whl", hash = "sha256:6cd97c58b47813d3619e63e9081169880fbe331f0ca883c871ff1f3f11814f5c", size = 341333, upload-time = "2024-08-06T08:07:54.44Z" }, ] [[package]] name = "h11" version = "0.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 }, + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] [[package]] name = "identify" version = "2.5.24" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c4/f8/498e13e408d25ee6ff04aa0acbf91ad8e9caae74be91720fc0e811e649b7/identify-2.5.24.tar.gz", hash = "sha256:0aac67d5b4812498056d28a9a512a483f5085cc28640b02b258a59dac34301d4", size = 98886 } +sdist = { url = "https://files.pythonhosted.org/packages/c4/f8/498e13e408d25ee6ff04aa0acbf91ad8e9caae74be91720fc0e811e649b7/identify-2.5.24.tar.gz", hash = "sha256:0aac67d5b4812498056d28a9a512a483f5085cc28640b02b258a59dac34301d4", size = 98886, upload-time = "2023-05-03T14:45:38.863Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4f/fd/2c46fba2bc032ba4c970bb8de59d25187087d7138a0ebf7c1dcc91d94f01/identify-2.5.24-py2.py3-none-any.whl", hash = "sha256:986dbfb38b1140e763e413e6feb44cd731faf72d1909543178aa79b0e258265d", size = 98826 }, + { url = "https://files.pythonhosted.org/packages/4f/fd/2c46fba2bc032ba4c970bb8de59d25187087d7138a0ebf7c1dcc91d94f01/identify-2.5.24-py2.py3-none-any.whl", hash = "sha256:986dbfb38b1140e763e413e6feb44cd731faf72d1909543178aa79b0e258265d", size = 98826, upload-time = "2023-05-03T14:45:34.823Z" }, ] [[package]] name = "idna" version = "3.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, ] [[package]] name = "imagesize" version = "1.4.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026 } +sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026, upload-time = "2022-07-01T12:21:05.687Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769 }, + { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" }, ] [[package]] @@ -442,18 +443,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "zipp", marker = "python_full_version < '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/82/f6e29c8d5c098b6be61460371c2c5591f4a335923639edec43b3830650a4/importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4", size = 53569 } +sdist = { url = "https://files.pythonhosted.org/packages/a3/82/f6e29c8d5c098b6be61460371c2c5591f4a335923639edec43b3830650a4/importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4", size = 53569, upload-time = "2023-06-18T21:44:35.024Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/94/64287b38c7de4c90683630338cf28f129decbba0a44f0c6db35a873c73c4/importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5", size = 22934 }, + { url = "https://files.pythonhosted.org/packages/ff/94/64287b38c7de4c90683630338cf28f129decbba0a44f0c6db35a873c73c4/importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5", size = 22934, upload-time = "2023-06-18T21:44:33.441Z" }, ] [[package]] name = "iniconfig" version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646, upload-time = "2023-01-07T11:08:11.254Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892, upload-time = "2023-01-07T11:08:09.864Z" }, ] [[package]] @@ -463,9 +464,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] [[package]] @@ -475,77 +476,77 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, ] [[package]] name = "markupsafe" version = "3.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357 }, - { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393 }, - { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732 }, - { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866 }, - { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964 }, - { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977 }, - { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366 }, - { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091 }, - { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065 }, - { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514 }, - { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, - { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, - { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, - { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, - { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, - { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, - { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, - { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, - { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, - { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, - { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, - { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, - { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, - { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, - { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, - { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, - { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, - { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, - { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, - { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, - { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, - { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, - { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, - { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, - { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, - { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, - { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, - { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, - { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, - { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, - { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, - { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, - { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, - { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, - { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, - { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, - { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, - { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, - { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, - { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, - { url = "https://files.pythonhosted.org/packages/a7/ea/9b1530c3fdeeca613faeb0fb5cbcf2389d816072fab72a71b45749ef6062/MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", size = 14344 }, - { url = "https://files.pythonhosted.org/packages/4b/c2/fbdbfe48848e7112ab05e627e718e854d20192b674952d9042ebd8c9e5de/MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", size = 12389 }, - { url = "https://files.pythonhosted.org/packages/f0/25/7a7c6e4dbd4f867d95d94ca15449e91e52856f6ed1905d58ef1de5e211d0/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", size = 21607 }, - { url = "https://files.pythonhosted.org/packages/53/8f/f339c98a178f3c1e545622206b40986a4c3307fe39f70ccd3d9df9a9e425/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", size = 20728 }, - { url = "https://files.pythonhosted.org/packages/1a/03/8496a1a78308456dbd50b23a385c69b41f2e9661c67ea1329849a598a8f9/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", size = 20826 }, - { url = "https://files.pythonhosted.org/packages/e6/cf/0a490a4bd363048c3022f2f475c8c05582179bb179defcee4766fb3dcc18/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", size = 21843 }, - { url = "https://files.pythonhosted.org/packages/19/a3/34187a78613920dfd3cdf68ef6ce5e99c4f3417f035694074beb8848cd77/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", size = 21219 }, - { url = "https://files.pythonhosted.org/packages/17/d8/5811082f85bb88410ad7e452263af048d685669bbbfb7b595e8689152498/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", size = 20946 }, - { url = "https://files.pythonhosted.org/packages/7c/31/bd635fb5989440d9365c5e3c47556cfea121c7803f5034ac843e8f37c2f2/MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", size = 15063 }, - { url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506 }, +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357, upload-time = "2024-10-18T15:20:51.44Z" }, + { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393, upload-time = "2024-10-18T15:20:52.426Z" }, + { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732, upload-time = "2024-10-18T15:20:53.578Z" }, + { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866, upload-time = "2024-10-18T15:20:55.06Z" }, + { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964, upload-time = "2024-10-18T15:20:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977, upload-time = "2024-10-18T15:20:57.189Z" }, + { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366, upload-time = "2024-10-18T15:20:58.235Z" }, + { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091, upload-time = "2024-10-18T15:20:59.235Z" }, + { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065, upload-time = "2024-10-18T15:21:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514, upload-time = "2024-10-18T15:21:01.122Z" }, + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, + { url = "https://files.pythonhosted.org/packages/a7/ea/9b1530c3fdeeca613faeb0fb5cbcf2389d816072fab72a71b45749ef6062/MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", size = 14344, upload-time = "2024-10-18T15:21:43.721Z" }, + { url = "https://files.pythonhosted.org/packages/4b/c2/fbdbfe48848e7112ab05e627e718e854d20192b674952d9042ebd8c9e5de/MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", size = 12389, upload-time = "2024-10-18T15:21:44.666Z" }, + { url = "https://files.pythonhosted.org/packages/f0/25/7a7c6e4dbd4f867d95d94ca15449e91e52856f6ed1905d58ef1de5e211d0/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", size = 21607, upload-time = "2024-10-18T15:21:45.452Z" }, + { url = "https://files.pythonhosted.org/packages/53/8f/f339c98a178f3c1e545622206b40986a4c3307fe39f70ccd3d9df9a9e425/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", size = 20728, upload-time = "2024-10-18T15:21:46.295Z" }, + { url = "https://files.pythonhosted.org/packages/1a/03/8496a1a78308456dbd50b23a385c69b41f2e9661c67ea1329849a598a8f9/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", size = 20826, upload-time = "2024-10-18T15:21:47.134Z" }, + { url = "https://files.pythonhosted.org/packages/e6/cf/0a490a4bd363048c3022f2f475c8c05582179bb179defcee4766fb3dcc18/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", size = 21843, upload-time = "2024-10-18T15:21:48.334Z" }, + { url = "https://files.pythonhosted.org/packages/19/a3/34187a78613920dfd3cdf68ef6ce5e99c4f3417f035694074beb8848cd77/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", size = 21219, upload-time = "2024-10-18T15:21:49.587Z" }, + { url = "https://files.pythonhosted.org/packages/17/d8/5811082f85bb88410ad7e452263af048d685669bbbfb7b595e8689152498/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", size = 20946, upload-time = "2024-10-18T15:21:50.441Z" }, + { url = "https://files.pythonhosted.org/packages/7c/31/bd635fb5989440d9365c5e3c47556cfea121c7803f5034ac843e8f37c2f2/MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", size = 15063, upload-time = "2024-10-18T15:21:51.385Z" }, + { url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506, upload-time = "2024-10-18T15:21:52.974Z" }, ] [[package]] @@ -555,18 +556,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/03/a2ecab526543b152300717cf232bb4bb8605b6edb946c845016fa9c9c9fd/mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5", size = 43542 } +sdist = { url = "https://files.pythonhosted.org/packages/19/03/a2ecab526543b152300717cf232bb4bb8605b6edb946c845016fa9c9c9fd/mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5", size = 43542, upload-time = "2024-09-09T20:27:49.564Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/f7/7782a043553ee469c1ff49cfa1cdace2d6bf99a1f333cf38676b3ddf30da/mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636", size = 55316 }, + { url = "https://files.pythonhosted.org/packages/a7/f7/7782a043553ee469c1ff49cfa1cdace2d6bf99a1f333cf38676b3ddf30da/mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636", size = 55316, upload-time = "2024-09-09T20:27:48.397Z" }, ] [[package]] name = "mdurl" version = "0.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] [[package]] @@ -581,24 +582,24 @@ dependencies = [ { name = "tomli", marker = "python_full_version < '3.10'" }, { name = "typing-extensions", marker = "python_full_version < '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b3/28/d8a8233ff167d06108e53b7aefb4a8d7350adbbf9d7abd980f17fdb7a3a6/mypy-1.4.1.tar.gz", hash = "sha256:9bbcd9ab8ea1f2e1c8031c21445b511442cc45c89951e49bbf852cbb70755b1b", size = 2855162 } +sdist = { url = "https://files.pythonhosted.org/packages/b3/28/d8a8233ff167d06108e53b7aefb4a8d7350adbbf9d7abd980f17fdb7a3a6/mypy-1.4.1.tar.gz", hash = "sha256:9bbcd9ab8ea1f2e1c8031c21445b511442cc45c89951e49bbf852cbb70755b1b", size = 2855162, upload-time = "2023-06-25T23:22:54.364Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/3b/1c7363863b56c059f60a1dfdca9ac774a22ba64b7a4da0ee58ee53e5243f/mypy-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:566e72b0cd6598503e48ea610e0052d1b8168e60a46e0bfd34b3acf2d57f96a8", size = 10451043 }, - { url = "https://files.pythonhosted.org/packages/a7/24/6f0df1874118839db1155fed62a4bd7e80c181367ff8ea07d40fbaffcfb4/mypy-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca637024ca67ab24a7fd6f65d280572c3794665eaf5edcc7e90a866544076878", size = 9542079 }, - { url = "https://files.pythonhosted.org/packages/04/5c/deeac94fcccd11aa621e6b350df333e1b809b11443774ea67582cc0205da/mypy-1.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dde1d180cd84f0624c5dcaaa89c89775550a675aff96b5848de78fb11adabcd", size = 11974913 }, - { url = "https://files.pythonhosted.org/packages/e5/2f/de3c455c54e8cf5e37ea38705c1920f2df470389f8fc051084d2dd8c9c59/mypy-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8c4d8e89aa7de683e2056a581ce63c46a0c41e31bd2b6d34144e2c80f5ea53dc", size = 12044492 }, - { url = "https://files.pythonhosted.org/packages/e7/d3/6f65357dcb68109946de70cd55bd2e60f10114f387471302f48d54ff5dae/mypy-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:bfdca17c36ae01a21274a3c387a63aa1aafe72bff976522886869ef131b937f1", size = 8831655 }, - { url = "https://files.pythonhosted.org/packages/94/01/e34e37a044325af4d4af9825c15e8a0d26d89b5a9624b4d0908449d3411b/mypy-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7549fbf655e5825d787bbc9ecf6028731973f78088fbca3a1f4145c39ef09462", size = 10338636 }, - { url = "https://files.pythonhosted.org/packages/92/58/ccc0b714ecbd1a64b34d8ce1c38763ff6431de1d82551904ecc3711fbe05/mypy-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98324ec3ecf12296e6422939e54763faedbfcc502ea4a4c38502082711867258", size = 9444172 }, - { url = "https://files.pythonhosted.org/packages/73/72/dfc0b46e6905eafd598e7c48c0c4f2e232647e4e36547425c64e6c850495/mypy-1.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:141dedfdbfe8a04142881ff30ce6e6653c9685b354876b12e4fe6c78598b45e2", size = 11855450 }, - { url = "https://files.pythonhosted.org/packages/66/f4/60739a2d336f3adf5628e7c9b920d16e8af6dc078550d615e4ba2a1d7759/mypy-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8207b7105829eca6f3d774f64a904190bb2231de91b8b186d21ffd98005f14a7", size = 11928679 }, - { url = "https://files.pythonhosted.org/packages/8c/26/6ff2b55bf8b605a4cc898883654c2ca4dd4feedf0bb04ecaacf60d165cde/mypy-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:16f0db5b641ba159eff72cff08edc3875f2b62b2fa2bc24f68c1e7a4e8232d01", size = 8831134 }, - { url = "https://files.pythonhosted.org/packages/1d/1b/9050b5c444ef82c3d59bdbf21f91b259cf20b2ac1df37d55bc6b91d609a1/mypy-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c482e1246726616088532b5e964e39765b6d1520791348e6c9dc3af25b233828", size = 10447897 }, - { url = "https://files.pythonhosted.org/packages/da/00/ac2b58b321d85cac25be0dcd1bc2427dfc6cf403283fc205a0031576f14b/mypy-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43b592511672017f5b1a483527fd2684347fdffc041c9ef53428c8dc530f79a3", size = 9534091 }, - { url = "https://files.pythonhosted.org/packages/c4/10/26240f14e854a95af87d577b288d607ebe0ccb75cb37052f6386402f022d/mypy-1.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34a9239d5b3502c17f07fd7c0b2ae6b7dd7d7f6af35fbb5072c6208e76295816", size = 11970165 }, - { url = "https://files.pythonhosted.org/packages/b7/34/a3edaec8762181bfe97439c7e094f4c2f411ed9b79ac8f4d72156e88d5ce/mypy-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5703097c4936bbb9e9bce41478c8d08edd2865e177dc4c52be759f81ee4dd26c", size = 12040792 }, - { url = "https://files.pythonhosted.org/packages/d1/f3/0d0622d5a83859a992b01741a7b97949d6fb9efc9f05f20a09f0df10dc1e/mypy-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e02d700ec8d9b1859790c0475df4e4092c7bf3272a4fd2c9f33d87fac4427b8f", size = 8831367 }, - { url = "https://files.pythonhosted.org/packages/3d/9a/e13addb8d652cb068f835ac2746d9d42f85b730092f581bb17e2059c28f1/mypy-1.4.1-py3-none-any.whl", hash = "sha256:45d32cec14e7b97af848bddd97d85ea4f0db4d5a149ed9676caa4eb2f7402bb4", size = 2451741 }, + { url = "https://files.pythonhosted.org/packages/fb/3b/1c7363863b56c059f60a1dfdca9ac774a22ba64b7a4da0ee58ee53e5243f/mypy-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:566e72b0cd6598503e48ea610e0052d1b8168e60a46e0bfd34b3acf2d57f96a8", size = 10451043, upload-time = "2023-06-25T23:22:02.502Z" }, + { url = "https://files.pythonhosted.org/packages/a7/24/6f0df1874118839db1155fed62a4bd7e80c181367ff8ea07d40fbaffcfb4/mypy-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca637024ca67ab24a7fd6f65d280572c3794665eaf5edcc7e90a866544076878", size = 9542079, upload-time = "2023-06-25T23:22:37.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5c/deeac94fcccd11aa621e6b350df333e1b809b11443774ea67582cc0205da/mypy-1.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dde1d180cd84f0624c5dcaaa89c89775550a675aff96b5848de78fb11adabcd", size = 11974913, upload-time = "2023-06-25T23:21:14.603Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2f/de3c455c54e8cf5e37ea38705c1920f2df470389f8fc051084d2dd8c9c59/mypy-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8c4d8e89aa7de683e2056a581ce63c46a0c41e31bd2b6d34144e2c80f5ea53dc", size = 12044492, upload-time = "2023-06-25T23:22:17.551Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d3/6f65357dcb68109946de70cd55bd2e60f10114f387471302f48d54ff5dae/mypy-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:bfdca17c36ae01a21274a3c387a63aa1aafe72bff976522886869ef131b937f1", size = 8831655, upload-time = "2023-06-25T23:21:40.201Z" }, + { url = "https://files.pythonhosted.org/packages/94/01/e34e37a044325af4d4af9825c15e8a0d26d89b5a9624b4d0908449d3411b/mypy-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7549fbf655e5825d787bbc9ecf6028731973f78088fbca3a1f4145c39ef09462", size = 10338636, upload-time = "2023-06-25T23:22:43.45Z" }, + { url = "https://files.pythonhosted.org/packages/92/58/ccc0b714ecbd1a64b34d8ce1c38763ff6431de1d82551904ecc3711fbe05/mypy-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98324ec3ecf12296e6422939e54763faedbfcc502ea4a4c38502082711867258", size = 9444172, upload-time = "2023-06-25T23:21:25.502Z" }, + { url = "https://files.pythonhosted.org/packages/73/72/dfc0b46e6905eafd598e7c48c0c4f2e232647e4e36547425c64e6c850495/mypy-1.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:141dedfdbfe8a04142881ff30ce6e6653c9685b354876b12e4fe6c78598b45e2", size = 11855450, upload-time = "2023-06-25T23:21:37.234Z" }, + { url = "https://files.pythonhosted.org/packages/66/f4/60739a2d336f3adf5628e7c9b920d16e8af6dc078550d615e4ba2a1d7759/mypy-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8207b7105829eca6f3d774f64a904190bb2231de91b8b186d21ffd98005f14a7", size = 11928679, upload-time = "2023-06-25T23:22:40.757Z" }, + { url = "https://files.pythonhosted.org/packages/8c/26/6ff2b55bf8b605a4cc898883654c2ca4dd4feedf0bb04ecaacf60d165cde/mypy-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:16f0db5b641ba159eff72cff08edc3875f2b62b2fa2bc24f68c1e7a4e8232d01", size = 8831134, upload-time = "2023-06-25T23:22:09.178Z" }, + { url = "https://files.pythonhosted.org/packages/1d/1b/9050b5c444ef82c3d59bdbf21f91b259cf20b2ac1df37d55bc6b91d609a1/mypy-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c482e1246726616088532b5e964e39765b6d1520791348e6c9dc3af25b233828", size = 10447897, upload-time = "2023-06-25T23:21:22.06Z" }, + { url = "https://files.pythonhosted.org/packages/da/00/ac2b58b321d85cac25be0dcd1bc2427dfc6cf403283fc205a0031576f14b/mypy-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43b592511672017f5b1a483527fd2684347fdffc041c9ef53428c8dc530f79a3", size = 9534091, upload-time = "2023-06-25T23:22:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/26240f14e854a95af87d577b288d607ebe0ccb75cb37052f6386402f022d/mypy-1.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34a9239d5b3502c17f07fd7c0b2ae6b7dd7d7f6af35fbb5072c6208e76295816", size = 11970165, upload-time = "2023-06-25T23:22:05.673Z" }, + { url = "https://files.pythonhosted.org/packages/b7/34/a3edaec8762181bfe97439c7e094f4c2f411ed9b79ac8f4d72156e88d5ce/mypy-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5703097c4936bbb9e9bce41478c8d08edd2865e177dc4c52be759f81ee4dd26c", size = 12040792, upload-time = "2023-06-25T23:21:49.878Z" }, + { url = "https://files.pythonhosted.org/packages/d1/f3/0d0622d5a83859a992b01741a7b97949d6fb9efc9f05f20a09f0df10dc1e/mypy-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e02d700ec8d9b1859790c0475df4e4092c7bf3272a4fd2c9f33d87fac4427b8f", size = 8831367, upload-time = "2023-06-25T23:21:43.065Z" }, + { url = "https://files.pythonhosted.org/packages/3d/9a/e13addb8d652cb068f835ac2746d9d42f85b730092f581bb17e2059c28f1/mypy-1.4.1-py3-none-any.whl", hash = "sha256:45d32cec14e7b97af848bddd97d85ea4f0db4d5a149ed9676caa4eb2f7402bb4", size = 2451741, upload-time = "2023-06-25T23:22:49.033Z" }, ] [[package]] @@ -613,48 +614,48 @@ dependencies = [ { name = "tomli", marker = "python_full_version == '3.10.*'" }, { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/eb/2c92d8ea1e684440f54fa49ac5d9a5f19967b7b472a281f419e69a8d228e/mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6", size = 3216051 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/7a/87ae2adb31d68402da6da1e5f30c07ea6063e9f09b5e7cfc9dfa44075e74/mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb", size = 11211002 }, - { url = "https://files.pythonhosted.org/packages/e1/23/eada4c38608b444618a132be0d199b280049ded278b24cbb9d3fc59658e4/mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0", size = 10358400 }, - { url = "https://files.pythonhosted.org/packages/43/c9/d6785c6f66241c62fd2992b05057f404237deaad1566545e9f144ced07f5/mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d", size = 12095172 }, - { url = "https://files.pythonhosted.org/packages/c3/62/daa7e787770c83c52ce2aaf1a111eae5893de9e004743f51bfcad9e487ec/mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b", size = 12828732 }, - { url = "https://files.pythonhosted.org/packages/1b/a2/5fb18318a3637f29f16f4e41340b795da14f4751ef4f51c99ff39ab62e52/mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427", size = 13012197 }, - { url = "https://files.pythonhosted.org/packages/28/99/e153ce39105d164b5f02c06c35c7ba958aaff50a2babba7d080988b03fe7/mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f", size = 9780836 }, - { url = "https://files.pythonhosted.org/packages/da/11/a9422850fd506edbcdc7f6090682ecceaf1f87b9dd847f9df79942da8506/mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c", size = 11120432 }, - { url = "https://files.pythonhosted.org/packages/b6/9e/47e450fd39078d9c02d620545b2cb37993a8a8bdf7db3652ace2f80521ca/mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1", size = 10279515 }, - { url = "https://files.pythonhosted.org/packages/01/b5/6c8d33bd0f851a7692a8bfe4ee75eb82b6983a3cf39e5e32a5d2a723f0c1/mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8", size = 12025791 }, - { url = "https://files.pythonhosted.org/packages/f0/4c/e10e2c46ea37cab5c471d0ddaaa9a434dc1d28650078ac1b56c2d7b9b2e4/mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f", size = 12749203 }, - { url = "https://files.pythonhosted.org/packages/88/55/beacb0c69beab2153a0f57671ec07861d27d735a0faff135a494cd4f5020/mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1", size = 12885900 }, - { url = "https://files.pythonhosted.org/packages/a2/75/8c93ff7f315c4d086a2dfcde02f713004357d70a163eddb6c56a6a5eff40/mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae", size = 9777869 }, - { url = "https://files.pythonhosted.org/packages/43/1b/b38c079609bb4627905b74fc6a49849835acf68547ac33d8ceb707de5f52/mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14", size = 11266668 }, - { url = "https://files.pythonhosted.org/packages/6b/75/2ed0d2964c1ffc9971c729f7a544e9cd34b2cdabbe2d11afd148d7838aa2/mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9", size = 10254060 }, - { url = "https://files.pythonhosted.org/packages/a1/5f/7b8051552d4da3c51bbe8fcafffd76a6823779101a2b198d80886cd8f08e/mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11", size = 11933167 }, - { url = "https://files.pythonhosted.org/packages/04/90/f53971d3ac39d8b68bbaab9a4c6c58c8caa4d5fd3d587d16f5927eeeabe1/mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e", size = 12864341 }, - { url = "https://files.pythonhosted.org/packages/03/d2/8bc0aeaaf2e88c977db41583559319f1821c069e943ada2701e86d0430b7/mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89", size = 12972991 }, - { url = "https://files.pythonhosted.org/packages/6f/17/07815114b903b49b0f2cf7499f1c130e5aa459411596668267535fe9243c/mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b", size = 9879016 }, - { url = "https://files.pythonhosted.org/packages/9e/15/bb6a686901f59222275ab228453de741185f9d54fecbaacec041679496c6/mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255", size = 11252097 }, - { url = "https://files.pythonhosted.org/packages/f8/b3/8b0f74dfd072c802b7fa368829defdf3ee1566ba74c32a2cb2403f68024c/mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34", size = 10239728 }, - { url = "https://files.pythonhosted.org/packages/c5/9b/4fd95ab20c52bb5b8c03cc49169be5905d931de17edfe4d9d2986800b52e/mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a", size = 11924965 }, - { url = "https://files.pythonhosted.org/packages/56/9d/4a236b9c57f5d8f08ed346914b3f091a62dd7e19336b2b2a0d85485f82ff/mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9", size = 12867660 }, - { url = "https://files.pythonhosted.org/packages/40/88/a61a5497e2f68d9027de2bb139c7bb9abaeb1be1584649fa9d807f80a338/mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd", size = 12969198 }, - { url = "https://files.pythonhosted.org/packages/54/da/3d6fc5d92d324701b0c23fb413c853892bfe0e1dbe06c9138037d459756b/mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107", size = 9885276 }, - { url = "https://files.pythonhosted.org/packages/ca/1f/186d133ae2514633f8558e78cd658070ba686c0e9275c5a5c24a1e1f0d67/mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35", size = 11200493 }, - { url = "https://files.pythonhosted.org/packages/af/fc/4842485d034e38a4646cccd1369f6b1ccd7bc86989c52770d75d719a9941/mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc", size = 10357702 }, - { url = "https://files.pythonhosted.org/packages/b4/e6/457b83f2d701e23869cfec013a48a12638f75b9d37612a9ddf99072c1051/mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9", size = 12091104 }, - { url = "https://files.pythonhosted.org/packages/f1/bf/76a569158db678fee59f4fd30b8e7a0d75bcbaeef49edd882a0d63af6d66/mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb", size = 12830167 }, - { url = "https://files.pythonhosted.org/packages/43/bc/0bc6b694b3103de9fed61867f1c8bd33336b913d16831431e7cb48ef1c92/mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60", size = 13013834 }, - { url = "https://files.pythonhosted.org/packages/b0/79/5f5ec47849b6df1e6943d5fd8e6632fbfc04b4fd4acfa5a5a9535d11b4e2/mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c", size = 9781231 }, - { url = "https://files.pythonhosted.org/packages/a0/b5/32dd67b69a16d088e533962e5044e51004176a9952419de0370cdaead0f8/mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1", size = 2752905 }, +sdist = { url = "https://files.pythonhosted.org/packages/b9/eb/2c92d8ea1e684440f54fa49ac5d9a5f19967b7b472a281f419e69a8d228e/mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6", size = 3216051, upload-time = "2024-12-30T16:39:07.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/7a/87ae2adb31d68402da6da1e5f30c07ea6063e9f09b5e7cfc9dfa44075e74/mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb", size = 11211002, upload-time = "2024-12-30T16:37:22.435Z" }, + { url = "https://files.pythonhosted.org/packages/e1/23/eada4c38608b444618a132be0d199b280049ded278b24cbb9d3fc59658e4/mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0", size = 10358400, upload-time = "2024-12-30T16:37:53.526Z" }, + { url = "https://files.pythonhosted.org/packages/43/c9/d6785c6f66241c62fd2992b05057f404237deaad1566545e9f144ced07f5/mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d", size = 12095172, upload-time = "2024-12-30T16:37:50.332Z" }, + { url = "https://files.pythonhosted.org/packages/c3/62/daa7e787770c83c52ce2aaf1a111eae5893de9e004743f51bfcad9e487ec/mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b", size = 12828732, upload-time = "2024-12-30T16:37:29.96Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a2/5fb18318a3637f29f16f4e41340b795da14f4751ef4f51c99ff39ab62e52/mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427", size = 13012197, upload-time = "2024-12-30T16:38:05.037Z" }, + { url = "https://files.pythonhosted.org/packages/28/99/e153ce39105d164b5f02c06c35c7ba958aaff50a2babba7d080988b03fe7/mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f", size = 9780836, upload-time = "2024-12-30T16:37:19.726Z" }, + { url = "https://files.pythonhosted.org/packages/da/11/a9422850fd506edbcdc7f6090682ecceaf1f87b9dd847f9df79942da8506/mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c", size = 11120432, upload-time = "2024-12-30T16:37:11.533Z" }, + { url = "https://files.pythonhosted.org/packages/b6/9e/47e450fd39078d9c02d620545b2cb37993a8a8bdf7db3652ace2f80521ca/mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1", size = 10279515, upload-time = "2024-12-30T16:37:40.724Z" }, + { url = "https://files.pythonhosted.org/packages/01/b5/6c8d33bd0f851a7692a8bfe4ee75eb82b6983a3cf39e5e32a5d2a723f0c1/mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8", size = 12025791, upload-time = "2024-12-30T16:36:58.73Z" }, + { url = "https://files.pythonhosted.org/packages/f0/4c/e10e2c46ea37cab5c471d0ddaaa9a434dc1d28650078ac1b56c2d7b9b2e4/mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f", size = 12749203, upload-time = "2024-12-30T16:37:03.741Z" }, + { url = "https://files.pythonhosted.org/packages/88/55/beacb0c69beab2153a0f57671ec07861d27d735a0faff135a494cd4f5020/mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1", size = 12885900, upload-time = "2024-12-30T16:37:57.948Z" }, + { url = "https://files.pythonhosted.org/packages/a2/75/8c93ff7f315c4d086a2dfcde02f713004357d70a163eddb6c56a6a5eff40/mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae", size = 9777869, upload-time = "2024-12-30T16:37:33.428Z" }, + { url = "https://files.pythonhosted.org/packages/43/1b/b38c079609bb4627905b74fc6a49849835acf68547ac33d8ceb707de5f52/mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14", size = 11266668, upload-time = "2024-12-30T16:38:02.211Z" }, + { url = "https://files.pythonhosted.org/packages/6b/75/2ed0d2964c1ffc9971c729f7a544e9cd34b2cdabbe2d11afd148d7838aa2/mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9", size = 10254060, upload-time = "2024-12-30T16:37:46.131Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5f/7b8051552d4da3c51bbe8fcafffd76a6823779101a2b198d80886cd8f08e/mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11", size = 11933167, upload-time = "2024-12-30T16:37:43.534Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/f53971d3ac39d8b68bbaab9a4c6c58c8caa4d5fd3d587d16f5927eeeabe1/mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e", size = 12864341, upload-time = "2024-12-30T16:37:36.249Z" }, + { url = "https://files.pythonhosted.org/packages/03/d2/8bc0aeaaf2e88c977db41583559319f1821c069e943ada2701e86d0430b7/mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89", size = 12972991, upload-time = "2024-12-30T16:37:06.743Z" }, + { url = "https://files.pythonhosted.org/packages/6f/17/07815114b903b49b0f2cf7499f1c130e5aa459411596668267535fe9243c/mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b", size = 9879016, upload-time = "2024-12-30T16:37:15.02Z" }, + { url = "https://files.pythonhosted.org/packages/9e/15/bb6a686901f59222275ab228453de741185f9d54fecbaacec041679496c6/mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255", size = 11252097, upload-time = "2024-12-30T16:37:25.144Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b3/8b0f74dfd072c802b7fa368829defdf3ee1566ba74c32a2cb2403f68024c/mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34", size = 10239728, upload-time = "2024-12-30T16:38:08.634Z" }, + { url = "https://files.pythonhosted.org/packages/c5/9b/4fd95ab20c52bb5b8c03cc49169be5905d931de17edfe4d9d2986800b52e/mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a", size = 11924965, upload-time = "2024-12-30T16:38:12.132Z" }, + { url = "https://files.pythonhosted.org/packages/56/9d/4a236b9c57f5d8f08ed346914b3f091a62dd7e19336b2b2a0d85485f82ff/mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9", size = 12867660, upload-time = "2024-12-30T16:38:17.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/88/a61a5497e2f68d9027de2bb139c7bb9abaeb1be1584649fa9d807f80a338/mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd", size = 12969198, upload-time = "2024-12-30T16:38:32.839Z" }, + { url = "https://files.pythonhosted.org/packages/54/da/3d6fc5d92d324701b0c23fb413c853892bfe0e1dbe06c9138037d459756b/mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107", size = 9885276, upload-time = "2024-12-30T16:38:20.828Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1f/186d133ae2514633f8558e78cd658070ba686c0e9275c5a5c24a1e1f0d67/mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35", size = 11200493, upload-time = "2024-12-30T16:38:26.935Z" }, + { url = "https://files.pythonhosted.org/packages/af/fc/4842485d034e38a4646cccd1369f6b1ccd7bc86989c52770d75d719a9941/mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc", size = 10357702, upload-time = "2024-12-30T16:38:50.623Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e6/457b83f2d701e23869cfec013a48a12638f75b9d37612a9ddf99072c1051/mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9", size = 12091104, upload-time = "2024-12-30T16:38:53.735Z" }, + { url = "https://files.pythonhosted.org/packages/f1/bf/76a569158db678fee59f4fd30b8e7a0d75bcbaeef49edd882a0d63af6d66/mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb", size = 12830167, upload-time = "2024-12-30T16:38:56.437Z" }, + { url = "https://files.pythonhosted.org/packages/43/bc/0bc6b694b3103de9fed61867f1c8bd33336b913d16831431e7cb48ef1c92/mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60", size = 13013834, upload-time = "2024-12-30T16:38:59.204Z" }, + { url = "https://files.pythonhosted.org/packages/b0/79/5f5ec47849b6df1e6943d5fd8e6632fbfc04b4fd4acfa5a5a9535d11b4e2/mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c", size = 9781231, upload-time = "2024-12-30T16:39:05.124Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b5/32dd67b69a16d088e533962e5044e51004176a9952419de0370cdaead0f8/mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1", size = 2752905, upload-time = "2024-12-30T16:38:42.021Z" }, ] [[package]] name = "mypy-extensions" version = "1.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } +sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433, upload-time = "2023-02-04T12:11:27.157Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, + { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695, upload-time = "2023-02-04T12:11:25.002Z" }, ] [[package]] @@ -669,27 +670,27 @@ dependencies = [ { name = "pyyaml" }, { name = "sphinx" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/49/64/e2f13dac02f599980798c01156393b781aec983b52a6e4057ee58f07c43a/myst_parser-3.0.1.tar.gz", hash = "sha256:88f0cb406cb363b077d176b51c476f62d60604d68a8dcdf4832e080441301a87", size = 92392 } +sdist = { url = "https://files.pythonhosted.org/packages/49/64/e2f13dac02f599980798c01156393b781aec983b52a6e4057ee58f07c43a/myst_parser-3.0.1.tar.gz", hash = "sha256:88f0cb406cb363b077d176b51c476f62d60604d68a8dcdf4832e080441301a87", size = 92392, upload-time = "2024-04-28T20:22:42.116Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/de/21aa8394f16add8f7427f0a1326ccd2b3a2a8a3245c9252bc5ac034c6155/myst_parser-3.0.1-py3-none-any.whl", hash = "sha256:6457aaa33a5d474aca678b8ead9b3dc298e89c68e67012e73146ea6fd54babf1", size = 83163 }, + { url = "https://files.pythonhosted.org/packages/e2/de/21aa8394f16add8f7427f0a1326ccd2b3a2a8a3245c9252bc5ac034c6155/myst_parser-3.0.1-py3-none-any.whl", hash = "sha256:6457aaa33a5d474aca678b8ead9b3dc298e89c68e67012e73146ea6fd54babf1", size = 83163, upload-time = "2024-04-28T20:22:39.985Z" }, ] [[package]] name = "nodeenv" version = "1.9.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, ] [[package]] name = "packaging" version = "24.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/b5/b43a27ac7472e1818c4bafd44430e69605baefe1f34440593e0332ec8b4d/packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9", size = 147882 } +sdist = { url = "https://files.pythonhosted.org/packages/ee/b5/b43a27ac7472e1818c4bafd44430e69605baefe1f34440593e0332ec8b4d/packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9", size = 147882, upload-time = "2024-03-10T09:39:28.33Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/49/df/1fceb2f8900f8639e278b056416d49134fb8d84c5942ffaa01ad34782422/packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", size = 53488 }, + { url = "https://files.pythonhosted.org/packages/49/df/1fceb2f8900f8639e278b056416d49134fb8d84c5942ffaa01ad34782422/packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", size = 53488, upload-time = "2024-03-10T09:39:25.947Z" }, ] [[package]] @@ -700,140 +701,140 @@ dependencies = [ { name = "pyreadline3", marker = "sys_platform == 'win32'" }, { name = "rich" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/29/1d/40420fda7c53fd071d8f62dcdb550c9f82fee54c2fda6842337890d87334/pdbr-0.8.9.tar.gz", hash = "sha256:3e0e1fb78761402bcfc0713a9c73acc2f639406b1b8da7233c442b965eee009d", size = 15942 } +sdist = { url = "https://files.pythonhosted.org/packages/29/1d/40420fda7c53fd071d8f62dcdb550c9f82fee54c2fda6842337890d87334/pdbr-0.8.9.tar.gz", hash = "sha256:3e0e1fb78761402bcfc0713a9c73acc2f639406b1b8da7233c442b965eee009d", size = 15942, upload-time = "2024-09-17T14:06:57.679Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/66/250f546d090c94ad5291995b847f927afec9ca782c1944ac59ed20d34d12/pdbr-0.8.9-py3-none-any.whl", hash = "sha256:5d18e431cc69281626627199fd6a63dfeef26ccd7f4c11a8e7b53b288efd9a93", size = 16038 }, + { url = "https://files.pythonhosted.org/packages/b0/66/250f546d090c94ad5291995b847f927afec9ca782c1944ac59ed20d34d12/pdbr-0.8.9-py3-none-any.whl", hash = "sha256:5d18e431cc69281626627199fd6a63dfeef26ccd7f4c11a8e7b53b288efd9a93", size = 16038, upload-time = "2024-09-17T14:06:56.509Z" }, ] [[package]] name = "pillow" version = "11.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/5d/45a3553a253ac8763f3561371432a90bdbe6000fbdcf1397ffe502aa206c/pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860", size = 5316554 }, - { url = "https://files.pythonhosted.org/packages/7c/c8/67c12ab069ef586a25a4a79ced553586748fad100c77c0ce59bb4983ac98/pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad", size = 4686548 }, - { url = "https://files.pythonhosted.org/packages/2f/bd/6741ebd56263390b382ae4c5de02979af7f8bd9807346d068700dd6d5cf9/pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0", size = 5859742 }, - { url = "https://files.pythonhosted.org/packages/ca/0b/c412a9e27e1e6a829e6ab6c2dca52dd563efbedf4c9c6aa453d9a9b77359/pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b", size = 7633087 }, - { url = "https://files.pythonhosted.org/packages/59/9d/9b7076aaf30f5dd17e5e5589b2d2f5a5d7e30ff67a171eb686e4eecc2adf/pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50", size = 5963350 }, - { url = "https://files.pythonhosted.org/packages/f0/16/1a6bf01fb622fb9cf5c91683823f073f053005c849b1f52ed613afcf8dae/pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae", size = 6631840 }, - { url = "https://files.pythonhosted.org/packages/7b/e6/6ff7077077eb47fde78739e7d570bdcd7c10495666b6afcd23ab56b19a43/pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9", size = 6074005 }, - { url = "https://files.pythonhosted.org/packages/c3/3a/b13f36832ea6d279a697231658199e0a03cd87ef12048016bdcc84131601/pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e", size = 6708372 }, - { url = "https://files.pythonhosted.org/packages/6c/e4/61b2e1a7528740efbc70b3d581f33937e38e98ef3d50b05007267a55bcb2/pillow-11.3.0-cp310-cp310-win32.whl", hash = "sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6", size = 6277090 }, - { url = "https://files.pythonhosted.org/packages/a9/d3/60c781c83a785d6afbd6a326ed4d759d141de43aa7365725cbcd65ce5e54/pillow-11.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f", size = 6985988 }, - { url = "https://files.pythonhosted.org/packages/9f/28/4f4a0203165eefb3763939c6789ba31013a2e90adffb456610f30f613850/pillow-11.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f", size = 2422899 }, - { url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531 }, - { url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560 }, - { url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978 }, - { url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168 }, - { url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053 }, - { url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273 }, - { url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043 }, - { url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516 }, - { url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", size = 6274768 }, - { url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", size = 6986055 }, - { url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", size = 2423079 }, - { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800 }, - { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296 }, - { url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726 }, - { url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652 }, - { url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787 }, - { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236 }, - { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950 }, - { url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358 }, - { url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079 }, - { url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324 }, - { url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067 }, - { url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328 }, - { url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652 }, - { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443 }, - { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474 }, - { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038 }, - { url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407 }, - { url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094 }, - { url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503 }, - { url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574 }, - { url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060 }, - { url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407 }, - { url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841 }, - { url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450 }, - { url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055 }, - { url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110 }, - { url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547 }, - { url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554 }, - { url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132 }, - { url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001 }, - { url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814 }, - { url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124 }, - { url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186 }, - { url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546 }, - { url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102 }, - { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803 }, - { url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520 }, - { url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116 }, - { url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597 }, - { url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246 }, - { url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336 }, - { url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699 }, - { url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789 }, - { url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386 }, - { url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911 }, - { url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383 }, - { url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385 }, - { url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129 }, - { url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580 }, - { url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860 }, - { url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694 }, - { url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888 }, - { url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330 }, - { url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089 }, - { url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206 }, - { url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370 }, - { url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500 }, - { url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835 }, - { url = "https://files.pythonhosted.org/packages/9e/8e/9c089f01677d1264ab8648352dcb7773f37da6ad002542760c80107da816/pillow-11.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:48d254f8a4c776de343051023eb61ffe818299eeac478da55227d96e241de53f", size = 5316478 }, - { url = "https://files.pythonhosted.org/packages/b5/a9/5749930caf674695867eb56a581e78eb5f524b7583ff10b01b6e5048acb3/pillow-11.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7aee118e30a4cf54fdd873bd3a29de51e29105ab11f9aad8c32123f58c8f8081", size = 4686522 }, - { url = "https://files.pythonhosted.org/packages/43/46/0b85b763eb292b691030795f9f6bb6fcaf8948c39413c81696a01c3577f7/pillow-11.3.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:23cff760a9049c502721bdb743a7cb3e03365fafcdfc2ef9784610714166e5a4", size = 5853376 }, - { url = "https://files.pythonhosted.org/packages/5e/c6/1a230ec0067243cbd60bc2dad5dc3ab46a8a41e21c15f5c9b52b26873069/pillow-11.3.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6359a3bc43f57d5b375d1ad54a0074318a0844d11b76abccf478c37c986d3cfc", size = 7626020 }, - { url = "https://files.pythonhosted.org/packages/63/dd/f296c27ffba447bfad76c6a0c44c1ea97a90cb9472b9304c94a732e8dbfb/pillow-11.3.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:092c80c76635f5ecb10f3f83d76716165c96f5229addbd1ec2bdbbda7d496e06", size = 5956732 }, - { url = "https://files.pythonhosted.org/packages/a5/a0/98a3630f0b57f77bae67716562513d3032ae70414fcaf02750279c389a9e/pillow-11.3.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cadc9e0ea0a2431124cde7e1697106471fc4c1da01530e679b2391c37d3fbb3a", size = 6624404 }, - { url = "https://files.pythonhosted.org/packages/de/e6/83dfba5646a290edd9a21964da07674409e410579c341fc5b8f7abd81620/pillow-11.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6a418691000f2a418c9135a7cf0d797c1bb7d9a485e61fe8e7722845b95ef978", size = 6067760 }, - { url = "https://files.pythonhosted.org/packages/bc/41/15ab268fe6ee9a2bc7391e2bbb20a98d3974304ab1a406a992dcb297a370/pillow-11.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:97afb3a00b65cc0804d1c7abddbf090a81eaac02768af58cbdcaaa0a931e0b6d", size = 6700534 }, - { url = "https://files.pythonhosted.org/packages/64/79/6d4f638b288300bed727ff29f2a3cb63db054b33518a95f27724915e3fbc/pillow-11.3.0-cp39-cp39-win32.whl", hash = "sha256:ea944117a7974ae78059fcc1800e5d3295172bb97035c0c1d9345fca1419da71", size = 6277091 }, - { url = "https://files.pythonhosted.org/packages/46/05/4106422f45a05716fd34ed21763f8ec182e8ea00af6e9cb05b93a247361a/pillow-11.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:e5c5858ad8ec655450a7c7df532e9842cf8df7cc349df7225c60d5d348c8aada", size = 6986091 }, - { url = "https://files.pythonhosted.org/packages/63/c6/287fd55c2c12761d0591549d48885187579b7c257bef0c6660755b0b59ae/pillow-11.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:6abdbfd3aea42be05702a8dd98832329c167ee84400a1d1f61ab11437f1717eb", size = 2422632 }, - { url = "https://files.pythonhosted.org/packages/6f/8b/209bd6b62ce8367f47e68a218bffac88888fdf2c9fcf1ecadc6c3ec1ebc7/pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967", size = 5270556 }, - { url = "https://files.pythonhosted.org/packages/2e/e6/231a0b76070c2cfd9e260a7a5b504fb72da0a95279410fa7afd99d9751d6/pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe", size = 4654625 }, - { url = "https://files.pythonhosted.org/packages/13/f4/10cf94fda33cb12765f2397fc285fa6d8eb9c29de7f3185165b702fc7386/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c", size = 4874207 }, - { url = "https://files.pythonhosted.org/packages/72/c9/583821097dc691880c92892e8e2d41fe0a5a3d6021f4963371d2f6d57250/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25", size = 6583939 }, - { url = "https://files.pythonhosted.org/packages/3b/8e/5c9d410f9217b12320efc7c413e72693f48468979a013ad17fd690397b9a/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27", size = 4957166 }, - { url = "https://files.pythonhosted.org/packages/62/bb/78347dbe13219991877ffb3a91bf09da8317fbfcd4b5f9140aeae020ad71/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a", size = 5581482 }, - { url = "https://files.pythonhosted.org/packages/d9/28/1000353d5e61498aaeaaf7f1e4b49ddb05f2c6575f9d4f9f914a3538b6e1/pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f", size = 6984596 }, - { url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566 }, - { url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618 }, - { url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248 }, - { url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963 }, - { url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170 }, - { url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505 }, - { url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598 }, +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/5d/45a3553a253ac8763f3561371432a90bdbe6000fbdcf1397ffe502aa206c/pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860", size = 5316554, upload-time = "2025-07-01T09:13:39.342Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c8/67c12ab069ef586a25a4a79ced553586748fad100c77c0ce59bb4983ac98/pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad", size = 4686548, upload-time = "2025-07-01T09:13:41.835Z" }, + { url = "https://files.pythonhosted.org/packages/2f/bd/6741ebd56263390b382ae4c5de02979af7f8bd9807346d068700dd6d5cf9/pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0", size = 5859742, upload-time = "2025-07-03T13:09:47.439Z" }, + { url = "https://files.pythonhosted.org/packages/ca/0b/c412a9e27e1e6a829e6ab6c2dca52dd563efbedf4c9c6aa453d9a9b77359/pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b", size = 7633087, upload-time = "2025-07-03T13:09:51.796Z" }, + { url = "https://files.pythonhosted.org/packages/59/9d/9b7076aaf30f5dd17e5e5589b2d2f5a5d7e30ff67a171eb686e4eecc2adf/pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50", size = 5963350, upload-time = "2025-07-01T09:13:43.865Z" }, + { url = "https://files.pythonhosted.org/packages/f0/16/1a6bf01fb622fb9cf5c91683823f073f053005c849b1f52ed613afcf8dae/pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae", size = 6631840, upload-time = "2025-07-01T09:13:46.161Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e6/6ff7077077eb47fde78739e7d570bdcd7c10495666b6afcd23ab56b19a43/pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9", size = 6074005, upload-time = "2025-07-01T09:13:47.829Z" }, + { url = "https://files.pythonhosted.org/packages/c3/3a/b13f36832ea6d279a697231658199e0a03cd87ef12048016bdcc84131601/pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e", size = 6708372, upload-time = "2025-07-01T09:13:52.145Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e4/61b2e1a7528740efbc70b3d581f33937e38e98ef3d50b05007267a55bcb2/pillow-11.3.0-cp310-cp310-win32.whl", hash = "sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6", size = 6277090, upload-time = "2025-07-01T09:13:53.915Z" }, + { url = "https://files.pythonhosted.org/packages/a9/d3/60c781c83a785d6afbd6a326ed4d759d141de43aa7365725cbcd65ce5e54/pillow-11.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f", size = 6985988, upload-time = "2025-07-01T09:13:55.699Z" }, + { url = "https://files.pythonhosted.org/packages/9f/28/4f4a0203165eefb3763939c6789ba31013a2e90adffb456610f30f613850/pillow-11.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f", size = 2422899, upload-time = "2025-07-01T09:13:57.497Z" }, + { url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531, upload-time = "2025-07-01T09:13:59.203Z" }, + { url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560, upload-time = "2025-07-01T09:14:01.101Z" }, + { url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978, upload-time = "2025-07-03T13:09:55.638Z" }, + { url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168, upload-time = "2025-07-03T13:10:00.37Z" }, + { url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053, upload-time = "2025-07-01T09:14:04.491Z" }, + { url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273, upload-time = "2025-07-01T09:14:06.235Z" }, + { url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043, upload-time = "2025-07-01T09:14:07.978Z" }, + { url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516, upload-time = "2025-07-01T09:14:10.233Z" }, + { url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", size = 6274768, upload-time = "2025-07-01T09:14:11.921Z" }, + { url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", size = 6986055, upload-time = "2025-07-01T09:14:13.623Z" }, + { url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", size = 2423079, upload-time = "2025-07-01T09:14:15.268Z" }, + { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800, upload-time = "2025-07-01T09:14:17.648Z" }, + { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296, upload-time = "2025-07-01T09:14:19.828Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726, upload-time = "2025-07-03T13:10:04.448Z" }, + { url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652, upload-time = "2025-07-03T13:10:10.391Z" }, + { url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787, upload-time = "2025-07-01T09:14:21.63Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236, upload-time = "2025-07-01T09:14:23.321Z" }, + { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950, upload-time = "2025-07-01T09:14:25.237Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358, upload-time = "2025-07-01T09:14:27.053Z" }, + { url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079, upload-time = "2025-07-01T09:14:30.104Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324, upload-time = "2025-07-01T09:14:31.899Z" }, + { url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067, upload-time = "2025-07-01T09:14:33.709Z" }, + { url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328, upload-time = "2025-07-01T09:14:35.276Z" }, + { url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652, upload-time = "2025-07-01T09:14:37.203Z" }, + { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443, upload-time = "2025-07-01T09:14:39.344Z" }, + { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474, upload-time = "2025-07-01T09:14:41.843Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038, upload-time = "2025-07-01T09:14:44.008Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407, upload-time = "2025-07-03T13:10:15.628Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094, upload-time = "2025-07-03T13:10:21.857Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503, upload-time = "2025-07-01T09:14:45.698Z" }, + { url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574, upload-time = "2025-07-01T09:14:47.415Z" }, + { url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060, upload-time = "2025-07-01T09:14:49.636Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407, upload-time = "2025-07-01T09:14:51.962Z" }, + { url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841, upload-time = "2025-07-01T09:14:54.142Z" }, + { url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450, upload-time = "2025-07-01T09:14:56.436Z" }, + { url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055, upload-time = "2025-07-01T09:14:58.072Z" }, + { url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110, upload-time = "2025-07-01T09:14:59.79Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547, upload-time = "2025-07-01T09:15:01.648Z" }, + { url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554, upload-time = "2025-07-03T13:10:27.018Z" }, + { url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132, upload-time = "2025-07-03T13:10:33.01Z" }, + { url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001, upload-time = "2025-07-01T09:15:03.365Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814, upload-time = "2025-07-01T09:15:05.655Z" }, + { url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124, upload-time = "2025-07-01T09:15:07.358Z" }, + { url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186, upload-time = "2025-07-01T09:15:09.317Z" }, + { url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546, upload-time = "2025-07-01T09:15:11.311Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102, upload-time = "2025-07-01T09:15:13.164Z" }, + { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803, upload-time = "2025-07-01T09:15:15.695Z" }, + { url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520, upload-time = "2025-07-01T09:15:17.429Z" }, + { url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116, upload-time = "2025-07-01T09:15:19.423Z" }, + { url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597, upload-time = "2025-07-03T13:10:38.404Z" }, + { url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246, upload-time = "2025-07-03T13:10:44.987Z" }, + { url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336, upload-time = "2025-07-01T09:15:21.237Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699, upload-time = "2025-07-01T09:15:23.186Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789, upload-time = "2025-07-01T09:15:25.1Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386, upload-time = "2025-07-01T09:15:27.378Z" }, + { url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911, upload-time = "2025-07-01T09:15:29.294Z" }, + { url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383, upload-time = "2025-07-01T09:15:31.128Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385, upload-time = "2025-07-01T09:15:33.328Z" }, + { url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129, upload-time = "2025-07-01T09:15:35.194Z" }, + { url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580, upload-time = "2025-07-01T09:15:37.114Z" }, + { url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860, upload-time = "2025-07-03T13:10:50.248Z" }, + { url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694, upload-time = "2025-07-03T13:10:56.432Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888, upload-time = "2025-07-01T09:15:39.436Z" }, + { url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330, upload-time = "2025-07-01T09:15:41.269Z" }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089, upload-time = "2025-07-01T09:15:43.13Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206, upload-time = "2025-07-01T09:15:44.937Z" }, + { url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370, upload-time = "2025-07-01T09:15:46.673Z" }, + { url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500, upload-time = "2025-07-01T09:15:48.512Z" }, + { url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835, upload-time = "2025-07-01T09:15:50.399Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8e/9c089f01677d1264ab8648352dcb7773f37da6ad002542760c80107da816/pillow-11.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:48d254f8a4c776de343051023eb61ffe818299eeac478da55227d96e241de53f", size = 5316478, upload-time = "2025-07-01T09:15:52.209Z" }, + { url = "https://files.pythonhosted.org/packages/b5/a9/5749930caf674695867eb56a581e78eb5f524b7583ff10b01b6e5048acb3/pillow-11.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7aee118e30a4cf54fdd873bd3a29de51e29105ab11f9aad8c32123f58c8f8081", size = 4686522, upload-time = "2025-07-01T09:15:54.162Z" }, + { url = "https://files.pythonhosted.org/packages/43/46/0b85b763eb292b691030795f9f6bb6fcaf8948c39413c81696a01c3577f7/pillow-11.3.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:23cff760a9049c502721bdb743a7cb3e03365fafcdfc2ef9784610714166e5a4", size = 5853376, upload-time = "2025-07-03T13:11:01.066Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c6/1a230ec0067243cbd60bc2dad5dc3ab46a8a41e21c15f5c9b52b26873069/pillow-11.3.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6359a3bc43f57d5b375d1ad54a0074318a0844d11b76abccf478c37c986d3cfc", size = 7626020, upload-time = "2025-07-03T13:11:06.479Z" }, + { url = "https://files.pythonhosted.org/packages/63/dd/f296c27ffba447bfad76c6a0c44c1ea97a90cb9472b9304c94a732e8dbfb/pillow-11.3.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:092c80c76635f5ecb10f3f83d76716165c96f5229addbd1ec2bdbbda7d496e06", size = 5956732, upload-time = "2025-07-01T09:15:56.111Z" }, + { url = "https://files.pythonhosted.org/packages/a5/a0/98a3630f0b57f77bae67716562513d3032ae70414fcaf02750279c389a9e/pillow-11.3.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cadc9e0ea0a2431124cde7e1697106471fc4c1da01530e679b2391c37d3fbb3a", size = 6624404, upload-time = "2025-07-01T09:15:58.245Z" }, + { url = "https://files.pythonhosted.org/packages/de/e6/83dfba5646a290edd9a21964da07674409e410579c341fc5b8f7abd81620/pillow-11.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6a418691000f2a418c9135a7cf0d797c1bb7d9a485e61fe8e7722845b95ef978", size = 6067760, upload-time = "2025-07-01T09:16:00.003Z" }, + { url = "https://files.pythonhosted.org/packages/bc/41/15ab268fe6ee9a2bc7391e2bbb20a98d3974304ab1a406a992dcb297a370/pillow-11.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:97afb3a00b65cc0804d1c7abddbf090a81eaac02768af58cbdcaaa0a931e0b6d", size = 6700534, upload-time = "2025-07-01T09:16:02.29Z" }, + { url = "https://files.pythonhosted.org/packages/64/79/6d4f638b288300bed727ff29f2a3cb63db054b33518a95f27724915e3fbc/pillow-11.3.0-cp39-cp39-win32.whl", hash = "sha256:ea944117a7974ae78059fcc1800e5d3295172bb97035c0c1d9345fca1419da71", size = 6277091, upload-time = "2025-07-01T09:16:04.4Z" }, + { url = "https://files.pythonhosted.org/packages/46/05/4106422f45a05716fd34ed21763f8ec182e8ea00af6e9cb05b93a247361a/pillow-11.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:e5c5858ad8ec655450a7c7df532e9842cf8df7cc349df7225c60d5d348c8aada", size = 6986091, upload-time = "2025-07-01T09:16:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/63/c6/287fd55c2c12761d0591549d48885187579b7c257bef0c6660755b0b59ae/pillow-11.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:6abdbfd3aea42be05702a8dd98832329c167ee84400a1d1f61ab11437f1717eb", size = 2422632, upload-time = "2025-07-01T09:16:08.142Z" }, + { url = "https://files.pythonhosted.org/packages/6f/8b/209bd6b62ce8367f47e68a218bffac88888fdf2c9fcf1ecadc6c3ec1ebc7/pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967", size = 5270556, upload-time = "2025-07-01T09:16:09.961Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e6/231a0b76070c2cfd9e260a7a5b504fb72da0a95279410fa7afd99d9751d6/pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe", size = 4654625, upload-time = "2025-07-01T09:16:11.913Z" }, + { url = "https://files.pythonhosted.org/packages/13/f4/10cf94fda33cb12765f2397fc285fa6d8eb9c29de7f3185165b702fc7386/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c", size = 4874207, upload-time = "2025-07-03T13:11:10.201Z" }, + { url = "https://files.pythonhosted.org/packages/72/c9/583821097dc691880c92892e8e2d41fe0a5a3d6021f4963371d2f6d57250/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25", size = 6583939, upload-time = "2025-07-03T13:11:15.68Z" }, + { url = "https://files.pythonhosted.org/packages/3b/8e/5c9d410f9217b12320efc7c413e72693f48468979a013ad17fd690397b9a/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27", size = 4957166, upload-time = "2025-07-01T09:16:13.74Z" }, + { url = "https://files.pythonhosted.org/packages/62/bb/78347dbe13219991877ffb3a91bf09da8317fbfcd4b5f9140aeae020ad71/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a", size = 5581482, upload-time = "2025-07-01T09:16:16.107Z" }, + { url = "https://files.pythonhosted.org/packages/d9/28/1000353d5e61498aaeaaf7f1e4b49ddb05f2c6575f9d4f9f914a3538b6e1/pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f", size = 6984596, upload-time = "2025-07-01T09:16:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566, upload-time = "2025-07-01T09:16:19.801Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618, upload-time = "2025-07-01T09:16:21.818Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248, upload-time = "2025-07-03T13:11:20.738Z" }, + { url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963, upload-time = "2025-07-03T13:11:26.283Z" }, + { url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170, upload-time = "2025-07-01T09:16:23.762Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505, upload-time = "2025-07-01T09:16:25.593Z" }, + { url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598, upload-time = "2025-07-01T09:16:27.732Z" }, ] [[package]] name = "platformdirs" version = "4.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/31/28/e40d24d2e2eb23135f8533ad33d582359c7825623b1e022f9d460def7c05/platformdirs-4.0.0.tar.gz", hash = "sha256:cb633b2bcf10c51af60beb0ab06d2f1d69064b43abf4c185ca6b28865f3f9731", size = 19914 } +sdist = { url = "https://files.pythonhosted.org/packages/31/28/e40d24d2e2eb23135f8533ad33d582359c7825623b1e022f9d460def7c05/platformdirs-4.0.0.tar.gz", hash = "sha256:cb633b2bcf10c51af60beb0ab06d2f1d69064b43abf4c185ca6b28865f3f9731", size = 19914, upload-time = "2023-11-10T16:43:08.316Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/16/70be3b725073035aa5fc3229321d06e22e73e3e09f6af78dcfdf16c7636c/platformdirs-4.0.0-py3-none-any.whl", hash = "sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b", size = 17562 }, + { url = "https://files.pythonhosted.org/packages/31/16/70be3b725073035aa5fc3229321d06e22e73e3e09f6af78dcfdf16c7636c/platformdirs-4.0.0-py3-none-any.whl", hash = "sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b", size = 17562, upload-time = "2023-11-10T16:43:06.949Z" }, ] [[package]] name = "pluggy" version = "1.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, ] [[package]] @@ -848,18 +849,18 @@ dependencies = [ { name = "virtualenv", version = "20.26.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "virtualenv", version = "20.36.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6b/00/1637ae945c6e10838ef5c41965f1c864e59301811bb203e979f335608e7c/pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658", size = 174966 } +sdist = { url = "https://files.pythonhosted.org/packages/6b/00/1637ae945c6e10838ef5c41965f1c864e59301811bb203e979f335608e7c/pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658", size = 174966, upload-time = "2022-12-25T22:53:01.144Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/6b/6cfe3a8b351b54f4b6c6d2ad4286804e3367f628dce379c603d3b96635f4/pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad", size = 201938 }, + { url = "https://files.pythonhosted.org/packages/a6/6b/6cfe3a8b351b54f4b6c6d2ad4286804e3367f628dce379c603d3b96635f4/pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad", size = 201938, upload-time = "2022-12-25T22:52:59.649Z" }, ] [[package]] name = "py-cpuinfo" version = "9.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/37/a8/d832f7293ebb21690860d2e01d8115e5ff6f2ae8bbdc953f0eb0fa4bd2c7/py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690", size = 104716 } +sdist = { url = "https://files.pythonhosted.org/packages/37/a8/d832f7293ebb21690860d2e01d8115e5ff6f2ae8bbdc953f0eb0fa4bd2c7/py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690", size = 104716, upload-time = "2022-10-25T20:38:06.303Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5", size = 22335 }, + { url = "https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5", size = 22335, upload-time = "2022-10-25T20:38:27.636Z" }, ] [[package]] @@ -869,36 +870,36 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyparsing" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d7/2f/482fcbc389e180e7f8d7e7cb06bc5a7c37be6c57939dfb950951d97f2722/pydot-2.0.0.tar.gz", hash = "sha256:60246af215123fa062f21cd791be67dda23a6f280df09f68919e637a1e4f3235", size = 152022 } +sdist = { url = "https://files.pythonhosted.org/packages/d7/2f/482fcbc389e180e7f8d7e7cb06bc5a7c37be6c57939dfb950951d97f2722/pydot-2.0.0.tar.gz", hash = "sha256:60246af215123fa062f21cd791be67dda23a6f280df09f68919e637a1e4f3235", size = 152022, upload-time = "2023-12-30T20:00:16.583Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/90/c9b51f3cdff89cd8f93382060330f43d1af098a6624cff439e700791e922/pydot-2.0.0-py3-none-any.whl", hash = "sha256:408a47913ea7bd5d2d34b274144880c1310c4aee901f353cf21fe2e526a4ea28", size = 22675 }, + { url = "https://files.pythonhosted.org/packages/7f/90/c9b51f3cdff89cd8f93382060330f43d1af098a6624cff439e700791e922/pydot-2.0.0-py3-none-any.whl", hash = "sha256:408a47913ea7bd5d2d34b274144880c1310c4aee901f353cf21fe2e526a4ea28", size = 22675, upload-time = "2023-12-30T20:00:14.24Z" }, ] [[package]] name = "pygments" version = "2.18.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 } +sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905, upload-time = "2024-05-04T13:42:02.013Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, + { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513, upload-time = "2024-05-04T13:41:57.345Z" }, ] [[package]] name = "pyparsing" version = "3.1.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/83/08/13f3bce01b2061f2bbd582c9df82723de943784cf719a35ac886c652043a/pyparsing-3.1.4.tar.gz", hash = "sha256:f86ec8d1a83f11977c9a6ea7598e8c27fc5cddfa5b07ea2241edbbde1d7bc032", size = 900231 } +sdist = { url = "https://files.pythonhosted.org/packages/83/08/13f3bce01b2061f2bbd582c9df82723de943784cf719a35ac886c652043a/pyparsing-3.1.4.tar.gz", hash = "sha256:f86ec8d1a83f11977c9a6ea7598e8c27fc5cddfa5b07ea2241edbbde1d7bc032", size = 900231, upload-time = "2024-08-25T15:00:47.416Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/0c/0e3c05b1c87bb6a1c76d281b0f35e78d2d80ac91b5f8f524cebf77f51049/pyparsing-3.1.4-py3-none-any.whl", hash = "sha256:a6a7ee4235a3f944aa1fa2249307708f893fe5717dc603503c6c7969c070fb7c", size = 104100 }, + { url = "https://files.pythonhosted.org/packages/e5/0c/0e3c05b1c87bb6a1c76d281b0f35e78d2d80ac91b5f8f524cebf77f51049/pyparsing-3.1.4-py3-none-any.whl", hash = "sha256:a6a7ee4235a3f944aa1fa2249307708f893fe5717dc603503c6c7969c070fb7c", size = 104100, upload-time = "2024-08-25T15:00:45.361Z" }, ] [[package]] name = "pyreadline3" version = "3.5.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/49/4cea918a08f02817aabae639e3d0ac046fef9f9180518a3ad394e22da148/pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7", size = 99839 } +sdist = { url = "https://files.pythonhosted.org/packages/0f/49/4cea918a08f02817aabae639e3d0ac046fef9f9180518a3ad394e22da148/pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7", size = 99839, upload-time = "2024-09-19T02:40:10.062Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178 }, + { url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178, upload-time = "2024-09-19T02:40:08.598Z" }, ] [[package]] @@ -909,9 +910,9 @@ dependencies = [ { name = "nodeenv" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/b2/5db700e52554b8f025faa9c3c624c59f1f6c8841ba81ab97641b54322f16/pyright-1.1.408.tar.gz", hash = "sha256:f28f2321f96852fa50b5829ea492f6adb0e6954568d1caa3f3af3a5f555eb684", size = 4400578 } +sdist = { url = "https://files.pythonhosted.org/packages/74/b2/5db700e52554b8f025faa9c3c624c59f1f6c8841ba81ab97641b54322f16/pyright-1.1.408.tar.gz", hash = "sha256:f28f2321f96852fa50b5829ea492f6adb0e6954568d1caa3f3af3a5f555eb684", size = 4400578, upload-time = "2026-01-08T08:07:38.795Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/82/a2c93e32800940d9573fb28c346772a14778b84ba7524e691b324620ab89/pyright-1.1.408-py3-none-any.whl", hash = "sha256:090b32865f4fdb1e0e6cd82bf5618480d48eecd2eb2e70f960982a3d9a4c17c1", size = 6399144 }, + { url = "https://files.pythonhosted.org/packages/0c/82/a2c93e32800940d9573fb28c346772a14778b84ba7524e691b324620ab89/pyright-1.1.408-py3-none-any.whl", hash = "sha256:090b32865f4fdb1e0e6cd82bf5618480d48eecd2eb2e70f960982a3d9a4c17c1", size = 6399144, upload-time = "2026-01-08T08:07:37.082Z" }, ] [[package]] @@ -926,9 +927,9 @@ dependencies = [ { name = "pluggy" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8b/6c/62bbd536103af674e227c41a8f3dcd022d591f6eed5facb5a0f31ee33bbc/pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", size = 1442487 } +sdist = { url = "https://files.pythonhosted.org/packages/8b/6c/62bbd536103af674e227c41a8f3dcd022d591f6eed5facb5a0f31ee33bbc/pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", size = 1442487, upload-time = "2024-09-10T10:52:15.003Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 }, + { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341, upload-time = "2024-09-10T10:52:12.54Z" }, ] [[package]] @@ -943,9 +944,9 @@ dependencies = [ { name = "pytest", marker = "python_full_version < '3.10'" }, { name = "typing-extensions", marker = "python_full_version < '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/86/9e3c5f48f7b7b638b216e4b9e645f54d199d7abbbab7a64a13b4e12ba10f/pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57", size = 50119 } +sdist = { url = "https://files.pythonhosted.org/packages/42/86/9e3c5f48f7b7b638b216e4b9e645f54d199d7abbbab7a64a13b4e12ba10f/pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57", size = 50119, upload-time = "2025-09-12T07:33:53.816Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/93/2fa34714b7a4ae72f2f8dad66ba17dd9a2c793220719e736dda28b7aec27/pytest_asyncio-1.2.0-py3-none-any.whl", hash = "sha256:8e17ae5e46d8e7efe51ab6494dd2010f4ca8dae51652aa3c8d55acf50bfb2e99", size = 15095 }, + { url = "https://files.pythonhosted.org/packages/04/93/2fa34714b7a4ae72f2f8dad66ba17dd9a2c793220719e736dda28b7aec27/pytest_asyncio-1.2.0-py3-none-any.whl", hash = "sha256:8e17ae5e46d8e7efe51ab6494dd2010f4ca8dae51652aa3c8d55acf50bfb2e99", size = 15095, upload-time = "2025-09-12T07:33:52.639Z" }, ] [[package]] @@ -960,9 +961,9 @@ dependencies = [ { name = "pytest", marker = "python_full_version >= '3.10'" }, { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087 } +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075 }, + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, ] [[package]] @@ -976,9 +977,9 @@ dependencies = [ { name = "py-cpuinfo", marker = "python_full_version < '3.10'" }, { name = "pytest", marker = "python_full_version < '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/28/08/e6b0067efa9a1f2a1eb3043ecd8a0c48bfeb60d3255006dcc829d72d5da2/pytest-benchmark-4.0.0.tar.gz", hash = "sha256:fb0785b83efe599a6a956361c0691ae1dbb5318018561af10f3e915caa0048d1", size = 334641 } +sdist = { url = "https://files.pythonhosted.org/packages/28/08/e6b0067efa9a1f2a1eb3043ecd8a0c48bfeb60d3255006dcc829d72d5da2/pytest-benchmark-4.0.0.tar.gz", hash = "sha256:fb0785b83efe599a6a956361c0691ae1dbb5318018561af10f3e915caa0048d1", size = 334641, upload-time = "2022-10-25T21:21:55.686Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/a1/3b70862b5b3f830f0422844f25a823d0470739d994466be9dbbbb414d85a/pytest_benchmark-4.0.0-py3-none-any.whl", hash = "sha256:fdb7db64e31c8b277dff9850d2a2556d8b60bcb0ea6524e36e28ffd7c87f71d6", size = 43951 }, + { url = "https://files.pythonhosted.org/packages/4d/a1/3b70862b5b3f830f0422844f25a823d0470739d994466be9dbbbb414d85a/pytest_benchmark-4.0.0-py3-none-any.whl", hash = "sha256:fdb7db64e31c8b277dff9850d2a2556d8b60bcb0ea6524e36e28ffd7c87f71d6", size = 43951, upload-time = "2022-10-25T21:21:53.208Z" }, ] [[package]] @@ -992,9 +993,9 @@ dependencies = [ { name = "py-cpuinfo", marker = "python_full_version >= '3.10'" }, { name = "pytest", marker = "python_full_version >= '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/39/d0/a8bd08d641b393db3be3819b03e2d9bb8760ca8479080a26a5f6e540e99c/pytest-benchmark-5.1.0.tar.gz", hash = "sha256:9ea661cdc292e8231f7cd4c10b0319e56a2118e2c09d9f50e1b3d150d2aca105", size = 337810 } +sdist = { url = "https://files.pythonhosted.org/packages/39/d0/a8bd08d641b393db3be3819b03e2d9bb8760ca8479080a26a5f6e540e99c/pytest-benchmark-5.1.0.tar.gz", hash = "sha256:9ea661cdc292e8231f7cd4c10b0319e56a2118e2c09d9f50e1b3d150d2aca105", size = 337810, upload-time = "2024-10-30T11:51:48.521Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/d6/b41653199ea09d5969d4e385df9bbfd9a100f28ca7e824ce7c0a016e3053/pytest_benchmark-5.1.0-py3-none-any.whl", hash = "sha256:922de2dfa3033c227c96da942d1878191afa135a29485fb942e85dff1c592c89", size = 44259 }, + { url = "https://files.pythonhosted.org/packages/9e/d6/b41653199ea09d5969d4e385df9bbfd9a100f28ca7e824ce7c0a016e3053/pytest_benchmark-5.1.0-py3-none-any.whl", hash = "sha256:922de2dfa3033c227c96da942d1878191afa135a29485fb942e85dff1c592c89", size = 44259, upload-time = "2024-10-30T11:51:45.94Z" }, ] [[package]] @@ -1006,9 +1007,9 @@ dependencies = [ { name = "pluggy" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328 } +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424 }, + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, ] [[package]] @@ -1018,9 +1019,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/02/c0/43c8b2528c24d7f1a48a47e3f7381f5ab2ae8c64634b0c3f4bd843063955/pytest_django-4.9.0.tar.gz", hash = "sha256:8bf7bc358c9ae6f6fc51b6cebb190fe20212196e6807121f11bd6a3b03428314", size = 84067 } +sdist = { url = "https://files.pythonhosted.org/packages/02/c0/43c8b2528c24d7f1a48a47e3f7381f5ab2ae8c64634b0c3f4bd843063955/pytest_django-4.9.0.tar.gz", hash = "sha256:8bf7bc358c9ae6f6fc51b6cebb190fe20212196e6807121f11bd6a3b03428314", size = 84067, upload-time = "2024-09-02T15:49:18.407Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/47/fe/54f387ee1b41c9ad59e48fb8368a361fad0600fe404315e31a12bacaea7d/pytest_django-4.9.0-py3-none-any.whl", hash = "sha256:1d83692cb39188682dbb419ff0393867e9904094a549a7d38a3154d5731b2b99", size = 23723 }, + { url = "https://files.pythonhosted.org/packages/47/fe/54f387ee1b41c9ad59e48fb8368a361fad0600fe404315e31a12bacaea7d/pytest_django-4.9.0-py3-none-any.whl", hash = "sha256:1d83692cb39188682dbb419ff0393867e9904094a549a7d38a3154d5731b2b99", size = 23723, upload-time = "2024-09-02T15:49:17.127Z" }, ] [[package]] @@ -1030,9 +1031,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036 } +sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095 }, + { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, ] [[package]] @@ -1044,9 +1045,9 @@ dependencies = [ { name = "pytest" }, { name = "termcolor" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f5/ac/5754f5edd6d508bc6493bc37d74b928f102a5fff82d9a80347e180998f08/pytest-sugar-1.0.0.tar.gz", hash = "sha256:6422e83258f5b0c04ce7c632176c7732cab5fdb909cb39cca5c9139f81276c0a", size = 14992 } +sdist = { url = "https://files.pythonhosted.org/packages/f5/ac/5754f5edd6d508bc6493bc37d74b928f102a5fff82d9a80347e180998f08/pytest-sugar-1.0.0.tar.gz", hash = "sha256:6422e83258f5b0c04ce7c632176c7732cab5fdb909cb39cca5c9139f81276c0a", size = 14992, upload-time = "2024-02-01T18:30:36.735Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/92/fb/889f1b69da2f13691de09a111c16c4766a433382d44aa0ecf221deded44a/pytest_sugar-1.0.0-py3-none-any.whl", hash = "sha256:70ebcd8fc5795dc457ff8b69d266a4e2e8a74ae0c3edc749381c64b5246c8dfd", size = 10171 }, + { url = "https://files.pythonhosted.org/packages/92/fb/889f1b69da2f13691de09a111c16c4766a433382d44aa0ecf221deded44a/pytest_sugar-1.0.0-py3-none-any.whl", hash = "sha256:70ebcd8fc5795dc457ff8b69d266a4e2e8a74ae0c3edc749381c64b5246c8dfd", size = 10171, upload-time = "2024-02-01T18:30:29.395Z" }, ] [[package]] @@ -1056,9 +1057,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973 } +sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382 }, + { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, ] [[package]] @@ -1069,14 +1070,14 @@ dependencies = [ { name = "execnet" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069 } +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396 }, + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, ] [[package]] name = "python-statemachine" -version = "3.0.0" +version = "3.1.0" source = { editable = "." } [package.optional-dependencies] @@ -1113,10 +1114,13 @@ dev = [ { name = "sphinx-autobuild" }, { name = "sphinx-copybutton" }, { name = "sphinx-gallery" }, + { name = "sphinxcontrib-mermaid", version = "1.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "sphinxcontrib-mermaid", version = "2.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] [package.metadata] requires-dist = [{ name = "pydot", marker = "extra == 'diagrams'", specifier = ">=2.0.0" }] +provides-extras = ["diagrams"] [package.metadata.requires-dev] dev = [ @@ -1145,45 +1149,46 @@ dev = [ { name = "sphinx-autobuild", marker = "python_full_version >= '3.9'" }, { name = "sphinx-copybutton", marker = "python_full_version >= '3.9'", specifier = ">=0.5.2" }, { name = "sphinx-gallery", marker = "python_full_version >= '3.9'" }, + { name = "sphinxcontrib-mermaid", marker = "python_full_version >= '3.9'" }, ] [[package]] name = "pyyaml" version = "6.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/e5/af35f7ea75cf72f2cd079c95ee16797de7cd71f29ea7c68ae5ce7be1eda0/PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", size = 125201 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/96/06/4beb652c0fe16834032e54f0956443d4cc797fe645527acee59e7deaa0a2/PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", size = 189447 }, - { url = "https://files.pythonhosted.org/packages/5b/07/10033a403b23405a8fc48975444463d3d10a5c2736b7eb2550b07b367429/PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f", size = 169264 }, - { url = "https://files.pythonhosted.org/packages/f1/26/55e4f21db1f72eaef092015d9017c11510e7e6301c62a6cfee91295d13c6/PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", size = 677003 }, - { url = "https://files.pythonhosted.org/packages/ba/91/090818dfa62e85181f3ae23dd1e8b7ea7f09684864a900cab72d29c57346/PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", size = 699070 }, - { url = "https://files.pythonhosted.org/packages/29/61/bf33c6c85c55bc45a29eee3195848ff2d518d84735eb0e2d8cb42e0d285e/PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", size = 705525 }, - { url = "https://files.pythonhosted.org/packages/07/91/45dfd0ef821a7f41d9d0136ea3608bb5b1653e42fd56a7970532cb5c003f/PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290", size = 707514 }, - { url = "https://files.pythonhosted.org/packages/b6/a0/b6700da5d49e9fed49dc3243d3771b598dad07abb37cc32e524607f96adc/PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", size = 130488 }, - { url = "https://files.pythonhosted.org/packages/24/97/9b59b43431f98d01806b288532da38099cc6f2fea0f3d712e21e269c0279/PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", size = 145338 }, - { url = "https://files.pythonhosted.org/packages/ec/0d/26fb23e8863e0aeaac0c64e03fd27367ad2ae3f3cccf3798ee98ce160368/PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", size = 187867 }, - { url = "https://files.pythonhosted.org/packages/28/09/55f715ddbf95a054b764b547f617e22f1d5e45d83905660e9a088078fe67/PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", size = 167530 }, - { url = "https://files.pythonhosted.org/packages/5e/94/7d5ee059dfb92ca9e62f4057dcdec9ac08a9e42679644854dc01177f8145/PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", size = 732244 }, - { url = "https://files.pythonhosted.org/packages/06/92/e0224aa6ebf9dc54a06a4609da37da40bb08d126f5535d81bff6b417b2ae/PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", size = 752871 }, - { url = "https://files.pythonhosted.org/packages/7b/5e/efd033ab7199a0b2044dab3b9f7a4f6670e6a52c089de572e928d2873b06/PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", size = 757729 }, - { url = "https://files.pythonhosted.org/packages/03/5c/c4671451b2f1d76ebe352c0945d4cd13500adb5d05f5a51ee296d80152f7/PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", size = 748528 }, - { url = "https://files.pythonhosted.org/packages/73/9c/766e78d1efc0d1fca637a6b62cea1b4510a7fb93617eb805223294fef681/PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", size = 130286 }, - { url = "https://files.pythonhosted.org/packages/b3/34/65bb4b2d7908044963ebf614fe0fdb080773fc7030d7e39c8d3eddcd4257/PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", size = 144699 }, - { url = "https://files.pythonhosted.org/packages/bc/06/1b305bf6aa704343be85444c9d011f626c763abb40c0edc1cad13bfd7f86/PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", size = 178692 }, - { url = "https://files.pythonhosted.org/packages/84/02/404de95ced348b73dd84f70e15a41843d817ff8c1744516bf78358f2ffd2/PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", size = 165622 }, - { url = "https://files.pythonhosted.org/packages/c7/4c/4a2908632fc980da6d918b9de9c1d9d7d7e70b2672b1ad5166ed27841ef7/PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef", size = 696937 }, - { url = "https://files.pythonhosted.org/packages/b4/33/720548182ffa8344418126017aa1d4ab4aeec9a2275f04ce3f3573d8ace8/PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", size = 724969 }, - { url = "https://files.pythonhosted.org/packages/4f/78/77b40157b6cb5f2d3d31a3d9b2efd1ba3505371f76730d267e8b32cf4b7f/PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", size = 712604 }, - { url = "https://files.pythonhosted.org/packages/2e/97/3e0e089ee85e840f4b15bfa00e4e63d84a3691ababbfea92d6f820ea6f21/PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54", size = 126098 }, - { url = "https://files.pythonhosted.org/packages/2b/9f/fbade56564ad486809c27b322d0f7e6a89c01f6b4fe208402e90d4443a99/PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df", size = 138675 }, - { url = "https://files.pythonhosted.org/packages/57/c5/5d09b66b41d549914802f482a2118d925d876dc2a35b2d127694c1345c34/PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", size = 197846 }, - { url = "https://files.pythonhosted.org/packages/0e/88/21b2f16cb2123c1e9375f2c93486e35fdc86e63f02e274f0e99c589ef153/PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", size = 174396 }, - { url = "https://files.pythonhosted.org/packages/ac/6c/967d91a8edf98d2b2b01d149bd9e51b8f9fb527c98d80ebb60c6b21d60c4/PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", size = 731824 }, - { url = "https://files.pythonhosted.org/packages/4a/4b/c71ef18ef83c82f99e6da8332910692af78ea32bd1d1d76c9787dfa36aea/PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", size = 754777 }, - { url = "https://files.pythonhosted.org/packages/7d/39/472f2554a0f1e825bd7c5afc11c817cd7a2f3657460f7159f691fbb37c51/PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", size = 738883 }, - { url = "https://files.pythonhosted.org/packages/40/da/a175a35cf5583580e90ac3e2a3dbca90e43011593ae62ce63f79d7b28d92/PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", size = 750294 }, - { url = "https://files.pythonhosted.org/packages/24/62/7fcc372442ec8ea331da18c24b13710e010c5073ab851ef36bf9dacb283f/PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", size = 136936 }, - { url = "https://files.pythonhosted.org/packages/84/4d/82704d1ab9290b03da94e6425f5e87396b999fd7eb8e08f3a92c158402bf/PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", size = 152751 }, +sdist = { url = "https://files.pythonhosted.org/packages/cd/e5/af35f7ea75cf72f2cd079c95ee16797de7cd71f29ea7c68ae5ce7be1eda0/PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", size = 125201, upload-time = "2023-07-18T00:00:23.308Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/06/4beb652c0fe16834032e54f0956443d4cc797fe645527acee59e7deaa0a2/PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", size = 189447, upload-time = "2023-07-17T23:57:04.325Z" }, + { url = "https://files.pythonhosted.org/packages/5b/07/10033a403b23405a8fc48975444463d3d10a5c2736b7eb2550b07b367429/PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f", size = 169264, upload-time = "2023-07-17T23:57:07.787Z" }, + { url = "https://files.pythonhosted.org/packages/f1/26/55e4f21db1f72eaef092015d9017c11510e7e6301c62a6cfee91295d13c6/PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", size = 677003, upload-time = "2023-07-17T23:57:13.144Z" }, + { url = "https://files.pythonhosted.org/packages/ba/91/090818dfa62e85181f3ae23dd1e8b7ea7f09684864a900cab72d29c57346/PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", size = 699070, upload-time = "2023-07-17T23:57:19.402Z" }, + { url = "https://files.pythonhosted.org/packages/29/61/bf33c6c85c55bc45a29eee3195848ff2d518d84735eb0e2d8cb42e0d285e/PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", size = 705525, upload-time = "2023-07-17T23:57:25.272Z" }, + { url = "https://files.pythonhosted.org/packages/07/91/45dfd0ef821a7f41d9d0136ea3608bb5b1653e42fd56a7970532cb5c003f/PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290", size = 707514, upload-time = "2023-08-28T18:43:20.945Z" }, + { url = "https://files.pythonhosted.org/packages/b6/a0/b6700da5d49e9fed49dc3243d3771b598dad07abb37cc32e524607f96adc/PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", size = 130488, upload-time = "2023-07-17T23:57:28.144Z" }, + { url = "https://files.pythonhosted.org/packages/24/97/9b59b43431f98d01806b288532da38099cc6f2fea0f3d712e21e269c0279/PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", size = 145338, upload-time = "2023-07-17T23:57:31.118Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0d/26fb23e8863e0aeaac0c64e03fd27367ad2ae3f3cccf3798ee98ce160368/PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", size = 187867, upload-time = "2023-07-17T23:57:34.35Z" }, + { url = "https://files.pythonhosted.org/packages/28/09/55f715ddbf95a054b764b547f617e22f1d5e45d83905660e9a088078fe67/PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", size = 167530, upload-time = "2023-07-17T23:57:36.975Z" }, + { url = "https://files.pythonhosted.org/packages/5e/94/7d5ee059dfb92ca9e62f4057dcdec9ac08a9e42679644854dc01177f8145/PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", size = 732244, upload-time = "2023-07-17T23:57:43.774Z" }, + { url = "https://files.pythonhosted.org/packages/06/92/e0224aa6ebf9dc54a06a4609da37da40bb08d126f5535d81bff6b417b2ae/PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", size = 752871, upload-time = "2023-07-17T23:57:51.921Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5e/efd033ab7199a0b2044dab3b9f7a4f6670e6a52c089de572e928d2873b06/PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", size = 757729, upload-time = "2023-07-17T23:57:59.865Z" }, + { url = "https://files.pythonhosted.org/packages/03/5c/c4671451b2f1d76ebe352c0945d4cd13500adb5d05f5a51ee296d80152f7/PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", size = 748528, upload-time = "2023-08-28T18:43:23.207Z" }, + { url = "https://files.pythonhosted.org/packages/73/9c/766e78d1efc0d1fca637a6b62cea1b4510a7fb93617eb805223294fef681/PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", size = 130286, upload-time = "2023-07-17T23:58:02.964Z" }, + { url = "https://files.pythonhosted.org/packages/b3/34/65bb4b2d7908044963ebf614fe0fdb080773fc7030d7e39c8d3eddcd4257/PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", size = 144699, upload-time = "2023-07-17T23:58:05.586Z" }, + { url = "https://files.pythonhosted.org/packages/bc/06/1b305bf6aa704343be85444c9d011f626c763abb40c0edc1cad13bfd7f86/PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", size = 178692, upload-time = "2023-08-28T18:43:24.924Z" }, + { url = "https://files.pythonhosted.org/packages/84/02/404de95ced348b73dd84f70e15a41843d817ff8c1744516bf78358f2ffd2/PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", size = 165622, upload-time = "2023-08-28T18:43:26.54Z" }, + { url = "https://files.pythonhosted.org/packages/c7/4c/4a2908632fc980da6d918b9de9c1d9d7d7e70b2672b1ad5166ed27841ef7/PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef", size = 696937, upload-time = "2024-01-18T20:40:22.92Z" }, + { url = "https://files.pythonhosted.org/packages/b4/33/720548182ffa8344418126017aa1d4ab4aeec9a2275f04ce3f3573d8ace8/PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", size = 724969, upload-time = "2023-08-28T18:43:28.56Z" }, + { url = "https://files.pythonhosted.org/packages/4f/78/77b40157b6cb5f2d3d31a3d9b2efd1ba3505371f76730d267e8b32cf4b7f/PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", size = 712604, upload-time = "2023-08-28T18:43:30.206Z" }, + { url = "https://files.pythonhosted.org/packages/2e/97/3e0e089ee85e840f4b15bfa00e4e63d84a3691ababbfea92d6f820ea6f21/PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54", size = 126098, upload-time = "2023-08-28T18:43:31.835Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/fbade56564ad486809c27b322d0f7e6a89c01f6b4fe208402e90d4443a99/PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df", size = 138675, upload-time = "2023-08-28T18:43:33.613Z" }, + { url = "https://files.pythonhosted.org/packages/57/c5/5d09b66b41d549914802f482a2118d925d876dc2a35b2d127694c1345c34/PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", size = 197846, upload-time = "2023-07-17T23:59:46.424Z" }, + { url = "https://files.pythonhosted.org/packages/0e/88/21b2f16cb2123c1e9375f2c93486e35fdc86e63f02e274f0e99c589ef153/PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", size = 174396, upload-time = "2023-07-17T23:59:49.538Z" }, + { url = "https://files.pythonhosted.org/packages/ac/6c/967d91a8edf98d2b2b01d149bd9e51b8f9fb527c98d80ebb60c6b21d60c4/PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", size = 731824, upload-time = "2023-07-17T23:59:58.111Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4b/c71ef18ef83c82f99e6da8332910692af78ea32bd1d1d76c9787dfa36aea/PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", size = 754777, upload-time = "2023-07-18T00:00:06.716Z" }, + { url = "https://files.pythonhosted.org/packages/7d/39/472f2554a0f1e825bd7c5afc11c817cd7a2f3657460f7159f691fbb37c51/PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", size = 738883, upload-time = "2023-07-18T00:00:14.423Z" }, + { url = "https://files.pythonhosted.org/packages/40/da/a175a35cf5583580e90ac3e2a3dbca90e43011593ae62ce63f79d7b28d92/PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", size = 750294, upload-time = "2023-08-28T18:43:37.153Z" }, + { url = "https://files.pythonhosted.org/packages/24/62/7fcc372442ec8ea331da18c24b13710e010c5073ab851ef36bf9dacb283f/PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", size = 136936, upload-time = "2023-07-18T00:00:17.167Z" }, + { url = "https://files.pythonhosted.org/packages/84/4d/82704d1ab9290b03da94e6425f5e87396b999fd7eb8e08f3a92c158402bf/PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", size = 152751, upload-time = "2023-07-18T00:00:19.939Z" }, ] [[package]] @@ -1196,9 +1201,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517 } +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738 }, + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] [[package]] @@ -1210,61 +1215,61 @@ dependencies = [ { name = "pygments" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } +sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149, upload-time = "2024-11-01T16:43:57.873Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, + { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424, upload-time = "2024-11-01T16:43:55.817Z" }, ] [[package]] name = "ruff" version = "0.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c8/39/5cee96809fbca590abea6b46c6d1c586b49663d1d2830a751cc8fc42c666/ruff-0.15.0.tar.gz", hash = "sha256:6bdea47cdbea30d40f8f8d7d69c0854ba7c15420ec75a26f463290949d7f7e9a", size = 4524893 } +sdist = { url = "https://files.pythonhosted.org/packages/c8/39/5cee96809fbca590abea6b46c6d1c586b49663d1d2830a751cc8fc42c666/ruff-0.15.0.tar.gz", hash = "sha256:6bdea47cdbea30d40f8f8d7d69c0854ba7c15420ec75a26f463290949d7f7e9a", size = 4524893, upload-time = "2026-02-03T17:53:35.357Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/88/3fd1b0aa4b6330d6aaa63a285bc96c9f71970351579152d231ed90914586/ruff-0.15.0-py3-none-linux_armv6l.whl", hash = "sha256:aac4ebaa612a82b23d45964586f24ae9bc23ca101919f5590bdb368d74ad5455", size = 10354332 }, - { url = "https://files.pythonhosted.org/packages/72/f6/62e173fbb7eb75cc29fe2576a1e20f0a46f671a2587b5f604bfb0eaf5f6f/ruff-0.15.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dcd4be7cc75cfbbca24a98d04d0b9b36a270d0833241f776b788d59f4142b14d", size = 10767189 }, - { url = "https://files.pythonhosted.org/packages/99/e4/968ae17b676d1d2ff101d56dc69cf333e3a4c985e1ec23803df84fc7bf9e/ruff-0.15.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d747e3319b2bce179c7c1eaad3d884dc0a199b5f4d5187620530adf9105268ce", size = 10075384 }, - { url = "https://files.pythonhosted.org/packages/a2/bf/9843c6044ab9e20af879c751487e61333ca79a2c8c3058b15722386b8cae/ruff-0.15.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:650bd9c56ae03102c51a5e4b554d74d825ff3abe4db22b90fd32d816c2e90621", size = 10481363 }, - { url = "https://files.pythonhosted.org/packages/55/d9/4ada5ccf4cd1f532db1c8d44b6f664f2208d3d93acbeec18f82315e15193/ruff-0.15.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6664b7eac559e3048223a2da77769c2f92b43a6dfd4720cef42654299a599c9", size = 10187736 }, - { url = "https://files.pythonhosted.org/packages/86/e2/f25eaecd446af7bb132af0a1d5b135a62971a41f5366ff41d06d25e77a91/ruff-0.15.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f811f97b0f092b35320d1556f3353bf238763420ade5d9e62ebd2b73f2ff179", size = 10968415 }, - { url = "https://files.pythonhosted.org/packages/e7/dc/f06a8558d06333bf79b497d29a50c3a673d9251214e0d7ec78f90b30aa79/ruff-0.15.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:761ec0a66680fab6454236635a39abaf14198818c8cdf691e036f4bc0f406b2d", size = 11809643 }, - { url = "https://files.pythonhosted.org/packages/dd/45/0ece8db2c474ad7df13af3a6d50f76e22a09d078af63078f005057ca59eb/ruff-0.15.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:940f11c2604d317e797b289f4f9f3fa5555ffe4fb574b55ed006c3d9b6f0eb78", size = 11234787 }, - { url = "https://files.pythonhosted.org/packages/8a/d9/0e3a81467a120fd265658d127db648e4d3acfe3e4f6f5d4ea79fac47e587/ruff-0.15.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcbca3d40558789126da91d7ef9a7c87772ee107033db7191edefa34e2c7f1b4", size = 11112797 }, - { url = "https://files.pythonhosted.org/packages/b2/cb/8c0b3b0c692683f8ff31351dfb6241047fa873a4481a76df4335a8bff716/ruff-0.15.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9a121a96db1d75fa3eb39c4539e607f628920dd72ff1f7c5ee4f1b768ac62d6e", size = 11033133 }, - { url = "https://files.pythonhosted.org/packages/f8/5e/23b87370cf0f9081a8c89a753e69a4e8778805b8802ccfe175cc410e50b9/ruff-0.15.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5298d518e493061f2eabd4abd067c7e4fb89e2f63291c94332e35631c07c3662", size = 10442646 }, - { url = "https://files.pythonhosted.org/packages/e1/9a/3c94de5ce642830167e6d00b5c75aacd73e6347b4c7fc6828699b150a5ee/ruff-0.15.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afb6e603d6375ff0d6b0cee563fa21ab570fd15e65c852cb24922cef25050cf1", size = 10195750 }, - { url = "https://files.pythonhosted.org/packages/30/15/e396325080d600b436acc970848d69df9c13977942fb62bb8722d729bee8/ruff-0.15.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:77e515f6b15f828b94dc17d2b4ace334c9ddb7d9468c54b2f9ed2b9c1593ef16", size = 10676120 }, - { url = "https://files.pythonhosted.org/packages/8d/c9/229a23d52a2983de1ad0fb0ee37d36e0257e6f28bfd6b498ee2c76361874/ruff-0.15.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6f6e80850a01eb13b3e42ee0ebdf6e4497151b48c35051aab51c101266d187a3", size = 11201636 }, - { url = "https://files.pythonhosted.org/packages/6f/b0/69adf22f4e24f3677208adb715c578266842e6e6a3cc77483f48dd999ede/ruff-0.15.0-py3-none-win32.whl", hash = "sha256:238a717ef803e501b6d51e0bdd0d2c6e8513fe9eec14002445134d3907cd46c3", size = 10465945 }, - { url = "https://files.pythonhosted.org/packages/51/ad/f813b6e2c97e9b4598be25e94a9147b9af7e60523b0cb5d94d307c15229d/ruff-0.15.0-py3-none-win_amd64.whl", hash = "sha256:dd5e4d3301dc01de614da3cdffc33d4b1b96fb89e45721f1598e5532ccf78b18", size = 11564657 }, - { url = "https://files.pythonhosted.org/packages/f6/b0/2d823f6e77ebe560f4e397d078487e8d52c1516b331e3521bc75db4272ca/ruff-0.15.0-py3-none-win_arm64.whl", hash = "sha256:c480d632cc0ca3f0727acac8b7d053542d9e114a462a145d0b00e7cd658c515a", size = 10865753 }, + { url = "https://files.pythonhosted.org/packages/bc/88/3fd1b0aa4b6330d6aaa63a285bc96c9f71970351579152d231ed90914586/ruff-0.15.0-py3-none-linux_armv6l.whl", hash = "sha256:aac4ebaa612a82b23d45964586f24ae9bc23ca101919f5590bdb368d74ad5455", size = 10354332, upload-time = "2026-02-03T17:52:54.892Z" }, + { url = "https://files.pythonhosted.org/packages/72/f6/62e173fbb7eb75cc29fe2576a1e20f0a46f671a2587b5f604bfb0eaf5f6f/ruff-0.15.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dcd4be7cc75cfbbca24a98d04d0b9b36a270d0833241f776b788d59f4142b14d", size = 10767189, upload-time = "2026-02-03T17:53:19.778Z" }, + { url = "https://files.pythonhosted.org/packages/99/e4/968ae17b676d1d2ff101d56dc69cf333e3a4c985e1ec23803df84fc7bf9e/ruff-0.15.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d747e3319b2bce179c7c1eaad3d884dc0a199b5f4d5187620530adf9105268ce", size = 10075384, upload-time = "2026-02-03T17:53:29.241Z" }, + { url = "https://files.pythonhosted.org/packages/a2/bf/9843c6044ab9e20af879c751487e61333ca79a2c8c3058b15722386b8cae/ruff-0.15.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:650bd9c56ae03102c51a5e4b554d74d825ff3abe4db22b90fd32d816c2e90621", size = 10481363, upload-time = "2026-02-03T17:52:43.332Z" }, + { url = "https://files.pythonhosted.org/packages/55/d9/4ada5ccf4cd1f532db1c8d44b6f664f2208d3d93acbeec18f82315e15193/ruff-0.15.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6664b7eac559e3048223a2da77769c2f92b43a6dfd4720cef42654299a599c9", size = 10187736, upload-time = "2026-02-03T17:53:00.522Z" }, + { url = "https://files.pythonhosted.org/packages/86/e2/f25eaecd446af7bb132af0a1d5b135a62971a41f5366ff41d06d25e77a91/ruff-0.15.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f811f97b0f092b35320d1556f3353bf238763420ade5d9e62ebd2b73f2ff179", size = 10968415, upload-time = "2026-02-03T17:53:15.705Z" }, + { url = "https://files.pythonhosted.org/packages/e7/dc/f06a8558d06333bf79b497d29a50c3a673d9251214e0d7ec78f90b30aa79/ruff-0.15.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:761ec0a66680fab6454236635a39abaf14198818c8cdf691e036f4bc0f406b2d", size = 11809643, upload-time = "2026-02-03T17:53:23.031Z" }, + { url = "https://files.pythonhosted.org/packages/dd/45/0ece8db2c474ad7df13af3a6d50f76e22a09d078af63078f005057ca59eb/ruff-0.15.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:940f11c2604d317e797b289f4f9f3fa5555ffe4fb574b55ed006c3d9b6f0eb78", size = 11234787, upload-time = "2026-02-03T17:52:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/8a/d9/0e3a81467a120fd265658d127db648e4d3acfe3e4f6f5d4ea79fac47e587/ruff-0.15.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcbca3d40558789126da91d7ef9a7c87772ee107033db7191edefa34e2c7f1b4", size = 11112797, upload-time = "2026-02-03T17:52:49.274Z" }, + { url = "https://files.pythonhosted.org/packages/b2/cb/8c0b3b0c692683f8ff31351dfb6241047fa873a4481a76df4335a8bff716/ruff-0.15.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9a121a96db1d75fa3eb39c4539e607f628920dd72ff1f7c5ee4f1b768ac62d6e", size = 11033133, upload-time = "2026-02-03T17:53:33.105Z" }, + { url = "https://files.pythonhosted.org/packages/f8/5e/23b87370cf0f9081a8c89a753e69a4e8778805b8802ccfe175cc410e50b9/ruff-0.15.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5298d518e493061f2eabd4abd067c7e4fb89e2f63291c94332e35631c07c3662", size = 10442646, upload-time = "2026-02-03T17:53:06.278Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9a/3c94de5ce642830167e6d00b5c75aacd73e6347b4c7fc6828699b150a5ee/ruff-0.15.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afb6e603d6375ff0d6b0cee563fa21ab570fd15e65c852cb24922cef25050cf1", size = 10195750, upload-time = "2026-02-03T17:53:26.084Z" }, + { url = "https://files.pythonhosted.org/packages/30/15/e396325080d600b436acc970848d69df9c13977942fb62bb8722d729bee8/ruff-0.15.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:77e515f6b15f828b94dc17d2b4ace334c9ddb7d9468c54b2f9ed2b9c1593ef16", size = 10676120, upload-time = "2026-02-03T17:53:09.363Z" }, + { url = "https://files.pythonhosted.org/packages/8d/c9/229a23d52a2983de1ad0fb0ee37d36e0257e6f28bfd6b498ee2c76361874/ruff-0.15.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6f6e80850a01eb13b3e42ee0ebdf6e4497151b48c35051aab51c101266d187a3", size = 11201636, upload-time = "2026-02-03T17:52:57.281Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b0/69adf22f4e24f3677208adb715c578266842e6e6a3cc77483f48dd999ede/ruff-0.15.0-py3-none-win32.whl", hash = "sha256:238a717ef803e501b6d51e0bdd0d2c6e8513fe9eec14002445134d3907cd46c3", size = 10465945, upload-time = "2026-02-03T17:53:12.591Z" }, + { url = "https://files.pythonhosted.org/packages/51/ad/f813b6e2c97e9b4598be25e94a9147b9af7e60523b0cb5d94d307c15229d/ruff-0.15.0-py3-none-win_amd64.whl", hash = "sha256:dd5e4d3301dc01de614da3cdffc33d4b1b96fb89e45721f1598e5532ccf78b18", size = 11564657, upload-time = "2026-02-03T17:52:51.893Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b0/2d823f6e77ebe560f4e397d078487e8d52c1516b331e3521bc75db4272ca/ruff-0.15.0-py3-none-win_arm64.whl", hash = "sha256:c480d632cc0ca3f0727acac8b7d053542d9e114a462a145d0b00e7cd658c515a", size = 10865753, upload-time = "2026-02-03T17:53:03.014Z" }, ] [[package]] name = "sniffio" version = "1.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, ] [[package]] name = "snowballstemmer" version = "2.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/44/7b/af302bebf22c749c56c9c3e8ae13190b5b5db37a33d9068652e8f73b7089/snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", size = 86699 } +sdist = { url = "https://files.pythonhosted.org/packages/44/7b/af302bebf22c749c56c9c3e8ae13190b5b5db37a33d9068652e8f73b7089/snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", size = 86699, upload-time = "2021-11-16T18:38:38.009Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/dc/c02e01294f7265e63a7315fe086dd1df7dacb9f840a804da846b96d01b96/snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a", size = 93002 }, + { url = "https://files.pythonhosted.org/packages/ed/dc/c02e01294f7265e63a7315fe086dd1df7dacb9f840a804da846b96d01b96/snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a", size = 93002, upload-time = "2021-11-16T18:38:34.792Z" }, ] [[package]] name = "soupsieve" version = "2.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/ce/fbaeed4f9fb8b2daa961f90591662df6a86c1abf25c548329a86920aedfb/soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb", size = 101569 } +sdist = { url = "https://files.pythonhosted.org/packages/d7/ce/fbaeed4f9fb8b2daa961f90591662df6a86c1abf25c548329a86920aedfb/soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb", size = 101569, upload-time = "2024-08-13T13:39:12.166Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/c2/fe97d779f3ef3b15f05c94a2f1e3d21732574ed441687474db9d342a7315/soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9", size = 36186 }, + { url = "https://files.pythonhosted.org/packages/d1/c2/fe97d779f3ef3b15f05c94a2f1e3d21732574ed441687474db9d342a7315/soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9", size = 36186, upload-time = "2024-08-13T13:39:10.986Z" }, ] [[package]] @@ -1291,9 +1296,9 @@ dependencies = [ { name = "sphinxcontrib-serializinghtml" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5b/be/50e50cb4f2eff47df05673d361095cafd95521d2a22521b920c67a372dcb/sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe", size = 8067911 } +sdist = { url = "https://files.pythonhosted.org/packages/5b/be/50e50cb4f2eff47df05673d361095cafd95521d2a22521b920c67a372dcb/sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe", size = 8067911, upload-time = "2024-07-20T14:46:56.059Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/ef/153f6803c5d5f8917dbb7f7fcf6d34a871ede3296fa89c2c703f5f8a6c8e/sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239", size = 3401624 }, + { url = "https://files.pythonhosted.org/packages/0d/ef/153f6803c5d5f8917dbb7f7fcf6d34a871ede3296fa89c2c703f5f8a6c8e/sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239", size = 3401624, upload-time = "2024-07-20T14:46:52.142Z" }, ] [[package]] @@ -1309,9 +1314,9 @@ dependencies = [ { name = "watchfiles" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a5/2c/155e1de2c1ba96a72e5dba152c509a8b41e047ee5c2def9e9f0d812f8be7/sphinx_autobuild-2024.10.3.tar.gz", hash = "sha256:248150f8f333e825107b6d4b86113ab28fa51750e5f9ae63b59dc339be951fb1", size = 14023 } +sdist = { url = "https://files.pythonhosted.org/packages/a5/2c/155e1de2c1ba96a72e5dba152c509a8b41e047ee5c2def9e9f0d812f8be7/sphinx_autobuild-2024.10.3.tar.gz", hash = "sha256:248150f8f333e825107b6d4b86113ab28fa51750e5f9ae63b59dc339be951fb1", size = 14023, upload-time = "2024-10-02T23:15:30.172Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/c0/eba125db38c84d3c74717008fd3cb5000b68cd7e2cbafd1349c6a38c3d3b/sphinx_autobuild-2024.10.3-py3-none-any.whl", hash = "sha256:158e16c36f9d633e613c9aaf81c19b0fc458ca78b112533b20dafcda430d60fa", size = 11908 }, + { url = "https://files.pythonhosted.org/packages/18/c0/eba125db38c84d3c74717008fd3cb5000b68cd7e2cbafd1349c6a38c3d3b/sphinx_autobuild-2024.10.3-py3-none-any.whl", hash = "sha256:158e16c36f9d633e613c9aaf81c19b0fc458ca78b112533b20dafcda430d60fa", size = 11908, upload-time = "2024-10-02T23:15:28.739Z" }, ] [[package]] @@ -1321,9 +1326,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sphinx" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/98/0b/a866924ded68efec7a1759587a4e478aec7559d8165fac8b2ad1c0e774d6/sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9", size = 20736 } +sdist = { url = "https://files.pythonhosted.org/packages/98/0b/a866924ded68efec7a1759587a4e478aec7559d8165fac8b2ad1c0e774d6/sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9", size = 20736, upload-time = "2023-07-08T18:40:54.166Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/dd/018ce05c532a22007ac58d4f45232514cd9d6dd0ee1dc374e309db830983/sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b", size = 22496 }, + { url = "https://files.pythonhosted.org/packages/3c/dd/018ce05c532a22007ac58d4f45232514cd9d6dd0ee1dc374e309db830983/sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b", size = 22496, upload-time = "2023-07-08T18:40:52.659Z" }, ] [[package]] @@ -1333,9 +1338,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sphinx" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/2b/a964715e7f5295f77509e59309959f4125122d648f86b4fe7d70ca1d882c/sphinx-copybutton-0.5.2.tar.gz", hash = "sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd", size = 23039 } +sdist = { url = "https://files.pythonhosted.org/packages/fc/2b/a964715e7f5295f77509e59309959f4125122d648f86b4fe7d70ca1d882c/sphinx-copybutton-0.5.2.tar.gz", hash = "sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd", size = 23039, upload-time = "2023-04-14T08:10:22.998Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/48/1ea60e74949eecb12cdd6ac43987f9fd331156388dcc2319b45e2ebb81bf/sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e", size = 13343 }, + { url = "https://files.pythonhosted.org/packages/9e/48/1ea60e74949eecb12cdd6ac43987f9fd331156388dcc2319b45e2ebb81bf/sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e", size = 13343, upload-time = "2023-04-14T08:10:20.844Z" }, ] [[package]] @@ -1346,72 +1351,105 @@ dependencies = [ { name = "pillow" }, { name = "sphinx" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ac/84/e4b4cde6ea2f3a1dd7d523dcf28260e93999b4882cc352f8bc6a14cbd848/sphinx_gallery-0.18.0.tar.gz", hash = "sha256:4b5b5bc305348c01d00cf66ad852cfd2dd8b67f7f32ae3e2820c01557b3f92f9", size = 466371 } +sdist = { url = "https://files.pythonhosted.org/packages/ac/84/e4b4cde6ea2f3a1dd7d523dcf28260e93999b4882cc352f8bc6a14cbd848/sphinx_gallery-0.18.0.tar.gz", hash = "sha256:4b5b5bc305348c01d00cf66ad852cfd2dd8b67f7f32ae3e2820c01557b3f92f9", size = 466371, upload-time = "2024-10-09T06:01:28.005Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/f3/8fd1ef84f318d404dcd713c54647e616d93396beb28db216e281ba86d728/sphinx_gallery-0.18.0-py3-none-any.whl", hash = "sha256:54317366e77b182672797e5b46ab13cca9a27eafc3142c59dc4c211d4afe3420", size = 448725 }, + { url = "https://files.pythonhosted.org/packages/a3/f3/8fd1ef84f318d404dcd713c54647e616d93396beb28db216e281ba86d728/sphinx_gallery-0.18.0-py3-none-any.whl", hash = "sha256:54317366e77b182672797e5b46ab13cca9a27eafc3142c59dc4c211d4afe3420", size = 448725, upload-time = "2024-10-09T06:01:25.93Z" }, ] [[package]] name = "sphinxcontrib-applehelp" version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053 } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300 }, + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, ] [[package]] name = "sphinxcontrib-devhelp" version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967 } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530 }, + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, ] [[package]] name = "sphinxcontrib-htmlhelp" version = "2.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617 } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705 }, + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, ] [[package]] name = "sphinxcontrib-jsmath" version = "1.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787 } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, +] + +[[package]] +name = "sphinxcontrib-mermaid" +version = "1.2.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "pyyaml", marker = "python_full_version < '3.10'" }, + { name = "sphinx", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/49/c6ddfe709a4ab76ac6e5a00e696f73626b2c189dc1e1965a361ec102e6cc/sphinxcontrib_mermaid-1.2.3.tar.gz", hash = "sha256:358699d0ec924ef679b41873d9edd97d0773446daf9760c75e18dc0adfd91371", size = 18885, upload-time = "2025-11-26T04:18:32.43Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/39/8b54299ffa00e597d3b0b4d042241a0a0b22cb429ad007ccfb9c1745b4d1/sphinxcontrib_mermaid-1.2.3-py3-none-any.whl", hash = "sha256:5be782b27026bef97bfb15ccb2f7868b674a1afc0982b54cb149702cfc25aa02", size = 13413, upload-time = "2025-11-26T04:18:31.269Z" }, +] + +[[package]] +name = "sphinxcontrib-mermaid" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "jinja2", marker = "python_full_version >= '3.10'" }, + { name = "pyyaml", marker = "python_full_version >= '3.10'" }, + { name = "sphinx", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2b/ae/999891de292919b66ea34f2c22fc22c9be90ab3536fbc0fca95716277351/sphinxcontrib_mermaid-2.0.1.tar.gz", hash = "sha256:a21a385a059a6cafd192aa3a586b14bf5c42721e229db67b459dc825d7f0a497", size = 19839, upload-time = "2026-03-05T14:10:41.901Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071 }, + { url = "https://files.pythonhosted.org/packages/03/46/25d64bcd7821c8d6f1080e1c43d5fcdfc442a18f759a230b5ccdc891093e/sphinxcontrib_mermaid-2.0.1-py3-none-any.whl", hash = "sha256:9dca7fbe827bad5e7e2b97c4047682cfd26e3e07398cfdc96c7a8842ae7f06e7", size = 14064, upload-time = "2026-03-05T14:10:40.533Z" }, ] [[package]] name = "sphinxcontrib-qthelp" version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165 } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743 }, + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, ] [[package]] name = "sphinxcontrib-serializinghtml" version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080 } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072 }, + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, ] [[package]] name = "sqlparse" version = "0.5.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/73/82/dfa23ec2cbed08a801deab02fe7c904bfb00765256b155941d789a338c68/sqlparse-0.5.1.tar.gz", hash = "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e", size = 84502 } +sdist = { url = "https://files.pythonhosted.org/packages/73/82/dfa23ec2cbed08a801deab02fe7c904bfb00765256b155941d789a338c68/sqlparse-0.5.1.tar.gz", hash = "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e", size = 84502, upload-time = "2024-07-15T19:30:27.085Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/a5/b2860373aa8de1e626b2bdfdd6df4355f0565b47e51f7d0c54fe70faf8fe/sqlparse-0.5.1-py3-none-any.whl", hash = "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4", size = 44156 }, + { url = "https://files.pythonhosted.org/packages/5d/a5/b2860373aa8de1e626b2bdfdd6df4355f0565b47e51f7d0c54fe70faf8fe/sqlparse-0.5.1-py3-none-any.whl", hash = "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4", size = 44156, upload-time = "2024-07-15T19:30:25.033Z" }, ] [[package]] @@ -1425,9 +1463,9 @@ dependencies = [ { name = "anyio", marker = "python_full_version < '3.10'" }, { name = "typing-extensions", marker = "python_full_version < '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8b/d0/0332bd8a25779a0e2082b0e179805ad39afad642938b371ae0882e7f880d/starlette-0.47.0.tar.gz", hash = "sha256:1f64887e94a447fed5f23309fb6890ef23349b7e478faa7b24a851cd4eb844af", size = 2582856 } +sdist = { url = "https://files.pythonhosted.org/packages/8b/d0/0332bd8a25779a0e2082b0e179805ad39afad642938b371ae0882e7f880d/starlette-0.47.0.tar.gz", hash = "sha256:1f64887e94a447fed5f23309fb6890ef23349b7e478faa7b24a851cd4eb844af", size = 2582856, upload-time = "2025-05-29T15:45:27.628Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/81/c60b35fe9674f63b38a8feafc414fca0da378a9dbd5fa1e0b8d23fcc7a9b/starlette-0.47.0-py3-none-any.whl", hash = "sha256:9d052d4933683af40ffd47c7465433570b4949dc937e20ad1d73b34e72f10c37", size = 72796 }, + { url = "https://files.pythonhosted.org/packages/e3/81/c60b35fe9674f63b38a8feafc414fca0da378a9dbd5fa1e0b8d23fcc7a9b/starlette-0.47.0-py3-none-any.whl", hash = "sha256:9d052d4933683af40ffd47c7465433570b4949dc937e20ad1d73b34e72f10c37", size = 72796, upload-time = "2025-05-29T15:45:26.305Z" }, ] [[package]] @@ -1441,54 +1479,54 @@ dependencies = [ { name = "anyio", marker = "python_full_version >= '3.10'" }, { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/de/1a/608df0b10b53b0beb96a37854ee05864d182ddd4b1156a22f1ad3860425a/starlette-0.49.3.tar.gz", hash = "sha256:1c14546f299b5901a1ea0e34410575bc33bbd741377a10484a54445588d00284", size = 2655031 } +sdist = { url = "https://files.pythonhosted.org/packages/de/1a/608df0b10b53b0beb96a37854ee05864d182ddd4b1156a22f1ad3860425a/starlette-0.49.3.tar.gz", hash = "sha256:1c14546f299b5901a1ea0e34410575bc33bbd741377a10484a54445588d00284", size = 2655031, upload-time = "2025-11-01T15:12:26.13Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/e0/021c772d6a662f43b63044ab481dc6ac7592447605b5b35a957785363122/starlette-0.49.3-py3-none-any.whl", hash = "sha256:b579b99715fdc2980cf88c8ec96d3bf1ce16f5a8051a7c2b84ef9b1cdecaea2f", size = 74340 }, + { url = "https://files.pythonhosted.org/packages/a3/e0/021c772d6a662f43b63044ab481dc6ac7592447605b5b35a957785363122/starlette-0.49.3-py3-none-any.whl", hash = "sha256:b579b99715fdc2980cf88c8ec96d3bf1ce16f5a8051a7c2b84ef9b1cdecaea2f", size = 74340, upload-time = "2025-11-01T15:12:24.387Z" }, ] [[package]] name = "termcolor" version = "2.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b8/85/147a0529b4e80b6b9d021ca8db3a820fcac53ec7374b87073d004aaf444c/termcolor-2.3.0.tar.gz", hash = "sha256:b5b08f68937f138fe92f6c089b99f1e2da0ae56c52b78bf7075fd95420fd9a5a", size = 12163 } +sdist = { url = "https://files.pythonhosted.org/packages/b8/85/147a0529b4e80b6b9d021ca8db3a820fcac53ec7374b87073d004aaf444c/termcolor-2.3.0.tar.gz", hash = "sha256:b5b08f68937f138fe92f6c089b99f1e2da0ae56c52b78bf7075fd95420fd9a5a", size = 12163, upload-time = "2023-04-23T19:45:24.004Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/67/e1/434566ffce04448192369c1a282931cf4ae593e91907558eaecd2e9f2801/termcolor-2.3.0-py3-none-any.whl", hash = "sha256:3afb05607b89aed0ffe25202399ee0867ad4d3cb4180d98aaf8eefa6a5f7d475", size = 6872 }, + { url = "https://files.pythonhosted.org/packages/67/e1/434566ffce04448192369c1a282931cf4ae593e91907558eaecd2e9f2801/termcolor-2.3.0-py3-none-any.whl", hash = "sha256:3afb05607b89aed0ffe25202399ee0867ad4d3cb4180d98aaf8eefa6a5f7d475", size = 6872, upload-time = "2023-04-23T19:45:22.671Z" }, ] [[package]] name = "tomli" version = "2.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c0/3f/d7af728f075fb08564c5949a9c95e44352e23dee646869fa104a3b2060a3/tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f", size = 15164 } +sdist = { url = "https://files.pythonhosted.org/packages/c0/3f/d7af728f075fb08564c5949a9c95e44352e23dee646869fa104a3b2060a3/tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f", size = 15164, upload-time = "2022-02-08T10:54:04.006Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", size = 12757 }, + { url = "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", size = 12757, upload-time = "2022-02-08T10:54:02.017Z" }, ] [[package]] name = "typing-extensions" version = "4.13.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 } +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 }, + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, ] [[package]] name = "tzdata" version = "2024.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e1/34/943888654477a574a86a98e9896bae89c7aa15078ec29f490fef2f1e5384/tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc", size = 193282 } +sdist = { url = "https://files.pythonhosted.org/packages/e1/34/943888654477a574a86a98e9896bae89c7aa15078ec29f490fef2f1e5384/tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc", size = 193282, upload-time = "2024-09-23T18:56:46.89Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/ab/7e5f53c3b9d14972843a647d8d7a853969a58aecc7559cb3267302c94774/tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd", size = 346586 }, + { url = "https://files.pythonhosted.org/packages/a6/ab/7e5f53c3b9d14972843a647d8d7a853969a58aecc7559cb3267302c94774/tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd", size = 346586, upload-time = "2024-09-23T18:56:45.478Z" }, ] [[package]] name = "urllib3" version = "2.6.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556 } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584 }, + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] [[package]] @@ -1500,9 +1538,9 @@ dependencies = [ { name = "h11" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e0/fc/1d785078eefd6945f3e5bab5c076e4230698046231eb0f3747bc5c8fa992/uvicorn-0.32.0.tar.gz", hash = "sha256:f78b36b143c16f54ccdb8190d0a26b5f1901fe5a3c777e1ab29f26391af8551e", size = 77564 } +sdist = { url = "https://files.pythonhosted.org/packages/e0/fc/1d785078eefd6945f3e5bab5c076e4230698046231eb0f3747bc5c8fa992/uvicorn-0.32.0.tar.gz", hash = "sha256:f78b36b143c16f54ccdb8190d0a26b5f1901fe5a3c777e1ab29f26391af8551e", size = 77564, upload-time = "2024-10-15T17:27:33.848Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/14/78bd0e95dd2444b6caacbca2b730671d4295ccb628ef58b81bee903629df/uvicorn-0.32.0-py3-none-any.whl", hash = "sha256:60b8f3a5ac027dcd31448f411ced12b5ef452c646f76f02f8cc3f25d8d26fd82", size = 63723 }, + { url = "https://files.pythonhosted.org/packages/eb/14/78bd0e95dd2444b6caacbca2b730671d4295ccb628ef58b81bee903629df/uvicorn-0.32.0-py3-none-any.whl", hash = "sha256:60b8f3a5ac027dcd31448f411ced12b5ef452c646f76f02f8cc3f25d8d26fd82", size = 63723, upload-time = "2024-10-15T17:27:32.022Z" }, ] [[package]] @@ -1517,9 +1555,9 @@ dependencies = [ { name = "filelock", version = "3.12.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "platformdirs", marker = "python_full_version < '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3f/40/abc5a766da6b0b2457f819feab8e9203cbeae29327bd241359f866a3da9d/virtualenv-20.26.6.tar.gz", hash = "sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48", size = 9372482 } +sdist = { url = "https://files.pythonhosted.org/packages/3f/40/abc5a766da6b0b2457f819feab8e9203cbeae29327bd241359f866a3da9d/virtualenv-20.26.6.tar.gz", hash = "sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48", size = 9372482, upload-time = "2024-09-27T16:28:57.502Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/59/90/57b8ac0c8a231545adc7698c64c5a36fa7cd8e376c691b9bde877269f2eb/virtualenv-20.26.6-py3-none-any.whl", hash = "sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2", size = 5999862 }, + { url = "https://files.pythonhosted.org/packages/59/90/57b8ac0c8a231545adc7698c64c5a36fa7cd8e376c691b9bde877269f2eb/virtualenv-20.26.6-py3-none-any.whl", hash = "sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2", size = 5999862, upload-time = "2024-09-27T16:28:54.798Z" }, ] [[package]] @@ -1535,9 +1573,9 @@ dependencies = [ { name = "platformdirs", marker = "python_full_version >= '3.10'" }, { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239 } +sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258 }, + { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" }, ] [[package]] @@ -1547,199 +1585,199 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318 }, - { url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478 }, - { url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894 }, - { url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065 }, - { url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377 }, - { url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837 }, - { url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456 }, - { url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614 }, - { url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690 }, - { url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459 }, - { url = "https://files.pythonhosted.org/packages/f1/ac/c9bb0ec696e07a20bd58af5399aeadaef195fb2c73d26baf55180fe4a942/watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", size = 272663 }, - { url = "https://files.pythonhosted.org/packages/11/a0/a60c5a7c2ec59fa062d9a9c61d02e3b6abd94d32aac2d8344c4bdd033326/watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", size = 287453 }, - { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529 }, - { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384 }, - { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789 }, - { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521 }, - { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722 }, - { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088 }, - { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923 }, - { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080 }, - { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432 }, - { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046 }, - { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473 }, - { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598 }, - { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210 }, - { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745 }, - { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769 }, - { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374 }, - { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485 }, - { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813 }, - { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816 }, - { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186 }, - { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812 }, - { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196 }, - { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657 }, - { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042 }, - { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410 }, - { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209 }, - { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321 }, - { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783 }, - { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279 }, - { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405 }, - { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976 }, - { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506 }, - { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936 }, - { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147 }, - { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007 }, - { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280 }, - { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056 }, - { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162 }, - { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909 }, - { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389 }, - { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964 }, - { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114 }, - { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264 }, - { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877 }, - { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176 }, - { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577 }, - { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425 }, - { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826 }, - { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208 }, - { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315 }, - { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869 }, - { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919 }, - { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845 }, - { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027 }, - { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615 }, - { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836 }, - { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099 }, - { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626 }, - { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519 }, - { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078 }, - { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664 }, - { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154 }, - { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820 }, - { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510 }, - { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408 }, - { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968 }, - { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096 }, - { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040 }, - { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847 }, - { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072 }, - { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104 }, - { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112 }, - { url = "https://files.pythonhosted.org/packages/a4/68/a7303a15cc797ab04d58f1fea7f67c50bd7f80090dfd7e750e7576e07582/watchfiles-1.1.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c882d69f6903ef6092bedfb7be973d9319940d56b8427ab9187d1ecd73438a70", size = 409220 }, - { url = "https://files.pythonhosted.org/packages/99/b8/d1857ce9ac76034c053fa7ef0e0ef92d8bd031e842ea6f5171725d31e88f/watchfiles-1.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d6ff426a7cb54f310d51bfe83fe9f2bbe40d540c741dc974ebc30e6aa238f52e", size = 396712 }, - { url = "https://files.pythonhosted.org/packages/41/7a/da7ada566f48beaa6a30b13335b49d1f6febaf3a5ddbd1d92163a1002cf4/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79ff6c6eadf2e3fc0d7786331362e6ef1e51125892c75f1004bd6b52155fb956", size = 451462 }, - { url = "https://files.pythonhosted.org/packages/e2/b2/7cb9e0d5445a8d45c4cccd68a590d9e3a453289366b96ff37d1075aaebef/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c1f5210f1b8fc91ead1283c6fd89f70e76fb07283ec738056cf34d51e9c1d62c", size = 460811 }, - { url = "https://files.pythonhosted.org/packages/04/9d/b07d4491dde6db6ea6c680fdec452f4be363d65c82004faf2d853f59b76f/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9c4702f29ca48e023ffd9b7ff6b822acdf47cb1ff44cb490a3f1d5ec8987e9c", size = 490576 }, - { url = "https://files.pythonhosted.org/packages/56/03/e64dcab0a1806157db272a61b7891b062f441a30580a581ae72114259472/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acb08650863767cbc58bca4813b92df4d6c648459dcaa3d4155681962b2aa2d3", size = 597726 }, - { url = "https://files.pythonhosted.org/packages/5c/8e/a827cf4a8d5f2903a19a934dcf512082eb07675253e154d4cd9367978a58/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08af70fd77eee58549cd69c25055dc344f918d992ff626068242259f98d598a2", size = 474900 }, - { url = "https://files.pythonhosted.org/packages/dc/a6/94fed0b346b85b22303a12eee5f431006fae6af70d841cac2f4403245533/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c3631058c37e4a0ec440bf583bc53cdbd13e5661bb6f465bc1d88ee9a0a4d02", size = 457521 }, - { url = "https://files.pythonhosted.org/packages/c4/64/bc3331150e8f3c778d48a4615d4b72b3d2d87868635e6c54bbd924946189/watchfiles-1.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cf57a27fb986c6243d2ee78392c503826056ffe0287e8794503b10fb51b881be", size = 632191 }, - { url = "https://files.pythonhosted.org/packages/e4/84/f39e19549c2f3ec97225dcb2ceb9a7bb3c5004ed227aad1f321bf0ff2051/watchfiles-1.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d7e7067c98040d646982daa1f37a33d3544138ea155536c2e0e63e07ff8a7e0f", size = 623923 }, - { url = "https://files.pythonhosted.org/packages/0e/24/0759ae15d9a0c9c5fe946bd4cf45ab9e7bad7cfede2c06dc10f59171b29f/watchfiles-1.1.1-cp39-cp39-win32.whl", hash = "sha256:6c9c9262f454d1c4d8aaa7050121eb4f3aea197360553699520767daebf2180b", size = 274010 }, - { url = "https://files.pythonhosted.org/packages/7e/3b/eb26cddd4dfa081e2bf6918be3b2fc05ee3b55c1d21331d5562ee0c6aaad/watchfiles-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:74472234c8370669850e1c312490f6026d132ca2d396abfad8830b4f1c096957", size = 289090 }, - { url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611 }, - { url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889 }, - { url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616 }, - { url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413 }, - { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250 }, - { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117 }, - { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493 }, - { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546 }, - { url = "https://files.pythonhosted.org/packages/00/db/38a2c52fdbbfe2fc7ffaaaaaebc927d52b9f4d5139bba3186c19a7463001/watchfiles-1.1.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdab464fee731e0884c35ae3588514a9bcf718d0e2c82169c1c4a85cc19c3c7f", size = 409210 }, - { url = "https://files.pythonhosted.org/packages/d1/43/d7e8b71f6c21ff813ee8da1006f89b6c7fff047fb4c8b16ceb5e840599c5/watchfiles-1.1.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:3dbd8cbadd46984f802f6d479b7e3afa86c42d13e8f0f322d669d79722c8ec34", size = 397286 }, - { url = "https://files.pythonhosted.org/packages/1f/5d/884074a5269317e75bd0b915644b702b89de73e61a8a7446e2b225f45b1f/watchfiles-1.1.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5524298e3827105b61951a29c3512deb9578586abf3a7c5da4a8069df247cccc", size = 451768 }, - { url = "https://files.pythonhosted.org/packages/17/71/7ffcaa9b5e8961a25026058058c62ec8f604d2a6e8e1e94bee8a09e1593f/watchfiles-1.1.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b943d3668d61cfa528eb949577479d3b077fd25fb83c641235437bc0b5bc60e", size = 458561 }, +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318, upload-time = "2025-10-14T15:04:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478, upload-time = "2025-10-14T15:04:20.297Z" }, + { url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894, upload-time = "2025-10-14T15:04:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065, upload-time = "2025-10-14T15:04:22.795Z" }, + { url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377, upload-time = "2025-10-14T15:04:24.138Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837, upload-time = "2025-10-14T15:04:25.057Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456, upload-time = "2025-10-14T15:04:26.497Z" }, + { url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614, upload-time = "2025-10-14T15:04:27.539Z" }, + { url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690, upload-time = "2025-10-14T15:04:28.495Z" }, + { url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459, upload-time = "2025-10-14T15:04:29.491Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ac/c9bb0ec696e07a20bd58af5399aeadaef195fb2c73d26baf55180fe4a942/watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", size = 272663, upload-time = "2025-10-14T15:04:30.435Z" }, + { url = "https://files.pythonhosted.org/packages/11/a0/a60c5a7c2ec59fa062d9a9c61d02e3b6abd94d32aac2d8344c4bdd033326/watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", size = 287453, upload-time = "2025-10-14T15:04:31.53Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, + { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, + { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, + { url = "https://files.pythonhosted.org/packages/a4/68/a7303a15cc797ab04d58f1fea7f67c50bd7f80090dfd7e750e7576e07582/watchfiles-1.1.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c882d69f6903ef6092bedfb7be973d9319940d56b8427ab9187d1ecd73438a70", size = 409220, upload-time = "2025-10-14T15:05:51.917Z" }, + { url = "https://files.pythonhosted.org/packages/99/b8/d1857ce9ac76034c053fa7ef0e0ef92d8bd031e842ea6f5171725d31e88f/watchfiles-1.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d6ff426a7cb54f310d51bfe83fe9f2bbe40d540c741dc974ebc30e6aa238f52e", size = 396712, upload-time = "2025-10-14T15:05:53.437Z" }, + { url = "https://files.pythonhosted.org/packages/41/7a/da7ada566f48beaa6a30b13335b49d1f6febaf3a5ddbd1d92163a1002cf4/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79ff6c6eadf2e3fc0d7786331362e6ef1e51125892c75f1004bd6b52155fb956", size = 451462, upload-time = "2025-10-14T15:05:54.742Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b2/7cb9e0d5445a8d45c4cccd68a590d9e3a453289366b96ff37d1075aaebef/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c1f5210f1b8fc91ead1283c6fd89f70e76fb07283ec738056cf34d51e9c1d62c", size = 460811, upload-time = "2025-10-14T15:05:55.743Z" }, + { url = "https://files.pythonhosted.org/packages/04/9d/b07d4491dde6db6ea6c680fdec452f4be363d65c82004faf2d853f59b76f/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9c4702f29ca48e023ffd9b7ff6b822acdf47cb1ff44cb490a3f1d5ec8987e9c", size = 490576, upload-time = "2025-10-14T15:05:56.983Z" }, + { url = "https://files.pythonhosted.org/packages/56/03/e64dcab0a1806157db272a61b7891b062f441a30580a581ae72114259472/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acb08650863767cbc58bca4813b92df4d6c648459dcaa3d4155681962b2aa2d3", size = 597726, upload-time = "2025-10-14T15:05:57.986Z" }, + { url = "https://files.pythonhosted.org/packages/5c/8e/a827cf4a8d5f2903a19a934dcf512082eb07675253e154d4cd9367978a58/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08af70fd77eee58549cd69c25055dc344f918d992ff626068242259f98d598a2", size = 474900, upload-time = "2025-10-14T15:05:59.378Z" }, + { url = "https://files.pythonhosted.org/packages/dc/a6/94fed0b346b85b22303a12eee5f431006fae6af70d841cac2f4403245533/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c3631058c37e4a0ec440bf583bc53cdbd13e5661bb6f465bc1d88ee9a0a4d02", size = 457521, upload-time = "2025-10-14T15:06:00.419Z" }, + { url = "https://files.pythonhosted.org/packages/c4/64/bc3331150e8f3c778d48a4615d4b72b3d2d87868635e6c54bbd924946189/watchfiles-1.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cf57a27fb986c6243d2ee78392c503826056ffe0287e8794503b10fb51b881be", size = 632191, upload-time = "2025-10-14T15:06:01.621Z" }, + { url = "https://files.pythonhosted.org/packages/e4/84/f39e19549c2f3ec97225dcb2ceb9a7bb3c5004ed227aad1f321bf0ff2051/watchfiles-1.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d7e7067c98040d646982daa1f37a33d3544138ea155536c2e0e63e07ff8a7e0f", size = 623923, upload-time = "2025-10-14T15:06:02.671Z" }, + { url = "https://files.pythonhosted.org/packages/0e/24/0759ae15d9a0c9c5fe946bd4cf45ab9e7bad7cfede2c06dc10f59171b29f/watchfiles-1.1.1-cp39-cp39-win32.whl", hash = "sha256:6c9c9262f454d1c4d8aaa7050121eb4f3aea197360553699520767daebf2180b", size = 274010, upload-time = "2025-10-14T15:06:03.779Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3b/eb26cddd4dfa081e2bf6918be3b2fc05ee3b55c1d21331d5562ee0c6aaad/watchfiles-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:74472234c8370669850e1c312490f6026d132ca2d396abfad8830b4f1c096957", size = 289090, upload-time = "2025-10-14T15:06:04.821Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611, upload-time = "2025-10-14T15:06:05.809Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889, upload-time = "2025-10-14T15:06:07.035Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616, upload-time = "2025-10-14T15:06:08.072Z" }, + { url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413, upload-time = "2025-10-14T15:06:09.209Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, + { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, + { url = "https://files.pythonhosted.org/packages/00/db/38a2c52fdbbfe2fc7ffaaaaaebc927d52b9f4d5139bba3186c19a7463001/watchfiles-1.1.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdab464fee731e0884c35ae3588514a9bcf718d0e2c82169c1c4a85cc19c3c7f", size = 409210, upload-time = "2025-10-14T15:06:14.492Z" }, + { url = "https://files.pythonhosted.org/packages/d1/43/d7e8b71f6c21ff813ee8da1006f89b6c7fff047fb4c8b16ceb5e840599c5/watchfiles-1.1.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:3dbd8cbadd46984f802f6d479b7e3afa86c42d13e8f0f322d669d79722c8ec34", size = 397286, upload-time = "2025-10-14T15:06:16.177Z" }, + { url = "https://files.pythonhosted.org/packages/1f/5d/884074a5269317e75bd0b915644b702b89de73e61a8a7446e2b225f45b1f/watchfiles-1.1.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5524298e3827105b61951a29c3512deb9578586abf3a7c5da4a8069df247cccc", size = 451768, upload-time = "2025-10-14T15:06:18.266Z" }, + { url = "https://files.pythonhosted.org/packages/17/71/7ffcaa9b5e8961a25026058058c62ec8f604d2a6e8e1e94bee8a09e1593f/watchfiles-1.1.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b943d3668d61cfa528eb949577479d3b077fd25fb83c641235437bc0b5bc60e", size = 458561, upload-time = "2025-10-14T15:06:19.323Z" }, ] [[package]] name = "websockets" version = "13.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e2/73/9223dbc7be3dcaf2a7bbf756c351ec8da04b1fa573edaf545b95f6b0c7fd/websockets-13.1.tar.gz", hash = "sha256:a3b3366087c1bc0a2795111edcadddb8b3b59509d5db5d7ea3fdd69f954a8878", size = 158549 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/94/d15dbfc6a5eb636dbc754303fba18208f2e88cf97e733e1d64fb9cb5c89e/websockets-13.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f48c749857f8fb598fb890a75f540e3221d0976ed0bf879cf3c7eef34151acee", size = 157815 }, - { url = "https://files.pythonhosted.org/packages/30/02/c04af33f4663945a26f5e8cf561eb140c35452b50af47a83c3fbcfe62ae1/websockets-13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c7e72ce6bda6fb9409cc1e8164dd41d7c91466fb599eb047cfda72fe758a34a7", size = 155466 }, - { url = "https://files.pythonhosted.org/packages/35/e8/719f08d12303ea643655e52d9e9851b2dadbb1991d4926d9ce8862efa2f5/websockets-13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f779498eeec470295a2b1a5d97aa1bc9814ecd25e1eb637bd9d1c73a327387f6", size = 155716 }, - { url = "https://files.pythonhosted.org/packages/91/e1/14963ae0252a8925f7434065d25dcd4701d5e281a0b4b460a3b5963d2594/websockets-13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676df3fe46956fbb0437d8800cd5f2b6d41143b6e7e842e60554398432cf29b", size = 164806 }, - { url = "https://files.pythonhosted.org/packages/ec/fa/ab28441bae5e682a0f7ddf3d03440c0c352f930da419301f4a717f675ef3/websockets-13.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7affedeb43a70351bb811dadf49493c9cfd1ed94c9c70095fd177e9cc1541fa", size = 163810 }, - { url = "https://files.pythonhosted.org/packages/44/77/dea187bd9d16d4b91566a2832be31f99a40d0f5bfa55eeb638eb2c3bc33d/websockets-13.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1971e62d2caa443e57588e1d82d15f663b29ff9dfe7446d9964a4b6f12c1e700", size = 164125 }, - { url = "https://files.pythonhosted.org/packages/cf/d9/3af14544e83f1437eb684b399e6ba0fa769438e869bf5d83d74bc197fae8/websockets-13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5f2e75431f8dc4a47f31565a6e1355fb4f2ecaa99d6b89737527ea917066e26c", size = 164532 }, - { url = "https://files.pythonhosted.org/packages/1c/8a/6d332eabe7d59dfefe4b8ba6f46c8c5fabb15b71c8a8bc3d2b65de19a7b6/websockets-13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:58cf7e75dbf7e566088b07e36ea2e3e2bd5676e22216e4cad108d4df4a7402a0", size = 163948 }, - { url = "https://files.pythonhosted.org/packages/1a/91/a0aeadbaf3017467a1ee03f8fb67accdae233fe2d5ad4b038c0a84e357b0/websockets-13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c90d6dec6be2c7d03378a574de87af9b1efea77d0c52a8301dd831ece938452f", size = 163898 }, - { url = "https://files.pythonhosted.org/packages/71/31/a90fb47c63e0ae605be914b0b969d7c6e6ffe2038cd744798e4b3fbce53b/websockets-13.1-cp310-cp310-win32.whl", hash = "sha256:730f42125ccb14602f455155084f978bd9e8e57e89b569b4d7f0f0c17a448ffe", size = 158706 }, - { url = "https://files.pythonhosted.org/packages/93/ca/9540a9ba80da04dc7f36d790c30cae4252589dbd52ccdc92e75b0be22437/websockets-13.1-cp310-cp310-win_amd64.whl", hash = "sha256:5993260f483d05a9737073be197371940c01b257cc45ae3f1d5d7adb371b266a", size = 159141 }, - { url = "https://files.pythonhosted.org/packages/b2/f0/cf0b8a30d86b49e267ac84addbebbc7a48a6e7bb7c19db80f62411452311/websockets-13.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:61fc0dfcda609cda0fc9fe7977694c0c59cf9d749fbb17f4e9483929e3c48a19", size = 157813 }, - { url = "https://files.pythonhosted.org/packages/bf/e7/22285852502e33071a8cf0ac814f8988480ec6db4754e067b8b9d0e92498/websockets-13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ceec59f59d092c5007e815def4ebb80c2de330e9588e101cf8bd94c143ec78a5", size = 155469 }, - { url = "https://files.pythonhosted.org/packages/68/d4/c8c7c1e5b40ee03c5cc235955b0fb1ec90e7e37685a5f69229ad4708dcde/websockets-13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1dca61c6db1166c48b95198c0b7d9c990b30c756fc2923cc66f68d17dc558fd", size = 155717 }, - { url = "https://files.pythonhosted.org/packages/c9/e4/c50999b9b848b1332b07c7fd8886179ac395cb766fda62725d1539e7bc6c/websockets-13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:308e20f22c2c77f3f39caca508e765f8725020b84aa963474e18c59accbf4c02", size = 165379 }, - { url = "https://files.pythonhosted.org/packages/bc/49/4a4ad8c072f18fd79ab127650e47b160571aacfc30b110ee305ba25fffc9/websockets-13.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d516c325e6540e8a57b94abefc3459d7dab8ce52ac75c96cad5549e187e3a7", size = 164376 }, - { url = "https://files.pythonhosted.org/packages/af/9b/8c06d425a1d5a74fd764dd793edd02be18cf6fc3b1ccd1f29244ba132dc0/websockets-13.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c6e35319b46b99e168eb98472d6c7d8634ee37750d7693656dc766395df096", size = 164753 }, - { url = "https://files.pythonhosted.org/packages/d5/5b/0acb5815095ff800b579ffc38b13ab1b915b317915023748812d24e0c1ac/websockets-13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5f9fee94ebafbc3117c30be1844ed01a3b177bb6e39088bc6b2fa1dc15572084", size = 165051 }, - { url = "https://files.pythonhosted.org/packages/30/93/c3891c20114eacb1af09dedfcc620c65c397f4fd80a7009cd12d9457f7f5/websockets-13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7c1e90228c2f5cdde263253fa5db63e6653f1c00e7ec64108065a0b9713fa1b3", size = 164489 }, - { url = "https://files.pythonhosted.org/packages/28/09/af9e19885539759efa2e2cd29b8b3f9eecef7ecefea40d46612f12138b36/websockets-13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6548f29b0e401eea2b967b2fdc1c7c7b5ebb3eeb470ed23a54cd45ef078a0db9", size = 164438 }, - { url = "https://files.pythonhosted.org/packages/b6/08/6f38b8e625b3d93de731f1d248cc1493327f16cb45b9645b3e791782cff0/websockets-13.1-cp311-cp311-win32.whl", hash = "sha256:c11d4d16e133f6df8916cc5b7e3e96ee4c44c936717d684a94f48f82edb7c92f", size = 158710 }, - { url = "https://files.pythonhosted.org/packages/fb/39/ec8832ecb9bb04a8d318149005ed8cee0ba4e0205835da99e0aa497a091f/websockets-13.1-cp311-cp311-win_amd64.whl", hash = "sha256:d04f13a1d75cb2b8382bdc16ae6fa58c97337253826dfe136195b7f89f661557", size = 159137 }, - { url = "https://files.pythonhosted.org/packages/df/46/c426282f543b3c0296cf964aa5a7bb17e984f58dde23460c3d39b3148fcf/websockets-13.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9d75baf00138f80b48f1eac72ad1535aac0b6461265a0bcad391fc5aba875cfc", size = 157821 }, - { url = "https://files.pythonhosted.org/packages/aa/85/22529867010baac258da7c45848f9415e6cf37fef00a43856627806ffd04/websockets-13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9b6f347deb3dcfbfde1c20baa21c2ac0751afaa73e64e5b693bb2b848efeaa49", size = 155480 }, - { url = "https://files.pythonhosted.org/packages/29/2c/bdb339bfbde0119a6e84af43ebf6275278698a2241c2719afc0d8b0bdbf2/websockets-13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de58647e3f9c42f13f90ac7e5f58900c80a39019848c5547bc691693098ae1bd", size = 155715 }, - { url = "https://files.pythonhosted.org/packages/9f/d0/8612029ea04c5c22bf7af2fd3d63876c4eaeef9b97e86c11972a43aa0e6c/websockets-13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1b54689e38d1279a51d11e3467dd2f3a50f5f2e879012ce8f2d6943f00e83f0", size = 165647 }, - { url = "https://files.pythonhosted.org/packages/56/04/1681ed516fa19ca9083f26d3f3a302257e0911ba75009533ed60fbb7b8d1/websockets-13.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf1781ef73c073e6b0f90af841aaf98501f975d306bbf6221683dd594ccc52b6", size = 164592 }, - { url = "https://files.pythonhosted.org/packages/38/6f/a96417a49c0ed132bb6087e8e39a37db851c70974f5c724a4b2a70066996/websockets-13.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d23b88b9388ed85c6faf0e74d8dec4f4d3baf3ecf20a65a47b836d56260d4b9", size = 165012 }, - { url = "https://files.pythonhosted.org/packages/40/8b/fccf294919a1b37d190e86042e1a907b8f66cff2b61e9befdbce03783e25/websockets-13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3c78383585f47ccb0fcf186dcb8a43f5438bd7d8f47d69e0b56f71bf431a0a68", size = 165311 }, - { url = "https://files.pythonhosted.org/packages/c1/61/f8615cf7ce5fe538476ab6b4defff52beb7262ff8a73d5ef386322d9761d/websockets-13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d6d300f8ec35c24025ceb9b9019ae9040c1ab2f01cddc2bcc0b518af31c75c14", size = 164692 }, - { url = "https://files.pythonhosted.org/packages/5c/f1/a29dd6046d3a722d26f182b783a7997d25298873a14028c4760347974ea3/websockets-13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a9dcaf8b0cc72a392760bb8755922c03e17a5a54e08cca58e8b74f6902b433cf", size = 164686 }, - { url = "https://files.pythonhosted.org/packages/0f/99/ab1cdb282f7e595391226f03f9b498f52109d25a2ba03832e21614967dfa/websockets-13.1-cp312-cp312-win32.whl", hash = "sha256:2f85cf4f2a1ba8f602298a853cec8526c2ca42a9a4b947ec236eaedb8f2dc80c", size = 158712 }, - { url = "https://files.pythonhosted.org/packages/46/93/e19160db48b5581feac8468330aa11b7292880a94a37d7030478596cc14e/websockets-13.1-cp312-cp312-win_amd64.whl", hash = "sha256:38377f8b0cdeee97c552d20cf1865695fcd56aba155ad1b4ca8779a5b6ef4ac3", size = 159145 }, - { url = "https://files.pythonhosted.org/packages/51/20/2b99ca918e1cbd33c53db2cace5f0c0cd8296fc77558e1908799c712e1cd/websockets-13.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a9ab1e71d3d2e54a0aa646ab6d4eebfaa5f416fe78dfe4da2839525dc5d765c6", size = 157828 }, - { url = "https://files.pythonhosted.org/packages/b8/47/0932a71d3d9c0e9483174f60713c84cee58d62839a143f21a2bcdbd2d205/websockets-13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b9d7439d7fab4dce00570bb906875734df13d9faa4b48e261c440a5fec6d9708", size = 155487 }, - { url = "https://files.pythonhosted.org/packages/a9/60/f1711eb59ac7a6c5e98e5637fef5302f45b6f76a2c9d64fd83bbb341377a/websockets-13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:327b74e915cf13c5931334c61e1a41040e365d380f812513a255aa804b183418", size = 155721 }, - { url = "https://files.pythonhosted.org/packages/6a/e6/ba9a8db7f9d9b0e5f829cf626ff32677f39824968317223605a6b419d445/websockets-13.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:325b1ccdbf5e5725fdcb1b0e9ad4d2545056479d0eee392c291c1bf76206435a", size = 165609 }, - { url = "https://files.pythonhosted.org/packages/c1/22/4ec80f1b9c27a0aebd84ccd857252eda8418ab9681eb571b37ca4c5e1305/websockets-13.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:346bee67a65f189e0e33f520f253d5147ab76ae42493804319b5716e46dddf0f", size = 164556 }, - { url = "https://files.pythonhosted.org/packages/27/ac/35f423cb6bb15600438db80755609d27eda36d4c0b3c9d745ea12766c45e/websockets-13.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91a0fa841646320ec0d3accdff5b757b06e2e5c86ba32af2e0815c96c7a603c5", size = 164993 }, - { url = "https://files.pythonhosted.org/packages/31/4e/98db4fd267f8be9e52e86b6ee4e9aa7c42b83452ea0ea0672f176224b977/websockets-13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:18503d2c5f3943e93819238bf20df71982d193f73dcecd26c94514f417f6b135", size = 165360 }, - { url = "https://files.pythonhosted.org/packages/3f/15/3f0de7cda70ffc94b7e7024544072bc5b26e2c1eb36545291abb755d8cdb/websockets-13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9cd1af7e18e5221d2878378fbc287a14cd527fdd5939ed56a18df8a31136bb2", size = 164745 }, - { url = "https://files.pythonhosted.org/packages/a1/6e/66b6b756aebbd680b934c8bdbb6dcb9ce45aad72cde5f8a7208dbb00dd36/websockets-13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:70c5be9f416aa72aab7a2a76c90ae0a4fe2755c1816c153c1a2bcc3333ce4ce6", size = 164732 }, - { url = "https://files.pythonhosted.org/packages/35/c6/12e3aab52c11aeb289e3dbbc05929e7a9d90d7a9173958477d3ef4f8ce2d/websockets-13.1-cp313-cp313-win32.whl", hash = "sha256:624459daabeb310d3815b276c1adef475b3e6804abaf2d9d2c061c319f7f187d", size = 158709 }, - { url = "https://files.pythonhosted.org/packages/41/d8/63d6194aae711d7263df4498200c690a9c39fb437ede10f3e157a6343e0d/websockets-13.1-cp313-cp313-win_amd64.whl", hash = "sha256:c518e84bb59c2baae725accd355c8dc517b4a3ed8db88b4bc93c78dae2974bf2", size = 159144 }, - { url = "https://files.pythonhosted.org/packages/61/26/5f7a7fb03efedb4f90ed61968338bfe7c389863b0ceda239b94ae61c5ae4/websockets-13.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9b37c184f8b976f0c0a231a5f3d6efe10807d41ccbe4488df8c74174805eea7d", size = 157810 }, - { url = "https://files.pythonhosted.org/packages/0e/d4/9b4814a07dffaa7a79d71b4944d10836f9adbd527a113f6675734ef3abed/websockets-13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:163e7277e1a0bd9fb3c8842a71661ad19c6aa7bb3d6678dc7f89b17fbcc4aeb7", size = 155467 }, - { url = "https://files.pythonhosted.org/packages/1a/1a/2abdc7ce3b56429ae39d6bfb48d8c791f5a26bbcb6f44aabcf71ffc3fda2/websockets-13.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4b889dbd1342820cc210ba44307cf75ae5f2f96226c0038094455a96e64fb07a", size = 155714 }, - { url = "https://files.pythonhosted.org/packages/2a/98/189d7cf232753a719b2726ec55e7922522632248d5d830adf078e3f612be/websockets-13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:586a356928692c1fed0eca68b4d1c2cbbd1ca2acf2ac7e7ebd3b9052582deefa", size = 164587 }, - { url = "https://files.pythonhosted.org/packages/a5/2b/fb77cedf3f9f55ef8605238c801eef6b9a5269b01a396875a86896aea3a6/websockets-13.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7bd6abf1e070a6b72bfeb71049d6ad286852e285f146682bf30d0296f5fbadfa", size = 163588 }, - { url = "https://files.pythonhosted.org/packages/a3/b7/070481b83d2d5ac0f19233d9f364294e224e6478b0762f07fa7f060e0619/websockets-13.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2aad13a200e5934f5a6767492fb07151e1de1d6079c003ab31e1823733ae79", size = 163894 }, - { url = "https://files.pythonhosted.org/packages/eb/be/d6e1cff7d441cfe5eafaacc5935463e5f14c8b1c0d39cb8afde82709b55a/websockets-13.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:df01aea34b6e9e33572c35cd16bae5a47785e7d5c8cb2b54b2acdb9678315a17", size = 164315 }, - { url = "https://files.pythonhosted.org/packages/8b/5e/ffa234473e46ab2d3f9fd9858163d5db3ecea1439e4cb52966d78906424b/websockets-13.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e54affdeb21026329fb0744ad187cf812f7d3c2aa702a5edb562b325191fcab6", size = 163714 }, - { url = "https://files.pythonhosted.org/packages/cc/92/cea9eb9d381ca57065a5eb4ec2ce7a291bd96c85ce742915c3c9ffc1069f/websockets-13.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9ef8aa8bdbac47f4968a5d66462a2a0935d044bf35c0e5a8af152d58516dbeb5", size = 163673 }, - { url = "https://files.pythonhosted.org/packages/a4/f1/279104fff239bfd04c12b1e58afea227d72fd1acf431e3eed3f6ac2c96d2/websockets-13.1-cp39-cp39-win32.whl", hash = "sha256:deeb929efe52bed518f6eb2ddc00cc496366a14c726005726ad62c2dd9017a3c", size = 158702 }, - { url = "https://files.pythonhosted.org/packages/25/0b/b87370ff141375c41f7dd67941728e4b3682ebb45882591516c792a2ebee/websockets-13.1-cp39-cp39-win_amd64.whl", hash = "sha256:7c65ffa900e7cc958cd088b9a9157a8141c991f8c53d11087e6fb7277a03f81d", size = 159146 }, - { url = "https://files.pythonhosted.org/packages/2d/75/6da22cb3ad5b8c606963f9a5f9f88656256fecc29d420b4b2bf9e0c7d56f/websockets-13.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5dd6da9bec02735931fccec99d97c29f47cc61f644264eb995ad6c0c27667238", size = 155499 }, - { url = "https://files.pythonhosted.org/packages/c0/ba/22833d58629088fcb2ccccedfae725ac0bbcd713319629e97125b52ac681/websockets-13.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:2510c09d8e8df777177ee3d40cd35450dc169a81e747455cc4197e63f7e7bfe5", size = 155737 }, - { url = "https://files.pythonhosted.org/packages/95/54/61684fe22bdb831e9e1843d972adadf359cf04ab8613285282baea6a24bb/websockets-13.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1c3cf67185543730888b20682fb186fc8d0fa6f07ccc3ef4390831ab4b388d9", size = 157095 }, - { url = "https://files.pythonhosted.org/packages/fc/f5/6652fb82440813822022a9301a30afde85e5ff3fb2aebb77f34aabe2b4e8/websockets-13.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcc03c8b72267e97b49149e4863d57c2d77f13fae12066622dc78fe322490fe6", size = 156701 }, - { url = "https://files.pythonhosted.org/packages/67/33/ae82a7b860fa8a08aba68818bdf7ff61f04598aa5ab96df4cd5a3e418ca4/websockets-13.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:004280a140f220c812e65f36944a9ca92d766b6cc4560be652a0a3883a79ed8a", size = 156654 }, - { url = "https://files.pythonhosted.org/packages/63/0b/a1b528d36934f833e20f6da1032b995bf093d55cb416b9f2266f229fb237/websockets-13.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e2620453c075abeb0daa949a292e19f56de518988e079c36478bacf9546ced23", size = 159192 }, - { url = "https://files.pythonhosted.org/packages/59/fd/e4bf9a7159dba6a16c59ae9e670e3e8ad9dcb6791bc0599eb86de32d50a9/websockets-13.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:25c35bf84bf7c7369d247f0b8cfa157f989862c49104c5cf85cb5436a641d93e", size = 155499 }, - { url = "https://files.pythonhosted.org/packages/74/42/d48ede93cfe0c343f3b552af08efc60778d234989227b16882eed1b8b189/websockets-13.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:83f91d8a9bb404b8c2c41a707ac7f7f75b9442a0a876df295de27251a856ad09", size = 155731 }, - { url = "https://files.pythonhosted.org/packages/f6/f2/2ef6bff1c90a43b80622a17c0852b48c09d3954ab169266ad7b15e17cdcb/websockets-13.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a43cfdcddd07f4ca2b1afb459824dd3c6d53a51410636a2c7fc97b9a8cf4842", size = 157093 }, - { url = "https://files.pythonhosted.org/packages/d1/14/6f20bbaeeb350f155edf599aad949c554216f90e5d4ae7373d1f2e5931fb/websockets-13.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48a2ef1381632a2f0cb4efeff34efa97901c9fbc118e01951ad7cfc10601a9bb", size = 156701 }, - { url = "https://files.pythonhosted.org/packages/c7/86/38279dfefecd035e22b79c38722d4f87c4b6196f1556b7a631d0a3095ca7/websockets-13.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:459bf774c754c35dbb487360b12c5727adab887f1622b8aed5755880a21c4a20", size = 156649 }, - { url = "https://files.pythonhosted.org/packages/f6/c5/12c6859a2eaa8c53f59a647617a27f1835a226cd7106c601067c53251d98/websockets-13.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:95858ca14a9f6fa8413d29e0a585b31b278388aa775b8a81fa24830123874678", size = 159187 }, - { url = "https://files.pythonhosted.org/packages/56/27/96a5cd2626d11c8280656c6c71d8ab50fe006490ef9971ccd154e0c42cd2/websockets-13.1-py3-none-any.whl", hash = "sha256:a9a396a6ad26130cdae92ae10c36af09d9bfe6cafe69670fd3b6da9b07b4044f", size = 152134 }, +sdist = { url = "https://files.pythonhosted.org/packages/e2/73/9223dbc7be3dcaf2a7bbf756c351ec8da04b1fa573edaf545b95f6b0c7fd/websockets-13.1.tar.gz", hash = "sha256:a3b3366087c1bc0a2795111edcadddb8b3b59509d5db5d7ea3fdd69f954a8878", size = 158549, upload-time = "2024-09-21T17:34:21.54Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/94/d15dbfc6a5eb636dbc754303fba18208f2e88cf97e733e1d64fb9cb5c89e/websockets-13.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f48c749857f8fb598fb890a75f540e3221d0976ed0bf879cf3c7eef34151acee", size = 157815, upload-time = "2024-09-21T17:32:27.107Z" }, + { url = "https://files.pythonhosted.org/packages/30/02/c04af33f4663945a26f5e8cf561eb140c35452b50af47a83c3fbcfe62ae1/websockets-13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c7e72ce6bda6fb9409cc1e8164dd41d7c91466fb599eb047cfda72fe758a34a7", size = 155466, upload-time = "2024-09-21T17:32:28.428Z" }, + { url = "https://files.pythonhosted.org/packages/35/e8/719f08d12303ea643655e52d9e9851b2dadbb1991d4926d9ce8862efa2f5/websockets-13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f779498eeec470295a2b1a5d97aa1bc9814ecd25e1eb637bd9d1c73a327387f6", size = 155716, upload-time = "2024-09-21T17:32:29.905Z" }, + { url = "https://files.pythonhosted.org/packages/91/e1/14963ae0252a8925f7434065d25dcd4701d5e281a0b4b460a3b5963d2594/websockets-13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676df3fe46956fbb0437d8800cd5f2b6d41143b6e7e842e60554398432cf29b", size = 164806, upload-time = "2024-09-21T17:32:31.384Z" }, + { url = "https://files.pythonhosted.org/packages/ec/fa/ab28441bae5e682a0f7ddf3d03440c0c352f930da419301f4a717f675ef3/websockets-13.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7affedeb43a70351bb811dadf49493c9cfd1ed94c9c70095fd177e9cc1541fa", size = 163810, upload-time = "2024-09-21T17:32:32.384Z" }, + { url = "https://files.pythonhosted.org/packages/44/77/dea187bd9d16d4b91566a2832be31f99a40d0f5bfa55eeb638eb2c3bc33d/websockets-13.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1971e62d2caa443e57588e1d82d15f663b29ff9dfe7446d9964a4b6f12c1e700", size = 164125, upload-time = "2024-09-21T17:32:33.398Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d9/3af14544e83f1437eb684b399e6ba0fa769438e869bf5d83d74bc197fae8/websockets-13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5f2e75431f8dc4a47f31565a6e1355fb4f2ecaa99d6b89737527ea917066e26c", size = 164532, upload-time = "2024-09-21T17:32:35.109Z" }, + { url = "https://files.pythonhosted.org/packages/1c/8a/6d332eabe7d59dfefe4b8ba6f46c8c5fabb15b71c8a8bc3d2b65de19a7b6/websockets-13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:58cf7e75dbf7e566088b07e36ea2e3e2bd5676e22216e4cad108d4df4a7402a0", size = 163948, upload-time = "2024-09-21T17:32:36.214Z" }, + { url = "https://files.pythonhosted.org/packages/1a/91/a0aeadbaf3017467a1ee03f8fb67accdae233fe2d5ad4b038c0a84e357b0/websockets-13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c90d6dec6be2c7d03378a574de87af9b1efea77d0c52a8301dd831ece938452f", size = 163898, upload-time = "2024-09-21T17:32:37.277Z" }, + { url = "https://files.pythonhosted.org/packages/71/31/a90fb47c63e0ae605be914b0b969d7c6e6ffe2038cd744798e4b3fbce53b/websockets-13.1-cp310-cp310-win32.whl", hash = "sha256:730f42125ccb14602f455155084f978bd9e8e57e89b569b4d7f0f0c17a448ffe", size = 158706, upload-time = "2024-09-21T17:32:38.755Z" }, + { url = "https://files.pythonhosted.org/packages/93/ca/9540a9ba80da04dc7f36d790c30cae4252589dbd52ccdc92e75b0be22437/websockets-13.1-cp310-cp310-win_amd64.whl", hash = "sha256:5993260f483d05a9737073be197371940c01b257cc45ae3f1d5d7adb371b266a", size = 159141, upload-time = "2024-09-21T17:32:40.495Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f0/cf0b8a30d86b49e267ac84addbebbc7a48a6e7bb7c19db80f62411452311/websockets-13.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:61fc0dfcda609cda0fc9fe7977694c0c59cf9d749fbb17f4e9483929e3c48a19", size = 157813, upload-time = "2024-09-21T17:32:42.188Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e7/22285852502e33071a8cf0ac814f8988480ec6db4754e067b8b9d0e92498/websockets-13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ceec59f59d092c5007e815def4ebb80c2de330e9588e101cf8bd94c143ec78a5", size = 155469, upload-time = "2024-09-21T17:32:43.858Z" }, + { url = "https://files.pythonhosted.org/packages/68/d4/c8c7c1e5b40ee03c5cc235955b0fb1ec90e7e37685a5f69229ad4708dcde/websockets-13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1dca61c6db1166c48b95198c0b7d9c990b30c756fc2923cc66f68d17dc558fd", size = 155717, upload-time = "2024-09-21T17:32:44.914Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/c50999b9b848b1332b07c7fd8886179ac395cb766fda62725d1539e7bc6c/websockets-13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:308e20f22c2c77f3f39caca508e765f8725020b84aa963474e18c59accbf4c02", size = 165379, upload-time = "2024-09-21T17:32:45.933Z" }, + { url = "https://files.pythonhosted.org/packages/bc/49/4a4ad8c072f18fd79ab127650e47b160571aacfc30b110ee305ba25fffc9/websockets-13.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d516c325e6540e8a57b94abefc3459d7dab8ce52ac75c96cad5549e187e3a7", size = 164376, upload-time = "2024-09-21T17:32:46.987Z" }, + { url = "https://files.pythonhosted.org/packages/af/9b/8c06d425a1d5a74fd764dd793edd02be18cf6fc3b1ccd1f29244ba132dc0/websockets-13.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c6e35319b46b99e168eb98472d6c7d8634ee37750d7693656dc766395df096", size = 164753, upload-time = "2024-09-21T17:32:48.046Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5b/0acb5815095ff800b579ffc38b13ab1b915b317915023748812d24e0c1ac/websockets-13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5f9fee94ebafbc3117c30be1844ed01a3b177bb6e39088bc6b2fa1dc15572084", size = 165051, upload-time = "2024-09-21T17:32:49.271Z" }, + { url = "https://files.pythonhosted.org/packages/30/93/c3891c20114eacb1af09dedfcc620c65c397f4fd80a7009cd12d9457f7f5/websockets-13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7c1e90228c2f5cdde263253fa5db63e6653f1c00e7ec64108065a0b9713fa1b3", size = 164489, upload-time = "2024-09-21T17:32:50.392Z" }, + { url = "https://files.pythonhosted.org/packages/28/09/af9e19885539759efa2e2cd29b8b3f9eecef7ecefea40d46612f12138b36/websockets-13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6548f29b0e401eea2b967b2fdc1c7c7b5ebb3eeb470ed23a54cd45ef078a0db9", size = 164438, upload-time = "2024-09-21T17:32:52.223Z" }, + { url = "https://files.pythonhosted.org/packages/b6/08/6f38b8e625b3d93de731f1d248cc1493327f16cb45b9645b3e791782cff0/websockets-13.1-cp311-cp311-win32.whl", hash = "sha256:c11d4d16e133f6df8916cc5b7e3e96ee4c44c936717d684a94f48f82edb7c92f", size = 158710, upload-time = "2024-09-21T17:32:53.244Z" }, + { url = "https://files.pythonhosted.org/packages/fb/39/ec8832ecb9bb04a8d318149005ed8cee0ba4e0205835da99e0aa497a091f/websockets-13.1-cp311-cp311-win_amd64.whl", hash = "sha256:d04f13a1d75cb2b8382bdc16ae6fa58c97337253826dfe136195b7f89f661557", size = 159137, upload-time = "2024-09-21T17:32:54.721Z" }, + { url = "https://files.pythonhosted.org/packages/df/46/c426282f543b3c0296cf964aa5a7bb17e984f58dde23460c3d39b3148fcf/websockets-13.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9d75baf00138f80b48f1eac72ad1535aac0b6461265a0bcad391fc5aba875cfc", size = 157821, upload-time = "2024-09-21T17:32:56.442Z" }, + { url = "https://files.pythonhosted.org/packages/aa/85/22529867010baac258da7c45848f9415e6cf37fef00a43856627806ffd04/websockets-13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9b6f347deb3dcfbfde1c20baa21c2ac0751afaa73e64e5b693bb2b848efeaa49", size = 155480, upload-time = "2024-09-21T17:32:57.698Z" }, + { url = "https://files.pythonhosted.org/packages/29/2c/bdb339bfbde0119a6e84af43ebf6275278698a2241c2719afc0d8b0bdbf2/websockets-13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de58647e3f9c42f13f90ac7e5f58900c80a39019848c5547bc691693098ae1bd", size = 155715, upload-time = "2024-09-21T17:32:59.429Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/8612029ea04c5c22bf7af2fd3d63876c4eaeef9b97e86c11972a43aa0e6c/websockets-13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1b54689e38d1279a51d11e3467dd2f3a50f5f2e879012ce8f2d6943f00e83f0", size = 165647, upload-time = "2024-09-21T17:33:00.495Z" }, + { url = "https://files.pythonhosted.org/packages/56/04/1681ed516fa19ca9083f26d3f3a302257e0911ba75009533ed60fbb7b8d1/websockets-13.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf1781ef73c073e6b0f90af841aaf98501f975d306bbf6221683dd594ccc52b6", size = 164592, upload-time = "2024-09-21T17:33:02.223Z" }, + { url = "https://files.pythonhosted.org/packages/38/6f/a96417a49c0ed132bb6087e8e39a37db851c70974f5c724a4b2a70066996/websockets-13.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d23b88b9388ed85c6faf0e74d8dec4f4d3baf3ecf20a65a47b836d56260d4b9", size = 165012, upload-time = "2024-09-21T17:33:03.288Z" }, + { url = "https://files.pythonhosted.org/packages/40/8b/fccf294919a1b37d190e86042e1a907b8f66cff2b61e9befdbce03783e25/websockets-13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3c78383585f47ccb0fcf186dcb8a43f5438bd7d8f47d69e0b56f71bf431a0a68", size = 165311, upload-time = "2024-09-21T17:33:04.728Z" }, + { url = "https://files.pythonhosted.org/packages/c1/61/f8615cf7ce5fe538476ab6b4defff52beb7262ff8a73d5ef386322d9761d/websockets-13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d6d300f8ec35c24025ceb9b9019ae9040c1ab2f01cddc2bcc0b518af31c75c14", size = 164692, upload-time = "2024-09-21T17:33:05.829Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f1/a29dd6046d3a722d26f182b783a7997d25298873a14028c4760347974ea3/websockets-13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a9dcaf8b0cc72a392760bb8755922c03e17a5a54e08cca58e8b74f6902b433cf", size = 164686, upload-time = "2024-09-21T17:33:06.823Z" }, + { url = "https://files.pythonhosted.org/packages/0f/99/ab1cdb282f7e595391226f03f9b498f52109d25a2ba03832e21614967dfa/websockets-13.1-cp312-cp312-win32.whl", hash = "sha256:2f85cf4f2a1ba8f602298a853cec8526c2ca42a9a4b947ec236eaedb8f2dc80c", size = 158712, upload-time = "2024-09-21T17:33:07.877Z" }, + { url = "https://files.pythonhosted.org/packages/46/93/e19160db48b5581feac8468330aa11b7292880a94a37d7030478596cc14e/websockets-13.1-cp312-cp312-win_amd64.whl", hash = "sha256:38377f8b0cdeee97c552d20cf1865695fcd56aba155ad1b4ca8779a5b6ef4ac3", size = 159145, upload-time = "2024-09-21T17:33:09.202Z" }, + { url = "https://files.pythonhosted.org/packages/51/20/2b99ca918e1cbd33c53db2cace5f0c0cd8296fc77558e1908799c712e1cd/websockets-13.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a9ab1e71d3d2e54a0aa646ab6d4eebfaa5f416fe78dfe4da2839525dc5d765c6", size = 157828, upload-time = "2024-09-21T17:33:10.987Z" }, + { url = "https://files.pythonhosted.org/packages/b8/47/0932a71d3d9c0e9483174f60713c84cee58d62839a143f21a2bcdbd2d205/websockets-13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b9d7439d7fab4dce00570bb906875734df13d9faa4b48e261c440a5fec6d9708", size = 155487, upload-time = "2024-09-21T17:33:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/a9/60/f1711eb59ac7a6c5e98e5637fef5302f45b6f76a2c9d64fd83bbb341377a/websockets-13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:327b74e915cf13c5931334c61e1a41040e365d380f812513a255aa804b183418", size = 155721, upload-time = "2024-09-21T17:33:13.909Z" }, + { url = "https://files.pythonhosted.org/packages/6a/e6/ba9a8db7f9d9b0e5f829cf626ff32677f39824968317223605a6b419d445/websockets-13.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:325b1ccdbf5e5725fdcb1b0e9ad4d2545056479d0eee392c291c1bf76206435a", size = 165609, upload-time = "2024-09-21T17:33:14.967Z" }, + { url = "https://files.pythonhosted.org/packages/c1/22/4ec80f1b9c27a0aebd84ccd857252eda8418ab9681eb571b37ca4c5e1305/websockets-13.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:346bee67a65f189e0e33f520f253d5147ab76ae42493804319b5716e46dddf0f", size = 164556, upload-time = "2024-09-21T17:33:17.113Z" }, + { url = "https://files.pythonhosted.org/packages/27/ac/35f423cb6bb15600438db80755609d27eda36d4c0b3c9d745ea12766c45e/websockets-13.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91a0fa841646320ec0d3accdff5b757b06e2e5c86ba32af2e0815c96c7a603c5", size = 164993, upload-time = "2024-09-21T17:33:18.168Z" }, + { url = "https://files.pythonhosted.org/packages/31/4e/98db4fd267f8be9e52e86b6ee4e9aa7c42b83452ea0ea0672f176224b977/websockets-13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:18503d2c5f3943e93819238bf20df71982d193f73dcecd26c94514f417f6b135", size = 165360, upload-time = "2024-09-21T17:33:19.233Z" }, + { url = "https://files.pythonhosted.org/packages/3f/15/3f0de7cda70ffc94b7e7024544072bc5b26e2c1eb36545291abb755d8cdb/websockets-13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9cd1af7e18e5221d2878378fbc287a14cd527fdd5939ed56a18df8a31136bb2", size = 164745, upload-time = "2024-09-21T17:33:20.361Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6e/66b6b756aebbd680b934c8bdbb6dcb9ce45aad72cde5f8a7208dbb00dd36/websockets-13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:70c5be9f416aa72aab7a2a76c90ae0a4fe2755c1816c153c1a2bcc3333ce4ce6", size = 164732, upload-time = "2024-09-21T17:33:23.103Z" }, + { url = "https://files.pythonhosted.org/packages/35/c6/12e3aab52c11aeb289e3dbbc05929e7a9d90d7a9173958477d3ef4f8ce2d/websockets-13.1-cp313-cp313-win32.whl", hash = "sha256:624459daabeb310d3815b276c1adef475b3e6804abaf2d9d2c061c319f7f187d", size = 158709, upload-time = "2024-09-21T17:33:24.196Z" }, + { url = "https://files.pythonhosted.org/packages/41/d8/63d6194aae711d7263df4498200c690a9c39fb437ede10f3e157a6343e0d/websockets-13.1-cp313-cp313-win_amd64.whl", hash = "sha256:c518e84bb59c2baae725accd355c8dc517b4a3ed8db88b4bc93c78dae2974bf2", size = 159144, upload-time = "2024-09-21T17:33:25.96Z" }, + { url = "https://files.pythonhosted.org/packages/61/26/5f7a7fb03efedb4f90ed61968338bfe7c389863b0ceda239b94ae61c5ae4/websockets-13.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9b37c184f8b976f0c0a231a5f3d6efe10807d41ccbe4488df8c74174805eea7d", size = 157810, upload-time = "2024-09-21T17:33:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/0e/d4/9b4814a07dffaa7a79d71b4944d10836f9adbd527a113f6675734ef3abed/websockets-13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:163e7277e1a0bd9fb3c8842a71661ad19c6aa7bb3d6678dc7f89b17fbcc4aeb7", size = 155467, upload-time = "2024-09-21T17:33:42.075Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1a/2abdc7ce3b56429ae39d6bfb48d8c791f5a26bbcb6f44aabcf71ffc3fda2/websockets-13.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4b889dbd1342820cc210ba44307cf75ae5f2f96226c0038094455a96e64fb07a", size = 155714, upload-time = "2024-09-21T17:33:43.128Z" }, + { url = "https://files.pythonhosted.org/packages/2a/98/189d7cf232753a719b2726ec55e7922522632248d5d830adf078e3f612be/websockets-13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:586a356928692c1fed0eca68b4d1c2cbbd1ca2acf2ac7e7ebd3b9052582deefa", size = 164587, upload-time = "2024-09-21T17:33:44.27Z" }, + { url = "https://files.pythonhosted.org/packages/a5/2b/fb77cedf3f9f55ef8605238c801eef6b9a5269b01a396875a86896aea3a6/websockets-13.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7bd6abf1e070a6b72bfeb71049d6ad286852e285f146682bf30d0296f5fbadfa", size = 163588, upload-time = "2024-09-21T17:33:45.38Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b7/070481b83d2d5ac0f19233d9f364294e224e6478b0762f07fa7f060e0619/websockets-13.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2aad13a200e5934f5a6767492fb07151e1de1d6079c003ab31e1823733ae79", size = 163894, upload-time = "2024-09-21T17:33:46.651Z" }, + { url = "https://files.pythonhosted.org/packages/eb/be/d6e1cff7d441cfe5eafaacc5935463e5f14c8b1c0d39cb8afde82709b55a/websockets-13.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:df01aea34b6e9e33572c35cd16bae5a47785e7d5c8cb2b54b2acdb9678315a17", size = 164315, upload-time = "2024-09-21T17:33:48.432Z" }, + { url = "https://files.pythonhosted.org/packages/8b/5e/ffa234473e46ab2d3f9fd9858163d5db3ecea1439e4cb52966d78906424b/websockets-13.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e54affdeb21026329fb0744ad187cf812f7d3c2aa702a5edb562b325191fcab6", size = 163714, upload-time = "2024-09-21T17:33:49.548Z" }, + { url = "https://files.pythonhosted.org/packages/cc/92/cea9eb9d381ca57065a5eb4ec2ce7a291bd96c85ce742915c3c9ffc1069f/websockets-13.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9ef8aa8bdbac47f4968a5d66462a2a0935d044bf35c0e5a8af152d58516dbeb5", size = 163673, upload-time = "2024-09-21T17:33:51.056Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f1/279104fff239bfd04c12b1e58afea227d72fd1acf431e3eed3f6ac2c96d2/websockets-13.1-cp39-cp39-win32.whl", hash = "sha256:deeb929efe52bed518f6eb2ddc00cc496366a14c726005726ad62c2dd9017a3c", size = 158702, upload-time = "2024-09-21T17:33:52.584Z" }, + { url = "https://files.pythonhosted.org/packages/25/0b/b87370ff141375c41f7dd67941728e4b3682ebb45882591516c792a2ebee/websockets-13.1-cp39-cp39-win_amd64.whl", hash = "sha256:7c65ffa900e7cc958cd088b9a9157a8141c991f8c53d11087e6fb7277a03f81d", size = 159146, upload-time = "2024-09-21T17:33:53.781Z" }, + { url = "https://files.pythonhosted.org/packages/2d/75/6da22cb3ad5b8c606963f9a5f9f88656256fecc29d420b4b2bf9e0c7d56f/websockets-13.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5dd6da9bec02735931fccec99d97c29f47cc61f644264eb995ad6c0c27667238", size = 155499, upload-time = "2024-09-21T17:33:54.917Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ba/22833d58629088fcb2ccccedfae725ac0bbcd713319629e97125b52ac681/websockets-13.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:2510c09d8e8df777177ee3d40cd35450dc169a81e747455cc4197e63f7e7bfe5", size = 155737, upload-time = "2024-09-21T17:33:56.052Z" }, + { url = "https://files.pythonhosted.org/packages/95/54/61684fe22bdb831e9e1843d972adadf359cf04ab8613285282baea6a24bb/websockets-13.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1c3cf67185543730888b20682fb186fc8d0fa6f07ccc3ef4390831ab4b388d9", size = 157095, upload-time = "2024-09-21T17:33:57.21Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f5/6652fb82440813822022a9301a30afde85e5ff3fb2aebb77f34aabe2b4e8/websockets-13.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcc03c8b72267e97b49149e4863d57c2d77f13fae12066622dc78fe322490fe6", size = 156701, upload-time = "2024-09-21T17:33:59.061Z" }, + { url = "https://files.pythonhosted.org/packages/67/33/ae82a7b860fa8a08aba68818bdf7ff61f04598aa5ab96df4cd5a3e418ca4/websockets-13.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:004280a140f220c812e65f36944a9ca92d766b6cc4560be652a0a3883a79ed8a", size = 156654, upload-time = "2024-09-21T17:34:00.944Z" }, + { url = "https://files.pythonhosted.org/packages/63/0b/a1b528d36934f833e20f6da1032b995bf093d55cb416b9f2266f229fb237/websockets-13.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e2620453c075abeb0daa949a292e19f56de518988e079c36478bacf9546ced23", size = 159192, upload-time = "2024-09-21T17:34:02.656Z" }, + { url = "https://files.pythonhosted.org/packages/59/fd/e4bf9a7159dba6a16c59ae9e670e3e8ad9dcb6791bc0599eb86de32d50a9/websockets-13.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:25c35bf84bf7c7369d247f0b8cfa157f989862c49104c5cf85cb5436a641d93e", size = 155499, upload-time = "2024-09-21T17:34:11.3Z" }, + { url = "https://files.pythonhosted.org/packages/74/42/d48ede93cfe0c343f3b552af08efc60778d234989227b16882eed1b8b189/websockets-13.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:83f91d8a9bb404b8c2c41a707ac7f7f75b9442a0a876df295de27251a856ad09", size = 155731, upload-time = "2024-09-21T17:34:13.151Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f2/2ef6bff1c90a43b80622a17c0852b48c09d3954ab169266ad7b15e17cdcb/websockets-13.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a43cfdcddd07f4ca2b1afb459824dd3c6d53a51410636a2c7fc97b9a8cf4842", size = 157093, upload-time = "2024-09-21T17:34:14.52Z" }, + { url = "https://files.pythonhosted.org/packages/d1/14/6f20bbaeeb350f155edf599aad949c554216f90e5d4ae7373d1f2e5931fb/websockets-13.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48a2ef1381632a2f0cb4efeff34efa97901c9fbc118e01951ad7cfc10601a9bb", size = 156701, upload-time = "2024-09-21T17:34:15.692Z" }, + { url = "https://files.pythonhosted.org/packages/c7/86/38279dfefecd035e22b79c38722d4f87c4b6196f1556b7a631d0a3095ca7/websockets-13.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:459bf774c754c35dbb487360b12c5727adab887f1622b8aed5755880a21c4a20", size = 156649, upload-time = "2024-09-21T17:34:17.335Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c5/12c6859a2eaa8c53f59a647617a27f1835a226cd7106c601067c53251d98/websockets-13.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:95858ca14a9f6fa8413d29e0a585b31b278388aa775b8a81fa24830123874678", size = 159187, upload-time = "2024-09-21T17:34:18.538Z" }, + { url = "https://files.pythonhosted.org/packages/56/27/96a5cd2626d11c8280656c6c71d8ab50fe006490ef9971ccd154e0c42cd2/websockets-13.1-py3-none-any.whl", hash = "sha256:a9a396a6ad26130cdae92ae10c36af09d9bfe6cafe69670fd3b6da9b07b4044f", size = 152134, upload-time = "2024-09-21T17:34:19.904Z" }, ] [[package]] name = "zipp" version = "3.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/00/27/f0ac6b846684cecce1ee93d32450c45ab607f65c2e0255f0092032d91f07/zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b", size = 18454 } +sdist = { url = "https://files.pythonhosted.org/packages/00/27/f0ac6b846684cecce1ee93d32450c45ab607f65c2e0255f0092032d91f07/zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b", size = 18454, upload-time = "2023-02-25T02:17:22.503Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/fa/c9e82bbe1af6266adf08afb563905eb87cab83fde00a0a08963510621047/zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556", size = 6758 }, + { url = "https://files.pythonhosted.org/packages/5b/fa/c9e82bbe1af6266adf08afb563905eb87cab83fde00a0a08963510621047/zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556", size = 6758, upload-time = "2023-02-25T02:17:20.807Z" }, ]