diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..0d49de8 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,201 @@ +# xstate-python — Project Context for AI Agents + +## What this is + +A Python implementation of [XState](https://github.com/statelyai/xstate) — statecharts and +hierarchical state machines for Python, following the W3C SCXML execution algorithm. The goal is +a **solid, published Python statechart library** that tracks XState v5's architecture and owns +the niche of native XState JSON compatibility. + +**PyPI name:** `xstate` | **License:** MIT | **Python:** 3.9–3.13 + +--- + +## Architecture + +``` +xstate/ + __init__.py Public API: `from xstate import Machine` + machine.py Machine class — entry point, orchestrates a statechart definition + state_node.py StateNode — represents a single state in the hierarchy + state.py State — current snapshot: {value, configuration, context, actions} + algorithm.py SCXML execution engine (microstep/macrostep, entry/exit sets) + transition.py Transition — event, guard, target, actions + action.py Action — entry/exit/transition side effects + event.py Event — typed event wrapper + interpreter.py STUB — not yet implemented (planned for 0.3.0) + scxml.py SCXML XML → Machine config converter (requires `scxml` extra for js conds) +``` + +`algorithm.py` is the heart of the library. It implements the W3C SCXML algorithm: +`compute_entry_set`, `enter_states`, `exit_states`, `microstep`, `main_event_loop`. +The critical execution order is: `main_event_loop` → `microstep` → `main_event_loop2` +(which handles eventless transitions / `always` blocks after each macrostep). + +--- + +## Current state (0.2.0 in progress) + +**Working:** +- Hierarchical (compound) states +- Parallel states (`type: "parallel"`) — independent regions, broadcast events, + nested parallel, `onDone` when all regions reach a final state +- Entry / exit actions +- Guards / conditions (pure Python): `cond` as a callable `(context, event)` or a + string resolved from `Machine(config, guards={...})` +- Context + `assign` actions (`from xstate import assign`); event payloads reach + guards/assigners via `event.data` (pass `transition(state, {"type": ..., ...})`) +- Final states + `onDone` transitions +- History states — shallow and deep (`{"type": "history", "history": "deep"}`), + persisted across transitions via `State.history_value` +- SCXML XML import (requires `pip install xstate[scxml]` for JS-cond evaluation) + +**Stubbed / incomplete:** +- `interpreter.py` — no running event loop / event queue yet (0.3.0 target) +- Delayed transitions / `after` (0.3.0 target) +- Async support (0.5.0 target) +- `setup()` API + XState v5 handler signatures (0.4.0+ target) + +Handler-signature note: guards/assigners are invoked arity-aware (`()`, +`(context)`, or `(context, event)`) by `algorithm._invoke`. The XState v5 +single-object signature `({context, event})` is a 0.4.0 target. + +--- + +## Release roadmap + +| Version | Theme | Key deliverables | +|---------|-------|-----------------| +| 0.1.0 | Capture & stabilize | Engine bug fixes, drop Js2Py hard dep, modern packaging, PyPI publish | +| 0.2.0 | Solid v4 FSM | Guards/conditions (pure Python), deep history, context/assign, full parallel | +| 0.3.0 | Interpreter | Synchronous event loop + queue, delayed transitions (`after`/`cancel`) | +| 0.4.0 | v5 config alignment | Rename `cond`→`guard`, `data`→`output`, `always:`, single-object handler signatures, MachineSnapshot | +| 0.5.0 | Actor model | `create_actor`, actor system, `from_promise`/`from_callback`, asyncio | +| 0.6.0+ | Setup & parity | `setup()`, composable guards, persistence, XState JSON from Stately.ai | + +The differentiating niche: **XState / Stately.ai JSON compatibility** — neither `transitions` +nor `python-statemachine` accepts XState JSON natively. + +--- + +## Development commands + +```bash +# Install dev deps +poetry install + +# Run the test suite (primary target — no extras needed) +python3 -m pytest tests/ --ignore=tests/test_scxml.py + +# Run SCXML tests (needs test-framework submodule and the scxml extra) +git submodule update --init +pip install xstate[scxml] +python3 -m pytest tests/test_scxml.py + +# Type check +mypy xstate/ + +# Format + lint +black xstate/ tests/ +isort xstate/ tests/ +flake8 xstate/ tests/ +``` + +--- + +## Key constraints and guardrails + +### Never reintroduce Js2Py as a hard dependency + +`Js2Py` cannot build on Python 3.11+ and is a security anti-pattern for a library. +It has been moved to the optional `scxml` extra, lazy-imported inside `_eval_scxml_cond()`. +**Do not `import js2py` at module level anywhere in `xstate/`.** + +The long-term plan (0.2.0) is to replace the remaining Js2Py usage in `scxml.py` with +a pure-Python SCXML condition evaluator. + +### Algorithm changes require SCXML test verification + +`xstate/algorithm.py` implements the W3C SCXML algorithm. Any change to it must be +verified against the SCXML test framework (`test-framework/`). The reference is: +https://www.w3.org/TR/scxml/#AlgorithmforSCXMLInterpretation + +Key functions and their SCXML algorithm counterparts: +- `compute_entry_set` → `computeEntrySet` +- `add_descendent_states_to_enter` → `addDescendantStatesToEnter` +- `add_ancestor_states_to_enter` → `addAncestorStatesToEnter` +- `main_event_loop` / `main_event_loop2` → `mainEventLoop` / `macrostep` +- `get_transition_domain` → `getTransitionDomain` +- `get_proper_ancestors` → `getProperAncestors` + +### API surface (v0.1.0 public contract) + +```python +from xstate import Machine + +# Construct a machine from a Python dict (XState JSON shape) +machine = Machine(config: dict, actions: dict = {}) + +# Get the initial state +state = machine.initial_state # State(value, configuration, context, actions) + +# Transition +state = machine.transition(state, "EVENT_NAME") + +# State values +state.value # str for atomic states, dict for compound: {"parent": "child"} +``` + +**Do not break this interface in 0.1.x patches.** API changes go in 0.4.0 (v5 alignment). + +--- + +## Upstream remotes + +``` +origin → JovaniPink/xstate-python (your fork — push here) +upstream → statelyai/xstate-python (original; dormant after 2021) +``` + +Notable upstream branches: +- `upstream/master` — last upstream commit ~2021; use as historical reference only +- `upstream/davidkpiano/parallel+interrupt` — maintainer's engine bug fixes (already merged into 0.1.0) + +PR #49 on upstream (`auphofBSF/tests/extend_testing_21w38`) was evaluated and intentionally +**not** adopted. Reason: it welds the codebase to Js2Py (its headline feature is pasting +JavaScript config), contains Python-3-invalid `raise ""` bugs, and adds 8k lines +of v4-style code while the maintainer signalled the project should target v5. Its +**test corpus** (`test_history.py`, `test_state_in.py`) is the valuable artifact and should +be ported as behavior targets in 0.2.0. + +--- + +## Target alignment: XState v5 + +The JS library lives at https://github.com/statelyai/xstate (packages/core). + +Key v5 concepts for Python alignment: +- `createMachine` + `setup()` replace `Machine()` (0.6.0+) +- `createActor(logic)` replaces `interpret(machine)` (0.5.0) +- All handlers use single-object signature: `guard({"context": c, "event": e})` (0.4.0) +- `State` → `MachineSnapshot` with `{status, value, context, output, error}` (0.4.0) +- `cond` → `guard`, `data` → `output`, `on: {'': ...}` → `always:` (0.4.0) +- Actor system: every actor belongs to a system, accessible via `system.get(id)` (0.5.0) + +The algorithm core (SCXML microstep/macrostep) is shared between v4 and v5. Do not +replace `algorithm.py` — evolve it. + +--- + +## Comparison context + +| Library | Stars | Differentiator | +|---------|-------|----------------| +| `transitions` | ~6,500 | Most popular; broad but not SCXML-compliant | +| `python-statemachine` | ~1,200 | W3C SCXML test suite; strong statechart features | +| `xstate-statemachine` (basiltt) | ~14 | XState JSON compatible; active | +| `Sismic` | ~163 | Design by Contract; formal verification | +| `xstate` (this) | 194 | **XState JSON native; SCXML algorithm core** | + +Our bet: own native XState JSON compatibility + SCXML algorithm correctness. +Ship a `SKILL.md` with the library once 0.1.0 API is stable (agensi.io-compatible). diff --git a/poetry.lock b/poetry.lock index b5c10ca..0bd61cb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,84 +1,142 @@ -[[package]] -name = "appdirs" -version = "1.4.4" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "atomicwrites" -version = "1.4.0" -description = "Atomic file writes." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "attrs" -version = "21.2.0" -description = "Classes Without Boilerplate" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] -docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] +# This file is automatically @generated by Poetry 2.3.3 and should not be changed by hand. [[package]] name = "black" -version = "21.5b1" +version = "24.10.0" description = "The uncompromising code formatter." -category = "dev" optional = false -python-versions = ">=3.6.2" +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, + {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, + {file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"}, + {file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"}, + {file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"}, + {file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"}, + {file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"}, + {file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"}, + {file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"}, + {file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"}, + {file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"}, + {file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"}, + {file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"}, + {file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"}, + {file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"}, + {file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"}, + {file = "black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd"}, + {file = "black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f"}, + {file = "black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800"}, + {file = "black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7"}, + {file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"}, + {file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"}, +] [package.dependencies] -appdirs = "*" -click = ">=7.1.2" -dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""} +click = ">=8.0.0" mypy-extensions = ">=0.4.3" -pathspec = ">=0.8.1,<1" -regex = ">=2020.1.8" -toml = ">=0.10.1" -typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\""} -typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.6.0)", "aiohttp-cors"] -python2 = ["typed-ast (>=1.4.2)"] +d = ["aiohttp (>=3.10)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "click" version = "8.0.1" description = "Composable command line interface toolkit" -category = "dev" optional = false python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, + {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"}, +] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "colorama" version = "0.4.4" description = "Cross-platform colored terminal text." -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["dev"] +markers = "sys_platform == \"win32\" or platform_system == \"Windows\"" +files = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] [[package]] name = "coverage" version = "5.5" description = "Code coverage measurement for Python" -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +groups = ["dev"] +markers = "python_version < \"3.15\"" +files = [ + {file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"}, + {file = "coverage-5.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b"}, + {file = "coverage-5.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669"}, + {file = "coverage-5.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90"}, + {file = "coverage-5.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c"}, + {file = "coverage-5.5-cp27-cp27m-win32.whl", hash = "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a"}, + {file = "coverage-5.5-cp27-cp27m-win_amd64.whl", hash = "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81"}, + {file = "coverage-5.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6"}, + {file = "coverage-5.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0"}, + {file = "coverage-5.5-cp310-cp310-win_amd64.whl", hash = "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae"}, + {file = "coverage-5.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb"}, + {file = "coverage-5.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160"}, + {file = "coverage-5.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"}, + {file = "coverage-5.5-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701"}, + {file = "coverage-5.5-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793"}, + {file = "coverage-5.5-cp35-cp35m-win32.whl", hash = "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e"}, + {file = "coverage-5.5-cp35-cp35m-win_amd64.whl", hash = "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3"}, + {file = "coverage-5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066"}, + {file = "coverage-5.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a"}, + {file = "coverage-5.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465"}, + {file = "coverage-5.5-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb"}, + {file = "coverage-5.5-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821"}, + {file = "coverage-5.5-cp36-cp36m-win32.whl", hash = "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45"}, + {file = "coverage-5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184"}, + {file = "coverage-5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a"}, + {file = "coverage-5.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53"}, + {file = "coverage-5.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d"}, + {file = "coverage-5.5-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638"}, + {file = "coverage-5.5-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3"}, + {file = "coverage-5.5-cp37-cp37m-win32.whl", hash = "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a"}, + {file = "coverage-5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a"}, + {file = "coverage-5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6"}, + {file = "coverage-5.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2"}, + {file = "coverage-5.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759"}, + {file = "coverage-5.5-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873"}, + {file = "coverage-5.5-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a"}, + {file = "coverage-5.5-cp38-cp38-win32.whl", hash = "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6"}, + {file = "coverage-5.5-cp38-cp38-win_amd64.whl", hash = "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502"}, + {file = "coverage-5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b"}, + {file = "coverage-5.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529"}, + {file = "coverage-5.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b"}, + {file = "coverage-5.5-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff"}, + {file = "coverage-5.5-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b"}, + {file = "coverage-5.5-cp39-cp39-win32.whl", hash = "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6"}, + {file = "coverage-5.5-cp39-cp39-win_amd64.whl", hash = "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03"}, + {file = "coverage-5.5-pp36-none-any.whl", hash = "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079"}, + {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, + {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, +] [package.dependencies] toml = {version = "*", optional = true, markers = "extra == \"toml\""} @@ -87,567 +145,746 @@ toml = {version = "*", optional = true, markers = "extra == \"toml\""} toml = ["toml"] [[package]] -name = "dataclasses" -version = "0.8" -description = "A backport of the dataclasses module for Python 3.6" -category = "dev" +name = "coverage" +version = "7.14.1" +description = "Code coverage measurement for Python" optional = false -python-versions = ">=3.6, <3.7" +python-versions = ">=3.10" +groups = ["dev"] +markers = "python_version >= \"3.15\"" +files = [ + {file = "coverage-7.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3e3680291c4a1d0dadfa84a2c459576a4af5133abb617905714339a0c73138cf"}, + {file = "coverage-7.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a5274669f37f2343635a347b91a60777621341ab3378e9c6ac9335eee704bddf"}, + {file = "coverage-7.14.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cfe5a5fec635799ef33428f1e5e61bafa45a92a96190ba731561ba558ccc214d"}, + {file = "coverage-7.14.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:62a9f70b52e0b5a95cfef4a5c5641b06983cadc5e538a3feeb5c00211f523ac2"}, + {file = "coverage-7.14.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c18ebc343e15be53049b3a2dce38fe82d58f37e20ab9094b3a39c0aa4f6bb47"}, + {file = "coverage-7.14.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b84ffdf877644e7096aa936991efeed873f7f3df57b9cd001312b7668ab08550"}, + {file = "coverage-7.14.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e854312c4103f2ad4c0dc023b69b77ebfd2c89db5f86c4c94dc2353f9a92167e"}, + {file = "coverage-7.14.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c643734307300234fafa36bf2a040a7235f8f177ea1fd6ec1423aea6fb7b929f"}, + {file = "coverage-7.14.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:84ac9499e48700399a5dd0ea7085b5091961fec52c68d66b4ec0d3cf7f4441b1"}, + {file = "coverage-7.14.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:7f02d09f70776579b926d889a4c9c235070a1f47c40458aeaca563fae5acfdb5"}, + {file = "coverage-7.14.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ce66d8e46da2bb5ee313a745cbd2e391d319176c1f7a9451bfcd3a2fb920859b"}, + {file = "coverage-7.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c912c259304cfb5ee584481cfb7ce1ff932b4d61e6c9140b8f19cb7b5ed82332"}, + {file = "coverage-7.14.1-cp310-cp310-win32.whl", hash = "sha256:1238cb94638e610e972c60dac68e813f868dc7d6e982535270558443058d9d59"}, + {file = "coverage-7.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:fc459e5d73be2d6332fcfe8dbf3d8994671fe33c700f4565988ecfa511547253"}, + {file = "coverage-7.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:478b5bcd63c2e1357c5c7e16c070690df7b07f676b1c114d7b93e533c664309f"}, + {file = "coverage-7.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a24a81f9715ee42ef59a316cc11611c98fe23920f7c81861315c9f3ff4a230f4"}, + {file = "coverage-7.14.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:196a13319ad88d6d8ef5ab489ec4f44ddde2143c0c7d5b27786f6c3ffd56a7e1"}, + {file = "coverage-7.14.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3d452fd08b5c72c5167c93e6867b5c08500bd40f2a21e1e854a500550b6cc36f"}, + {file = "coverage-7.14.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23bf7fa51ac02e07fc7c96849b82946da47ae862dc8f86d183b2a4864fc38129"}, + {file = "coverage-7.14.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bcaa50684dcaadfa599ac48f81103c756d791cfd85c97203d2217c593d48b860"}, + {file = "coverage-7.14.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4ea1c034f95c9b056e856b794630b17f9fa3d57e4800ff1e503d3be0f9c9078c"}, + {file = "coverage-7.14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c7e057326434e441306226fbeb5d1aaf14a2637efe97ba668306635835f32ad7"}, + {file = "coverage-7.14.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:59baf88468dbc8d63b1887afd92bda52e40bb1561696e5819670601403810cec"}, + {file = "coverage-7.14.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d34d75f892b3ab73ba11cab5442cce7b3e168fd64162b16f0e1e0d09c508edef"}, + {file = "coverage-7.14.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3a56abc20a472baf0304c455721bc601477440d28ecfde8a03dde79ede07e0df"}, + {file = "coverage-7.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6a3cb83d1552c0cd1b4906655b6a33fd4a8473229633a901c6b73bf86914dee9"}, + {file = "coverage-7.14.1-cp311-cp311-win32.whl", hash = "sha256:10274a1fbeb8ec5d72966e17bb198a3104257aca4ac09d98667c5f8aca8c8548"}, + {file = "coverage-7.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:87ebdf787d4888e3f3f2d523eadc6e18c6d18c6d0eb173801a189641627fb37e"}, + {file = "coverage-7.14.1-cp311-cp311-win_arm64.whl", hash = "sha256:dd34767fa19848d35659ffc0a75314f58c7af3f1cd87ec521e8292a1238398a3"}, + {file = "coverage-7.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a06c76364a9360e33d6d23769aefdf7f66f38e2ffb60ceb1baaa4989d83b695c"}, + {file = "coverage-7.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fad54e871165f6ec2f536063ac74c3104508a12963e64072ba44bd822de52b0c"}, + {file = "coverage-7.14.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:84b535f00655ecafe1d929d1fb00ed5d6fa3051ea643ab2c161a3887b86f294b"}, + {file = "coverage-7.14.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6b6b0853b895fe0e98cbfc580d1ec3393d9302b4b1e96a77b3f5c91fdab899e6"}, + {file = "coverage-7.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:442cc9c952b2df400cda54bb04ab87330cf2cd08a8692cbbea36773531eb6f37"}, + {file = "coverage-7.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8270544c361ed405a27a060dbc9ed2c124b084d96dfdc2d9a2510482aef981ad"}, + {file = "coverage-7.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:48b283b1dd6372e8de2a7a9a4c4d5dc06f4d4fd209b876f3c88a7a205a0c8f84"}, + {file = "coverage-7.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5b0c99ba93a07d56f6df340bb79be53202a082b2fdb81bfe6190b741a3470d54"}, + {file = "coverage-7.14.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e471bc5769ff073b058cfadb0d736b56ce067c8560eabeb0da88462df98c23e7"}, + {file = "coverage-7.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f497a1ea81d4cd7c10ddcaa685135b9aabd291af3d55775a9ddf3cb7a364cdd9"}, + {file = "coverage-7.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2222be86d0b54f5dd5a38f45f17f315f737245e857bf0bdedc70734f84a13c02"}, + {file = "coverage-7.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:85e85586565842f6932abebd4c18bcb1074223dc0b3576e7d173ca710622813a"}, + {file = "coverage-7.14.1-cp312-cp312-win32.whl", hash = "sha256:4a28fd227808366b196a75476dced2eb35b351d6766ba9c858dc93319e87f4f1"}, + {file = "coverage-7.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:54acdb6674a4661768d7bf7db32dfb9f46ab1d764f8aba6df75ce1a6a088724e"}, + {file = "coverage-7.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:99cd41ff91afd94896fea3bc002706b6ae4ce95727d06e4a0f39c0a8d8bd8b1a"}, + {file = "coverage-7.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:be9f2c802dcfce3f71298303aa5dad0dce440a76c52f2f60dacd8656dab78793"}, + {file = "coverage-7.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6223a72fd0e4c7156353ec0f08a5f93623e1d3034d0e2683b9bb8ea674131b1d"}, + {file = "coverage-7.14.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7279d2110a28cebc738b6459ecda2771735a4c18465fbbd36b3288fe5ed92247"}, + {file = "coverage-7.14.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9eeb3fcbc13ba40dfbdb22d01d196a28e9cef9ed4c29b60061a1e0e823a9929d"}, + {file = "coverage-7.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f0cfc27c539f07cf5c0a4cfe211d0b6cae039f8f40526dbaa71944e64b50a7b"}, + {file = "coverage-7.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:221c70f316241a78e77e607c227cefc8808d4e08f28d99c04f35694690e940be"}, + {file = "coverage-7.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:da028256b04ec30e5e0114b6f76172938c313991f0a2d3d894271315cf5d5e43"}, + {file = "coverage-7.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76a085d7005236a767e3426148b2c407e53ad61695c562f8a81da2d373324901"}, + {file = "coverage-7.14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b553d04b5e778a8e56d57eb134aff42a92718ecba45e79c4764ecfa40efd92ff"}, + {file = "coverage-7.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:46f714d2fb8ae2f4f29f23ada7f1e79b759fff5a70f94a1dac23af204c3ec9e4"}, + {file = "coverage-7.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1896f5e19ff3f0431c7ce2172adc54890fd97f86b59ced8ca1649145d9ffe35d"}, + {file = "coverage-7.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:62fd185ef9df3c33d1c8178c5af105f762afbad96038de9a4ae100aa6297ca33"}, + {file = "coverage-7.14.1-cp313-cp313-win32.whl", hash = "sha256:ab4af6352741a604c431c6072fce5bee33bf0f20dc7a56618d6bf6bb89e9810c"}, + {file = "coverage-7.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:7af486dabe8954d03b087f0021540897afe084f04e16ff5579e08cc46f871416"}, + {file = "coverage-7.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:2224f89ffd0c5605ccce1ed7a584da162bc7c55f601ab1c946bc9de31a486b42"}, + {file = "coverage-7.14.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de286598cc65d2b489411174b1faec2f5a7775fb3201fd925db2a76b4030f37d"}, + {file = "coverage-7.14.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:042c46ded7c288aeb07cf14a28b6c1e10b78fcba40171c3fa1e939377eeef0b5"}, + {file = "coverage-7.14.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f4ddbe407477f04c45115d1a4e5bc480f753553b534d338d4c3358b1cdd0ea52"}, + {file = "coverage-7.14.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d13e6725992e2d2fd7d81d4f5241952d13740121dfd501da09201be39b2c003a"}, + {file = "coverage-7.14.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f747dc8edcfe740130f28f32f3995e955494285717e86ee25af51db2219df08a"}, + {file = "coverage-7.14.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced2f09ef276fd58611a1ef502164ad266d2b75174e5a40cabbdb4033f9f6cf2"}, + {file = "coverage-7.14.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b84800013769a78ccb9ef4659402e26d06867e337b61ec365f77ad008adea80e"}, + {file = "coverage-7.14.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ea8cd6ca0ee9f616aaef3afc6882e32c2cbf18b00d96313ffd76af650574034d"}, + {file = "coverage-7.14.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:aa5e304a873fabddc11e484e9b6b738bd38bd7bed17b09aa84eecf5332e8b8bb"}, + {file = "coverage-7.14.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5a1c5215be81035e629d5bc756650634d0bf31991038db7a0eccb90f025ce16d"}, + {file = "coverage-7.14.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:79058c47dae6788504b5effb319961bcd72d7240551464b91d474bc0ed186d69"}, + {file = "coverage-7.14.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:370c5afae3fa0658e11694a32b24c2778f6bc2d17718121f94ee185e69f26b54"}, + {file = "coverage-7.14.1-cp313-cp313t-win32.whl", hash = "sha256:3758dd0a7f1fa57365ef2e781df0f0731d38b6e3772259d13dae4bd8a958d4b1"}, + {file = "coverage-7.14.1-cp313-cp313t-win_amd64.whl", hash = "sha256:6ff665fb023a77386fe11685190cee1f60a7d635994a30d9b0a061533d470fce"}, + {file = "coverage-7.14.1-cp313-cp313t-win_arm64.whl", hash = "sha256:17a5a241e5997621a956a7f402a7433ef4221e5152809b785bec79e2323799f1"}, + {file = "coverage-7.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d5ed429d0b8edaac649e889b4ffcedb6c80b06629a3f93050e3dddfb99235bee"}, + {file = "coverage-7.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8011224a62280e50dab346960c03cf47aca1a1e09e608c0fb33fd6e0cc8e9500"}, + {file = "coverage-7.14.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:12c42ec1e14f553c4f817e989365982e646e27211f10a0f717855b94a79c8906"}, + {file = "coverage-7.14.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:06144cd511cf2624873a035c5069cf297144f6e77a73ee3d7a55b605ec5efb42"}, + {file = "coverage-7.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a311d8e1da24be5c1ccf85cbfb06315dbaa1703d5a1eab3f6432c72b837917c8"}, + {file = "coverage-7.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c79cead5b5bc584d9c71451cb984d0e3a84e0c0937379c8efcbf27c8d661b851"}, + {file = "coverage-7.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dcbf65f1f66a26cdd88c35cf68fb4729c5d1cd2e88added72420541dfb212034"}, + {file = "coverage-7.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fd86572566fb40189a8260446158235159bc7a82dfbc87a3b39cf4fb57fcec1c"}, + {file = "coverage-7.14.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:7771b601718fdde84832c3a434ca9bbf4ae9adbc49d84198b4110700c3c77c36"}, + {file = "coverage-7.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:39b21e212c55af06fa375e3dbf90a8a8e38792f3a910c580066d23563830ddd5"}, + {file = "coverage-7.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f2302660e32562a532b442480121aef8aa61a5bdb20b30bf0adab29f10a5a4b4"}, + {file = "coverage-7.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:03a6f93c1ec3b7f2e77b5dbcc5573a2c21f12529a5c6bbe0f16f72303cc2fa4d"}, + {file = "coverage-7.14.1-cp314-cp314-win32.whl", hash = "sha256:8a3ce026d73290f42f08dafecbd82c193a74df280461fbf97300fec51fd133ee"}, + {file = "coverage-7.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:114c95ef29302423b87d159075805f4ab973254a2638a5d7d046c94887cc87d7"}, + {file = "coverage-7.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:a07891c3f4805442b31b71e84ba3cf29ed1aa9a428284e06deeb4b23e5b46343"}, + {file = "coverage-7.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1101a5ebb083aecb625ebb6209d4105b58f647b093cb2dc8122d7b33f743cfe1"}, + {file = "coverage-7.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:851b9e1e4e8a4608e77c79714b2e77c0970d2ed7202a05e92ae407817481887b"}, + {file = "coverage-7.14.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d5b89cdfb2ee051b71e8c3c70bd81a9eff81100f736a269136fe1a68efe00474"}, + {file = "coverage-7.14.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0177614a0370f227888b4e436a7c55686d6a9f90eb1ade2b624ba685a1686e86"}, + {file = "coverage-7.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d69af5dea2de76fc485a83032a630523f985198b7e25be901ec60181587b01e"}, + {file = "coverage-7.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35ab22d91de736e8966b980dc355cbcdd2c6dbbcfe275f9a2991bc8a91b3df65"}, + {file = "coverage-7.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:357d4e32935c36588aaba057d734fa32428c360c9fc2e4442afbf1b646beee6e"}, + {file = "coverage-7.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:51bd64741cc6fa065abd300ede1afe5a5291ece9c31da8b24884deda48bcc3f8"}, + {file = "coverage-7.14.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9132cd363a68a4c3daa7c8704a654b1e39d3360f6f5b8ddd470608a945236c07"}, + {file = "coverage-7.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07c6290b1697b862c0478eab545eec949a0d0e4d6d03497f446d706da3b4f2de"}, + {file = "coverage-7.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5ea0c297e27133853b4d8a3eb799bff5a2dbd9f2f41537a240d337ac9b4df890"}, + {file = "coverage-7.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:01b7733daad0237daa01ef80fe2dfceffc911e6a17fa7b55d14aa8214eaaaecd"}, + {file = "coverage-7.14.1-cp314-cp314t-win32.whl", hash = "sha256:6adc5a36984624a70bf11d7184e20fa0a49aa7c47ffab43804106a1a695ea22e"}, + {file = "coverage-7.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:ddf799247318f34dbcd2efa8c95a8d0642674e926bb1774cf9b63dfd2a389d1c"}, + {file = "coverage-7.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:145986fe66647eb489f18d9a997567a3fd358584c4b5a808769113abc07466af"}, + {file = "coverage-7.14.1-py3-none-any.whl", hash = "sha256:a252f21c27e38347e60111a3266b03827422a7d5525951aceee313aa68bab1d2"}, + {file = "coverage-7.14.1.tar.gz", hash = "sha256:30c08f7d90415aa98b3c990385dea2939b0da55f38515e5b369b83655f8523be"}, +] + +[package.extras] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] -name = "flake8" -version = "3.9.2" -description = "the modular source code checker: pep8 pyflakes and co" -category = "dev" +name = "exceptiongroup" +version = "1.3.1" +description = "Backport of PEP 654 (exception groups)" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version < \"3.11\"" +files = [ + {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, + {file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"}, +] [package.dependencies] -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} -mccabe = ">=0.6.0,<0.7.0" -pycodestyle = ">=2.7.0,<2.8.0" -pyflakes = ">=2.3.0,<2.4.0" +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} + +[package.extras] +test = ["pytest (>=6)"] [[package]] -name = "importlib-metadata" -version = "4.3.1" -description = "Read metadata from Python packages" -category = "dev" +name = "flake8" +version = "7.3.0" +description = "the modular source code checker: pep8 pyflakes and co" optional = false -python-versions = ">=3.6" +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e"}, + {file = "flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872"}, +] [package.dependencies] -typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} -zipp = ">=0.5" - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.14.0,<2.15.0" +pyflakes = ">=3.4.0,<3.5.0" [[package]] name = "iniconfig" version = "1.1.1" description = "iniconfig: brain-dead simple config-ini parsing" -category = "dev" optional = false python-versions = "*" +groups = ["dev"] +files = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] [[package]] name = "isort" -version = "5.8.0" +version = "5.13.2" description = "A Python utility / library to sort Python imports." -category = "dev" optional = false -python-versions = ">=3.6,<4.0" +python-versions = ">=3.8.0" +groups = ["dev"] +files = [ + {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, + {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, +] [package.extras] -pipfile_deprecated_finder = ["pipreqs", "requirementslib"] -requirements_deprecated_finder = ["pipreqs", "pip-api"] -colors = ["colorama (>=0.4.3,<0.5.0)"] +colors = ["colorama (>=0.4.6)"] [[package]] name = "js2py" version = "0.71" description = "JavaScript to Python Translator & JavaScript interpreter written in 100% pure Python." -category = "main" -optional = false +optional = true python-versions = "*" +groups = ["main"] +markers = "extra == \"scxml\"" +files = [ + {file = "Js2Py-0.71-py3-none-any.whl", hash = "sha256:7f3dfa30c4b8e3ac3edd64c361c950a8c2587a30a86854aeb8c1ffb6f4dd24b4"}, + {file = "Js2Py-0.71.tar.gz", hash = "sha256:a41b1009dd1498ae7d436bfa5ac952a08ca92a4bb9e31dca6e8bb966b49f7fce"}, +] [package.dependencies] pyjsparser = ">=2.5.1" six = ">=1.10" tzlocal = ">=1.2" +[[package]] +name = "librt" +version = "0.11.0" +description = "Mypyc runtime library" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "platform_python_implementation != \"PyPy\"" +files = [ + {file = "librt-0.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6e94ebfcfa2d5e9926d6c3b9aa4617ffc42a845b4321fb84021b872358c82a0f"}, + {file = "librt-0.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ae627397a2f351560440d872d6f7c8dbb4072e57868e7b2fc5b8b430fe489d45"}, + {file = "librt-0.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc329359321b67d24efdf4bc69012b0597001649544db662c001db5a0184794c"}, + {file = "librt-0.11.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:7e82e642ab0f7608ce2fe53d76ca2280a9ee33a1b06556142c7c6fe80a86fc33"}, + {file = "librt-0.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88145c15c67731d54283d135b03244028c750cc9edc334a96a4f5950ebdb2884"}, + {file = "librt-0.11.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d36a51b3d93320b686588e27123f4995804dbf1bce81df78c02fc3c6eea9280"}, + {file = "librt-0.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d00f3ac06a2a8b246327f11e186a53a100a4d5c7ed52346367e5ec751d51586c"}, + {file = "librt-0.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:461bbceede621f1ffb8839755f8663e886087ee7af16294cab7fb4d782c62eeb"}, + {file = "librt-0.11.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0cad8a4d6a8ff03c9b76f9414caccd78e7cfbc8a2e12fa334d8e1d9932753783"}, + {file = "librt-0.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f37aa505b3cf60701562eddb32df74b12a9e380c207fd8b06dd157a943ac7ea0"}, + {file = "librt-0.11.0-cp310-cp310-win32.whl", hash = "sha256:94663a21534637f0e787ec2a2a756022df6e5b7b2335a5cdd7d8e33d68a2af89"}, + {file = "librt-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:dec7db73758c2b54953fd8b7fe348c45188fe26b39ee18446196edd08453a5d4"}, + {file = "librt-0.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:93d95bd45b7d58343d8b90d904450a545144eec19a002511163426f8ab1fae29"}, + {file = "librt-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ee278c769a713638cdacd4c0436d72156e75df3ebc0166ab2b9dc43acc386c9"}, + {file = "librt-0.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f230cb1cbc9faaa616f9a678f530ebcf186e414b6bcbd88b960e4ba1b92428d5"}, + {file = "librt-0.11.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5d63c855d86938d9de93e265c9bd8c705b51ec494de5738340ee93767a686e4b"}, + {file = "librt-0.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f028be9e96a08d31df3479ac80d99be374d17f3b78e4796b3fd3c913d4e89"}, + {file = "librt-0.11.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:258d73a0aa66a055e65b2e4d1b8cdb23b9d132c5bb915d9547d804fcaed116cc"}, + {file = "librt-0.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0827efe7854718f04aaddf6496e96960a956e676fe1d0f04eb41511fd8ad06d5"}, + {file = "librt-0.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7753e57d6e12d019c0d8786f1c09c709f4c3fcc57c3887b24e36e6c06ec938b7"}, + {file = "librt-0.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:11bd19822431cc21af9f27374e7ae2e58103c7d98bda823536a6c47f6bb2bb3d"}, + {file = "librt-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:22bdf239b219d3993761a148ffa134b19e52e9989c84f845d5d7b71d70a17412"}, + {file = "librt-0.11.0-cp311-cp311-win32.whl", hash = "sha256:46c60b61e308eb535fbd6fa622b1ee1bb2815691c1ad9c98bf7b84952ec3bc8d"}, + {file = "librt-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:902e546ff044f579ff1c953ff5fce97b636fe9e3943996b2177710c6ef076f73"}, + {file = "librt-0.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:65ac3bc20f78aa0ee5ae84baa68917f89fef4af63e941084dd019a0d0e749f0c"}, + {file = "librt-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b87504f1690a23b9a2cca841191a04f83895d4fc2dd04df91d82b1a04ca2ad46"}, + {file = "librt-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40071fc5fe0ce8daa6de616702314a01e1250711682b0523d6ab8d4525910cb3"}, + {file = "librt-0.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:137e79445c896a0ea7b265f52d23954e05b64222ee1af69e2cb34219067cbb67"}, + {file = "librt-0.11.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:cca6644054e78746d8d4ef238681f9c34ff8b584fe6b988ecebb8db3b15e622a"}, + {file = "librt-0.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5b0eea49f5562861ee8d757a32ef7d559c1d35be2aaaa1ec28941d74c9ffc8a"}, + {file = "librt-0.11.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d1029d7e1ae1a7e647ed6fb5df8c4ce2dffefb7a9f5fd1376a4554d96dac09f"}, + {file = "librt-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc3ce6b33c5828d9e80592011a5c584cb2ce86edbc4088405f70da47dc1d1b3b"}, + {file = "librt-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:936c5995f3514a42111f20099397d8177c79b4d7e70961e396c6f5a0a3566766"}, + {file = "librt-0.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9bc0ca6ad9381cbe8e4aa6e5726e4c80c78115a6e9723c599ed1d73e092bc49d"}, + {file = "librt-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:070aa8c26c0a74774317a72df8851facc7f0f012a5b406557ac56992d92e1ec8"}, + {file = "librt-0.11.0-cp312-cp312-win32.whl", hash = "sha256:6bf14feb84b05ae945277395451998c89c54d0def4070eb5c08de544930b245a"}, + {file = "librt-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:75672f0bc524ede266287d532d7923dbce94c7514ad07627bac3d0c6d92cc4d9"}, + {file = "librt-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f10cf143e4a9bb0f4f5af568a00df94a2d69ef41c2579584454bb0fe5cc642c"}, + {file = "librt-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78dc31f7fdfe9c9d0eb0e8f42d139db230e826415bbcabd9f0e9faaaee909894"}, + {file = "librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fa475675db22290c3158e1d42326d0f5a65f04f44a0e68c3630a25b53560fb9c"}, + {file = "librt-0.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:621db29691044bdeda22e789e482e1b0f3a985d90e3426c9c6d17606416205ea"}, + {file = "librt-0.11.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a9010e2ed5b3a9e158c5fd966b3ab7e834bb3d3aacc8f66c91dd4b57a3799230"}, + {file = "librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c39513d8b7477a2e1ed8c43fc21c524e8d5a0f8d4e8b7b074dbdbe7820a08e2"}, + {file = "librt-0.11.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7aef3cf1d5af86e770ab04bfd993dfc4ae8b8c17f66fb77dd4a7d50de7bbb1a3"}, + {file = "librt-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:557183ddc36babe46b27dd60facbd5adb4492181a5be887587d57cda6e092f21"}, + {file = "librt-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83d3e1f72bd42f6c5c0b7daec530c3f829bd02db42c70b8ddf0c2d90a2459930"}, + {file = "librt-0.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4ce1f21fbe589bc1afd7872dece84fb0e1144f794a288e58a10d2c54a55c43be"}, + {file = "librt-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b09f7044ea2b64c9da42fd3d335666518cfd1c6e8a182c95da73d0214b41e"}, + {file = "librt-0.11.0-cp313-cp313-win32.whl", hash = "sha256:78fddc31cd4d3caa897ad5d31f856b1faadc9474021ad6cb182b9018793e254e"}, + {file = "librt-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ca8aa88751a775870b764e93bad5135385f563cb8dcee399abf034ea4d3cb47"}, + {file = "librt-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:96f044bb325fd9cf1a723015638c219e9143f0dfbc0ca54c565df2b7fc748b44"}, + {file = "librt-0.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4a017a95e5837dc15a8c5661d60e05daa96b90908b1aa6b7acdf443cd25c8ebd"}, + {file = "librt-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ecbd9819deccc39b7542bf4d2a740d8a620694d39989e58661d3763458f8d4"}, + {file = "librt-0.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da327dacd7be8f8ec36547373550744a3cc0e536d54665cd83f8bcd961200e8"}, + {file = "librt-0.11.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0dc56b1f8d06e60db362cc3fdae206681817f86ce4725d34511473487f12a34b"}, + {file = "librt-0.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb8fb2ab90e21c8d12ea240d744ad514da9baf381ebfa70d91d20d21713175"}, + {file = "librt-0.11.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cae74872be221df4374d10fec61f93ed1513b9546ea84f2c0bf73ab3e9bd0b03"}, + {file = "librt-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32bcc918c0148eb7e3d57385125bac7e5f9e4359d05f07448b09f6f778c2f31c"}, + {file = "librt-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f9743fc99135d5f78d2454435615f6dec0473ca507c26ce9d92b10b562a280d3"}, + {file = "librt-0.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5ba067f4aadae8fda802d91d2124c90c42195ff32d9161d3549e6d05cfe26f96"}, + {file = "librt-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:de3bf945454d032f9e390b85c4072e0a0570bf825421c8be0e71209fa65e1abe"}, + {file = "librt-0.11.0-cp314-cp314-win32.whl", hash = "sha256:d2277a05f6dcb9fd13db9566aac4fabd68c3ea1ea46ee5567d4eef8efa495a2f"}, + {file = "librt-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ab73e8db5e3f564d812c1f5c3a175930a5f9bc96ccb5e3b22a34d7858b401cf7"}, + {file = "librt-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aea3caa317752e3a466fa8af45d91ee0ea8c7fdd96e42b0a8dd9b76a7931eba1"}, + {file = "librt-0.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d1b36540d7aaf9b9101b3a6f376c8d8e9f7a9aec93ed05918f2c69d493ffef72"}, + {file = "librt-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:efbb343ab2ce3540f4ecbe6315d677ed70f37cd9a72b1e58066c918ca83acbaa"}, + {file = "librt-0.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0dd688aab3f7914d3e6e5e3554978e0383312fb8e771d84be008a35b9ee548"}, + {file = "librt-0.11.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f5fb36b8c6c63fdcbb1d526d94c0d1331610d43f4118cc1beb4efef4f3faacb2"}, + {file = "librt-0.11.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a9a237d13addb93715b6fee74023d5ee3469b53fce527626c0e088aa585805f"}, + {file = "librt-0.11.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5ddd17bd87b2c56ddd60e546a7984a2e64c4e8eab92fb4cf3830a48ad5469d51"}, + {file = "librt-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd43992b4473d42f12ff9e68326079f0696d9d4e6000e8f39a0238d482ba6ee2"}, + {file = "librt-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f8e3e8056dd674e279741485e2e512d6e9a751c7455809d0114e6ebf8d781085"}, + {file = "librt-0.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c1f708d8ae9c56cf38a903c44297243d2ec83fd82b396b977e0144a3e76217e3"}, + {file = "librt-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0add982e0e7b9fc14cf4b33789d5f13f66581889b88c2f58099f6ce8f92617bd"}, + {file = "librt-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:2b481d846ac894c4e8403c5fd0e87c5d11d6499e404b474602508a224ff531c8"}, + {file = "librt-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:28edb433edde181112a908c78907af28f964eabc15f4dd16c9d66c834302677c"}, + {file = "librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253"}, + {file = "librt-0.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6bd72d903911d995ab666dbd1871f8b1e80925a699af8063fbf50053329fb05f"}, + {file = "librt-0.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ef69ac715f3cd8e5cd252cb2aebfa72c015492aacc339d5d7bf8fef3c62c677"}, + {file = "librt-0.11.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:624a40c4a4ad7773315c287276cd024509b2c66ff5904f504bfc08d2c70293ab"}, + {file = "librt-0.11.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:41dc19fe150b69716c8ece4f76773a9e8813fe3e35e032a58b4d46423fb8d7c0"}, + {file = "librt-0.11.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e8bd98ea9c47ae90b319a087ab28dac493f1ffbc1ecd1f28fcdbf3b7e1108d1"}, + {file = "librt-0.11.0-cp39-cp39-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84308fc49423ce6475d1c5d1985cd69a8ca9f0325fc7d5f81bb690a3f3625d4e"}, + {file = "librt-0.11.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ff0fbaf5f44a21beeb0110f2ab64f45135a9536a834b79c0d1ef018f2786bbfa"}, + {file = "librt-0.11.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9c028a9442a18e266955d364ce42259136e79a7ba14d773e0d778d5f70cd56f1"}, + {file = "librt-0.11.0-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:9f1692105a02bcf853f355032a5fdc5494358ef83d8fd22d16de375c85cec3f5"}, + {file = "librt-0.11.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7a80a71e1fda83cc752a9141e87aae7fef279538597564d670e9ce513f286192"}, + {file = "librt-0.11.0-cp39-cp39-win32.whl", hash = "sha256:140695816ddf3c86eb972981a26f35efd871c44b0c3aed44c8cd01749386617f"}, + {file = "librt-0.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:92f7ff819c197fc30473190a12c2856f325ac90aabfccbeb2072d28cc2e234e3"}, + {file = "librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1"}, +] + [[package]] name = "mccabe" -version = "0.6.1" +version = "0.7.0" description = "McCabe checker, plugin for flake8" -category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "mypy" +version = "1.19.1" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version < \"3.15\"" +files = [ + {file = "mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec"}, + {file = "mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b"}, + {file = "mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6"}, + {file = "mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74"}, + {file = "mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1"}, + {file = "mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac"}, + {file = "mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288"}, + {file = "mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab"}, + {file = "mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6"}, + {file = "mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331"}, + {file = "mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925"}, + {file = "mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042"}, + {file = "mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1"}, + {file = "mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e"}, + {file = "mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2"}, + {file = "mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8"}, + {file = "mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a"}, + {file = "mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13"}, + {file = "mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250"}, + {file = "mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b"}, + {file = "mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e"}, + {file = "mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef"}, + {file = "mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75"}, + {file = "mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd"}, + {file = "mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1"}, + {file = "mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718"}, + {file = "mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b"}, + {file = "mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045"}, + {file = "mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957"}, + {file = "mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f"}, + {file = "mypy-1.19.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3"}, + {file = "mypy-1.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a"}, + {file = "mypy-1.19.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67"}, + {file = "mypy-1.19.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e"}, + {file = "mypy-1.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376"}, + {file = "mypy-1.19.1-cp39-cp39-win_amd64.whl", hash = "sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24"}, + {file = "mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247"}, + {file = "mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba"}, +] + +[package.dependencies] +librt = {version = ">=0.6.2", markers = "platform_python_implementation != \"PyPy\""} +mypy_extensions = ">=1.0.0" +pathspec = ">=0.9.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing_extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] [[package]] name = "mypy" -version = "0.812" +version = "1.20.2" description = "Optional static typing for Python" -category = "dev" optional = false -python-versions = ">=3.5" +python-versions = ">=3.10" +groups = ["dev"] +markers = "python_version >= \"3.15\"" +files = [ + {file = "mypy-1.20.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cf5a4db6dca263010e2c7bff081c89383c72d187ba2cf4c44759aac970e2f0c4"}, + {file = "mypy-1.20.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7b0e817b518bff7facd7f85ea05b643ad8bdcce684cf29784987b0a7c8e1f997"}, + {file = "mypy-1.20.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97d7b9a485b40f8ca425460e89bf1da2814625b2da627c0dcc6aa46c92631d14"}, + {file = "mypy-1.20.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e1c12f6d2db3d78b909b5f77513c11eb7f2dd2782b96a3ab6dffc7d44575c99"}, + {file = "mypy-1.20.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:89dce27e142d25ffbc154c1819383b69f2e9234dc4ed4766f42e0e8cb264ab5c"}, + {file = "mypy-1.20.2-cp310-cp310-win_amd64.whl", hash = "sha256:f376e37f9bf2a946872fc5fd1199c99310748e3c26c7a26683f13f8bdb756cbd"}, + {file = "mypy-1.20.2-cp310-cp310-win_arm64.whl", hash = "sha256:6e2b469efd811707bc530fd1effef0f5d6eebcb7fe376affae69025da4b979a2"}, + {file = "mypy-1.20.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4077797a273e56e8843d001e9dfe4ba10e33323d6ade647ff260e5cd97d9758c"}, + {file = "mypy-1.20.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cdecf62abcc4292500d7858aeae87a1f8f1150f4c4dd08fb0b336ee79b2a6df3"}, + {file = "mypy-1.20.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c566c3a88b6ece59b3d70f65bedef17304f48eb52ff040a6a18214e1917b3254"}, + {file = "mypy-1.20.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0deb80d062b2479f2c87ae568f89845afc71d11bc41b04179e58165fd9f31e98"}, + {file = "mypy-1.20.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bba9ad231e92a3e424b3e56b65aa17704993425bba97e302c832f9466bb85bac"}, + {file = "mypy-1.20.2-cp311-cp311-win_amd64.whl", hash = "sha256:baf593f2765fa3a6b1ef95807dbaa3d25b594f6a52adcc506a6b9cb115e1be67"}, + {file = "mypy-1.20.2-cp311-cp311-win_arm64.whl", hash = "sha256:20175a1c0f49863946ec20b7f63255768058ac4f07d2b9ded6a6b46cfb5a9100"}, + {file = "mypy-1.20.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4dbfcf869f6b0517f70cf0030ba6ea1d6645e132337a7d5204a18d8d5636c02b"}, + {file = "mypy-1.20.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b6481b228d072315b053210b01ac320e1be243dc17f9e5887ef167f23f5fae4"}, + {file = "mypy-1.20.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34397cdced6b90b836e38182076049fdb41424322e0b0728c946b0939ebdf9f6"}, + {file = "mypy-1.20.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5da6976f20cae27059ea8d0c86e7cef3de720e04c4bb9ee18e3690fdb792066"}, + {file = "mypy-1.20.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:56908d7e08318d39f85b1f0c6cfd47b0cac1a130da677630dac0de3e0623e102"}, + {file = "mypy-1.20.2-cp312-cp312-win_amd64.whl", hash = "sha256:d52ad8d78522da1d308789df651ee5379088e77c76cb1994858d40a426b343b9"}, + {file = "mypy-1.20.2-cp312-cp312-win_arm64.whl", hash = "sha256:785b08db19c9f214dc37d65f7c165d19a30fcecb48abfa30f31b01b5acaabb58"}, + {file = "mypy-1.20.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:edfbfca868cdd6bd8d974a60f8a3682f5565d3f5c99b327640cedd24c4264026"}, + {file = "mypy-1.20.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e2877a02380adfcdbc69071a0f74d6e9dbbf593c0dc9d174e1f223ffd5281943"}, + {file = "mypy-1.20.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7488448de6007cd5177c6cea0517ac33b4c0f5ee9b5e9f2be51ce75511a85517"}, + {file = "mypy-1.20.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb9c2fa06887e21d6a3a868762acb82aec34e2c6fd0174064f27c93ede68ad15"}, + {file = "mypy-1.20.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d56a78b646f2e3daa865bc70cd5ec5a46c50045801ca8ff17a0c43abc97e3ee"}, + {file = "mypy-1.20.2-cp313-cp313-win_amd64.whl", hash = "sha256:2a4102b03bb7481d9a91a6da8d174740c9c8c4401024684b9ca3b7cc5e49852f"}, + {file = "mypy-1.20.2-cp313-cp313-win_arm64.whl", hash = "sha256:a95a9248b0c6fd933a442c03c3b113c3b61320086b88e2c444676d3fd1ca3330"}, + {file = "mypy-1.20.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:419413398fe250aae057fd2fe50166b61077083c9b82754c341cf4fd73038f30"}, + {file = "mypy-1.20.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e73c07f23009962885c197ccb9b41356a30cc0e5a1d0c2ea8fd8fb1362d7f924"}, + {file = "mypy-1.20.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c64e5973df366b747646fc98da921f9d6eba9716d57d1db94a83c026a08e0fb"}, + {file = "mypy-1.20.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a65aa591af023864fd08a97da9974e919452cfe19cb146c8a5dc692626445dc"}, + {file = "mypy-1.20.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4fef51b01e638974a6e69885687e9bd40c8d1e09a6cd291cca0619625cf1f558"}, + {file = "mypy-1.20.2-cp314-cp314-win_amd64.whl", hash = "sha256:913485a03f1bcf5d279409a9d2b9ed565c151f61c09f29991e5faa14033da4c8"}, + {file = "mypy-1.20.2-cp314-cp314-win_arm64.whl", hash = "sha256:c3bae4f855d965b5453784300c12ffc63a548304ac7f99e55d4dc7c898673aa3"}, + {file = "mypy-1.20.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2de3dcea53babc1c3237a19002bc3d228ce1833278f093b8d619e06e7cc79609"}, + {file = "mypy-1.20.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:52b176444e2e5054dfcbcb8c75b0b719865c96247b37407184bbfca5c353f2c2"}, + {file = "mypy-1.20.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:688c3312e5dadb573a2c69c82af3a298d43ecf9e6d264e0f95df960b5f6ac19c"}, + {file = "mypy-1.20.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29752dbbf8cc53f89f6ac096d363314333045c257c9c75cbd189ca2de0455744"}, + {file = "mypy-1.20.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:803203d2b6ea644982c644895c2f78b28d0e208bba7b27d9b921e0ec5eb207c6"}, + {file = "mypy-1.20.2-cp314-cp314t-win_amd64.whl", hash = "sha256:9bcb8aa397ff0093c824182fd76a935a9ba7ad097fcbef80ae89bf6c1731d8ec"}, + {file = "mypy-1.20.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e061b58443f1736f8a37c48978d7ab581636d6ab03e3d4f99e3fa90463bb9382"}, + {file = "mypy-1.20.2-py3-none-any.whl", hash = "sha256:a94c5a76ab46c5e6257c7972b6c8cff0574201ca7dc05647e33e795d78680563"}, + {file = "mypy-1.20.2.tar.gz", hash = "sha256:e8222c26daaafd9e8626dec58ae36029f82585890589576f769a650dd20fd665"}, +] [package.dependencies] -mypy-extensions = ">=0.4.3,<0.5.0" -typed-ast = ">=1.4.0,<1.5.0" -typing-extensions = ">=3.7.4" +librt = {version = ">=0.8.0", markers = "platform_python_implementation != \"PyPy\""} +mypy_extensions = ">=1.0.0" +pathspec = ">=1.0.0" +typing_extensions = {version = ">=4.14.0", markers = "python_version >= \"3.15\""} [package.extras] dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +native-parser = ["ast-serialize (>=0.1.1,<1.0.0)"] +reports = ["lxml"] [[package]] name = "mypy-extensions" -version = "0.4.3" -description = "Experimental type system extensions for programs checked with the mypy typechecker." -category = "dev" +version = "1.1.0" +description = "Type system extensions for programs checked with the mypy type checker." optional = false -python-versions = "*" +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, + {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, +] [[package]] name = "packaging" -version = "20.9" +version = "26.2" description = "Core utilities for Python packages" -category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[package.dependencies] -pyparsing = ">=2.0.2" +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e"}, + {file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"}, +] [[package]] name = "pathspec" -version = "0.8.1" +version = "1.1.1" description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189"}, + {file = "pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a"}, +] + +[package.extras] +hyperscan = ["hyperscan (>=0.7)"] +optional = ["typing-extensions (>=4)"] +re2 = ["google-re2 (>=1.1)"] + +[[package]] +name = "platformdirs" +version = "4.4.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version < \"3.15\"" +files = [ + {file = "platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85"}, + {file = "platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.14.1)"] + +[[package]] +name = "platformdirs" +version = "4.10.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +markers = "python_version >= \"3.15\"" +files = [ + {file = "platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a"}, + {file = "platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7"}, +] [[package]] name = "pluggy" version = "0.13.1" description = "plugin and hook calling mechanisms for python" -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[package.dependencies] -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +groups = ["dev"] +files = [ + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, +] [package.extras] dev = ["pre-commit", "tox"] -[[package]] -name = "py" -version = "1.10.0" -description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - [[package]] name = "pycodestyle" -version = "2.7.0" +version = "2.14.0" description = "Python style guide checker" -category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d"}, + {file = "pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783"}, +] [[package]] name = "pyflakes" -version = "2.3.1" +version = "3.4.0" description = "passive checker of Python programs" -category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f"}, + {file = "pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58"}, +] [[package]] name = "pyjsparser" version = "2.7.1" description = "Fast javascript parser (based on esprima.js)" -category = "main" -optional = false +optional = true python-versions = "*" - -[[package]] -name = "pyparsing" -version = "2.4.7" -description = "Python parsing module" -category = "dev" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["main"] +markers = "extra == \"scxml\"" +files = [ + {file = "pyjsparser-2.7.1-py2-none-any.whl", hash = "sha256:2b12842df98d83f65934e0772fa4a5d8b123b3bc79f1af1789172ac70265dd21"}, + {file = "pyjsparser-2.7.1.tar.gz", hash = "sha256:be60da6b778cc5a5296a69d8e7d614f1f870faf94e1b1b6ac591f2ad5d729579"}, +] [[package]] name = "pytest" -version = "6.2.4" +version = "7.4.4" description = "pytest: simple powerful testing with Python" -category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, +] [package.dependencies] -atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} -attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" -pluggy = ">=0.12,<1.0.0a1" -py = ">=1.8.2" -toml = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-cov" -version = "2.12.0" +version = "4.1.0" description = "Pytest plugin for measuring coverage." -category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, +] [package.dependencies] coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] -testing = ["fields", "hunter", "process-tests (==2.0.2)", "six", "pytest-xdist", "virtualenv"] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] [[package]] name = "pytz" version = "2021.1" description = "World timezone definitions, modern and historical" -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "regex" -version = "2021.4.4" -description = "Alternative regular expression module, to replace re." -category = "dev" -optional = false +optional = true python-versions = "*" +groups = ["main"] +markers = "extra == \"scxml\"" +files = [ + {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, + {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, +] [[package]] name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" -category = "main" -optional = false +optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["main"] +markers = "extra == \"scxml\"" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] [[package]] name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" -category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["dev"] +markers = "python_version < \"3.15\"" +files = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] [[package]] -name = "typed-ast" -version = "1.4.3" -description = "a fork of Python 2 and 3 ast modules with type comment support" -category = "dev" -optional = false -python-versions = "*" +name = "tomli" +version = "2.4.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version < \"3.11\"" +files = [ + {file = "tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30"}, + {file = "tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a"}, + {file = "tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076"}, + {file = "tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9"}, + {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c"}, + {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc"}, + {file = "tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049"}, + {file = "tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e"}, + {file = "tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece"}, + {file = "tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a"}, + {file = "tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085"}, + {file = "tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9"}, + {file = "tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5"}, + {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585"}, + {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1"}, + {file = "tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917"}, + {file = "tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9"}, + {file = "tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257"}, + {file = "tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54"}, + {file = "tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a"}, + {file = "tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897"}, + {file = "tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f"}, + {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d"}, + {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5"}, + {file = "tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd"}, + {file = "tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36"}, + {file = "tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd"}, + {file = "tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf"}, + {file = "tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac"}, + {file = "tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662"}, + {file = "tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853"}, + {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15"}, + {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba"}, + {file = "tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6"}, + {file = "tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7"}, + {file = "tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232"}, + {file = "tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4"}, + {file = "tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c"}, + {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d"}, + {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41"}, + {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c"}, + {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f"}, + {file = "tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8"}, + {file = "tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26"}, + {file = "tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396"}, + {file = "tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe"}, + {file = "tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f"}, +] [[package]] name = "typing-extensions" -version = "3.10.0.0" -description = "Backported and Experimental Type Hints for Python 3.5+" -category = "dev" +version = "4.15.0" +description = "Backported and Experimental Type Hints for Python 3.9+" optional = false -python-versions = "*" +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, +] [[package]] name = "tzlocal" version = "2.1" description = "tzinfo object for the local timezone" -category = "main" -optional = false +optional = true python-versions = "*" +groups = ["main"] +markers = "extra == \"scxml\"" +files = [ + {file = "tzlocal-2.1-py2.py3-none-any.whl", hash = "sha256:e2cb6c6b5b604af38597403e9852872d7f534962ae2954c7f35efcb1ccacf4a4"}, + {file = "tzlocal-2.1.tar.gz", hash = "sha256:643c97c5294aedc737780a49d9df30889321cbe1204eac2c2ec6134035a92e44"}, +] [package.dependencies] pytz = "*" -[[package]] -name = "zipp" -version = "3.4.1" -description = "Backport of pathlib-compatible object wrapper for zip files" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] +[extras] +scxml = ["Js2Py"] [metadata] -lock-version = "1.1" -python-versions = "^3.6.2" -content-hash = "fb41e670a8523bc3b4e0f68dbaf2d3cc6cbdf975d05b44ec4854af257151815a" - -[metadata.files] -appdirs = [ - {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, - {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, -] -atomicwrites = [ - {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, - {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, -] -attrs = [ - {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, - {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, -] -black = [ - {file = "black-21.5b1-py3-none-any.whl", hash = "sha256:8a60071a0043876a4ae96e6c69bd3a127dad2c1ca7c8083573eb82f92705d008"}, - {file = "black-21.5b1.tar.gz", hash = "sha256:23695358dbcb3deafe7f0a3ad89feee5999a46be5fec21f4f1d108be0bcdb3b1"}, -] -click = [ - {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, - {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"}, -] -colorama = [ - {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, - {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, -] -coverage = [ - {file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"}, - {file = "coverage-5.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b"}, - {file = "coverage-5.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669"}, - {file = "coverage-5.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90"}, - {file = "coverage-5.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c"}, - {file = "coverage-5.5-cp27-cp27m-win32.whl", hash = "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a"}, - {file = "coverage-5.5-cp27-cp27m-win_amd64.whl", hash = "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82"}, - {file = "coverage-5.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905"}, - {file = "coverage-5.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083"}, - {file = "coverage-5.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5"}, - {file = "coverage-5.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81"}, - {file = "coverage-5.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6"}, - {file = "coverage-5.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0"}, - {file = "coverage-5.5-cp310-cp310-win_amd64.whl", hash = "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae"}, - {file = "coverage-5.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb"}, - {file = "coverage-5.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160"}, - {file = "coverage-5.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"}, - {file = "coverage-5.5-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701"}, - {file = "coverage-5.5-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793"}, - {file = "coverage-5.5-cp35-cp35m-win32.whl", hash = "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e"}, - {file = "coverage-5.5-cp35-cp35m-win_amd64.whl", hash = "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3"}, - {file = "coverage-5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066"}, - {file = "coverage-5.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a"}, - {file = "coverage-5.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465"}, - {file = "coverage-5.5-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb"}, - {file = "coverage-5.5-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821"}, - {file = "coverage-5.5-cp36-cp36m-win32.whl", hash = "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45"}, - {file = "coverage-5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184"}, - {file = "coverage-5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a"}, - {file = "coverage-5.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53"}, - {file = "coverage-5.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d"}, - {file = "coverage-5.5-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638"}, - {file = "coverage-5.5-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3"}, - {file = "coverage-5.5-cp37-cp37m-win32.whl", hash = "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a"}, - {file = "coverage-5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a"}, - {file = "coverage-5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6"}, - {file = "coverage-5.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2"}, - {file = "coverage-5.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759"}, - {file = "coverage-5.5-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873"}, - {file = "coverage-5.5-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a"}, - {file = "coverage-5.5-cp38-cp38-win32.whl", hash = "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6"}, - {file = "coverage-5.5-cp38-cp38-win_amd64.whl", hash = "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502"}, - {file = "coverage-5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b"}, - {file = "coverage-5.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529"}, - {file = "coverage-5.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b"}, - {file = "coverage-5.5-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff"}, - {file = "coverage-5.5-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b"}, - {file = "coverage-5.5-cp39-cp39-win32.whl", hash = "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6"}, - {file = "coverage-5.5-cp39-cp39-win_amd64.whl", hash = "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03"}, - {file = "coverage-5.5-pp36-none-any.whl", hash = "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079"}, - {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, - {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, -] -dataclasses = [ - {file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"}, - {file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"}, -] -flake8 = [ - {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, - {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, -] -importlib-metadata = [ - {file = "importlib_metadata-4.3.1-py3-none-any.whl", hash = "sha256:c2e27fa8b6c8b34ebfcd4056ae2ca290e36250d1fbeceec85c1c67c711449fac"}, - {file = "importlib_metadata-4.3.1.tar.gz", hash = "sha256:2d932ea08814f745863fd20172fe7de4794ad74567db78f2377343e24520a5b6"}, -] -iniconfig = [ - {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, - {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, -] -isort = [ - {file = "isort-5.8.0-py3-none-any.whl", hash = "sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d"}, - {file = "isort-5.8.0.tar.gz", hash = "sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6"}, -] -js2py = [ - {file = "Js2Py-0.71-py3-none-any.whl", hash = "sha256:7f3dfa30c4b8e3ac3edd64c361c950a8c2587a30a86854aeb8c1ffb6f4dd24b4"}, - {file = "Js2Py-0.71.tar.gz", hash = "sha256:a41b1009dd1498ae7d436bfa5ac952a08ca92a4bb9e31dca6e8bb966b49f7fce"}, -] -mccabe = [ - {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, - {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, -] -mypy = [ - {file = "mypy-0.812-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a26f8ec704e5a7423c8824d425086705e381b4f1dfdef6e3a1edab7ba174ec49"}, - {file = "mypy-0.812-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:28fb5479c494b1bab244620685e2eb3c3f988d71fd5d64cc753195e8ed53df7c"}, - {file = "mypy-0.812-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:9743c91088d396c1a5a3c9978354b61b0382b4e3c440ce83cf77994a43e8c521"}, - {file = "mypy-0.812-cp35-cp35m-win_amd64.whl", hash = "sha256:d7da2e1d5f558c37d6e8c1246f1aec1e7349e4913d8fb3cb289a35de573fe2eb"}, - {file = "mypy-0.812-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4eec37370483331d13514c3f55f446fc5248d6373e7029a29ecb7b7494851e7a"}, - {file = "mypy-0.812-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d65cc1df038ef55a99e617431f0553cd77763869eebdf9042403e16089fe746c"}, - {file = "mypy-0.812-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:61a3d5b97955422964be6b3baf05ff2ce7f26f52c85dd88db11d5e03e146a3a6"}, - {file = "mypy-0.812-cp36-cp36m-win_amd64.whl", hash = "sha256:25adde9b862f8f9aac9d2d11971f226bd4c8fbaa89fb76bdadb267ef22d10064"}, - {file = "mypy-0.812-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:552a815579aa1e995f39fd05dde6cd378e191b063f031f2acfe73ce9fb7f9e56"}, - {file = "mypy-0.812-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:499c798053cdebcaa916eef8cd733e5584b5909f789de856b482cd7d069bdad8"}, - {file = "mypy-0.812-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:5873888fff1c7cf5b71efbe80e0e73153fe9212fafdf8e44adfe4c20ec9f82d7"}, - {file = "mypy-0.812-cp37-cp37m-win_amd64.whl", hash = "sha256:9f94aac67a2045ec719ffe6111df543bac7874cee01f41928f6969756e030564"}, - {file = "mypy-0.812-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d23e0ea196702d918b60c8288561e722bf437d82cb7ef2edcd98cfa38905d506"}, - {file = "mypy-0.812-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:674e822aa665b9fd75130c6c5f5ed9564a38c6cea6a6432ce47eafb68ee578c5"}, - {file = "mypy-0.812-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:abf7e0c3cf117c44d9285cc6128856106183938c68fd4944763003decdcfeb66"}, - {file = "mypy-0.812-cp38-cp38-win_amd64.whl", hash = "sha256:0d0a87c0e7e3a9becdfbe936c981d32e5ee0ccda3e0f07e1ef2c3d1a817cf73e"}, - {file = "mypy-0.812-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7ce3175801d0ae5fdfa79b4f0cfed08807af4d075b402b7e294e6aa72af9aa2a"}, - {file = "mypy-0.812-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:b09669bcda124e83708f34a94606e01b614fa71931d356c1f1a5297ba11f110a"}, - {file = "mypy-0.812-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:33f159443db0829d16f0a8d83d94df3109bb6dd801975fe86bacb9bf71628e97"}, - {file = "mypy-0.812-cp39-cp39-win_amd64.whl", hash = "sha256:3f2aca7f68580dc2508289c729bd49ee929a436208d2b2b6aab15745a70a57df"}, - {file = "mypy-0.812-py3-none-any.whl", hash = "sha256:2f9b3407c58347a452fc0736861593e105139b905cca7d097e413453a1d650b4"}, - {file = "mypy-0.812.tar.gz", hash = "sha256:cd07039aa5df222037005b08fbbfd69b3ab0b0bd7a07d7906de75ae52c4e3119"}, -] -mypy-extensions = [ - {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, - {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, -] -packaging = [ - {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, - {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, -] -pathspec = [ - {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, - {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, -] -pluggy = [ - {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, - {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, -] -py = [ - {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, - {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, -] -pycodestyle = [ - {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, - {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, -] -pyflakes = [ - {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, - {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, -] -pyjsparser = [ - {file = "pyjsparser-2.7.1-py2-none-any.whl", hash = "sha256:2b12842df98d83f65934e0772fa4a5d8b123b3bc79f1af1789172ac70265dd21"}, - {file = "pyjsparser-2.7.1.tar.gz", hash = "sha256:be60da6b778cc5a5296a69d8e7d614f1f870faf94e1b1b6ac591f2ad5d729579"}, -] -pyparsing = [ - {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, - {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, -] -pytest = [ - {file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"}, - {file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"}, -] -pytest-cov = [ - {file = "pytest-cov-2.12.0.tar.gz", hash = "sha256:8535764137fecce504a49c2b742288e3d34bc09eed298ad65963616cc98fd45e"}, - {file = "pytest_cov-2.12.0-py2.py3-none-any.whl", hash = "sha256:95d4933dcbbacfa377bb60b29801daa30d90c33981ab2a79e9ab4452c165066e"}, -] -pytz = [ - {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, - {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, -] -regex = [ - {file = "regex-2021.4.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:619d71c59a78b84d7f18891fe914446d07edd48dc8328c8e149cbe0929b4e000"}, - {file = "regex-2021.4.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:47bf5bf60cf04d72bf6055ae5927a0bd9016096bf3d742fa50d9bf9f45aa0711"}, - {file = "regex-2021.4.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:281d2fd05555079448537fe108d79eb031b403dac622621c78944c235f3fcf11"}, - {file = "regex-2021.4.4-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:bd28bc2e3a772acbb07787c6308e00d9626ff89e3bfcdebe87fa5afbfdedf968"}, - {file = "regex-2021.4.4-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:7c2a1af393fcc09e898beba5dd59196edaa3116191cc7257f9224beaed3e1aa0"}, - {file = "regex-2021.4.4-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c38c71df845e2aabb7fb0b920d11a1b5ac8526005e533a8920aea97efb8ec6a4"}, - {file = "regex-2021.4.4-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:96fcd1888ab4d03adfc9303a7b3c0bd78c5412b2bfbe76db5b56d9eae004907a"}, - {file = "regex-2021.4.4-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:ade17eb5d643b7fead300a1641e9f45401c98eee23763e9ed66a43f92f20b4a7"}, - {file = "regex-2021.4.4-cp36-cp36m-win32.whl", hash = "sha256:e8e5b509d5c2ff12f8418006d5a90e9436766133b564db0abaec92fd27fcee29"}, - {file = "regex-2021.4.4-cp36-cp36m-win_amd64.whl", hash = "sha256:11d773d75fa650cd36f68d7ca936e3c7afaae41b863b8c387a22aaa78d3c5c79"}, - {file = "regex-2021.4.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d3029c340cfbb3ac0a71798100ccc13b97dddf373a4ae56b6a72cf70dfd53bc8"}, - {file = "regex-2021.4.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:18c071c3eb09c30a264879f0d310d37fe5d3a3111662438889ae2eb6fc570c31"}, - {file = "regex-2021.4.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:4c557a7b470908b1712fe27fb1ef20772b78079808c87d20a90d051660b1d69a"}, - {file = "regex-2021.4.4-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:01afaf2ec48e196ba91b37451aa353cb7eda77efe518e481707e0515025f0cd5"}, - {file = "regex-2021.4.4-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:3a9cd17e6e5c7eb328517969e0cb0c3d31fd329298dd0c04af99ebf42e904f82"}, - {file = "regex-2021.4.4-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:90f11ff637fe8798933fb29f5ae1148c978cccb0452005bf4c69e13db951e765"}, - {file = "regex-2021.4.4-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:919859aa909429fb5aa9cf8807f6045592c85ef56fdd30a9a3747e513db2536e"}, - {file = "regex-2021.4.4-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:339456e7d8c06dd36a22e451d58ef72cef293112b559010db3d054d5560ef439"}, - {file = "regex-2021.4.4-cp37-cp37m-win32.whl", hash = "sha256:67bdb9702427ceddc6ef3dc382455e90f785af4c13d495f9626861763ee13f9d"}, - {file = "regex-2021.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:32e65442138b7b76dd8173ffa2cf67356b7bc1768851dded39a7a13bf9223da3"}, - {file = "regex-2021.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1e1c20e29358165242928c2de1482fb2cf4ea54a6a6dea2bd7a0e0d8ee321500"}, - {file = "regex-2021.4.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:314d66636c494ed9c148a42731b3834496cc9a2c4251b1661e40936814542b14"}, - {file = "regex-2021.4.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6d1b01031dedf2503631d0903cb563743f397ccaf6607a5e3b19a3d76fc10480"}, - {file = "regex-2021.4.4-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:741a9647fcf2e45f3a1cf0e24f5e17febf3efe8d4ba1281dcc3aa0459ef424dc"}, - {file = "regex-2021.4.4-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:4c46e22a0933dd783467cf32b3516299fb98cfebd895817d685130cc50cd1093"}, - {file = "regex-2021.4.4-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:e512d8ef5ad7b898cdb2d8ee1cb09a8339e4f8be706d27eaa180c2f177248a10"}, - {file = "regex-2021.4.4-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:980d7be47c84979d9136328d882f67ec5e50008681d94ecc8afa8a65ed1f4a6f"}, - {file = "regex-2021.4.4-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:ce15b6d103daff8e9fee13cf7f0add05245a05d866e73926c358e871221eae87"}, - {file = "regex-2021.4.4-cp38-cp38-win32.whl", hash = "sha256:a91aa8619b23b79bcbeb37abe286f2f408d2f2d6f29a17237afda55bb54e7aac"}, - {file = "regex-2021.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:c0502c0fadef0d23b128605d69b58edb2c681c25d44574fc673b0e52dce71ee2"}, - {file = "regex-2021.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:598585c9f0af8374c28edd609eb291b5726d7cbce16be6a8b95aa074d252ee17"}, - {file = "regex-2021.4.4-cp39-cp39-manylinux1_i686.whl", hash = "sha256:ee54ff27bf0afaf4c3b3a62bcd016c12c3fdb4ec4f413391a90bd38bc3624605"}, - {file = "regex-2021.4.4-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7d9884d86dd4dd489e981d94a65cd30d6f07203d90e98f6f657f05170f6324c9"}, - {file = "regex-2021.4.4-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:bf5824bfac591ddb2c1f0a5f4ab72da28994548c708d2191e3b87dd207eb3ad7"}, - {file = "regex-2021.4.4-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:563085e55b0d4fb8f746f6a335893bda5c2cef43b2f0258fe1020ab1dd874df8"}, - {file = "regex-2021.4.4-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9c3db21af35e3b3c05764461b262d6f05bbca08a71a7849fd79d47ba7bc33ed"}, - {file = "regex-2021.4.4-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:3916d08be28a1149fb97f7728fca1f7c15d309a9f9682d89d79db75d5e52091c"}, - {file = "regex-2021.4.4-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:fd45ff9293d9274c5008a2054ecef86a9bfe819a67c7be1afb65e69b405b3042"}, - {file = "regex-2021.4.4-cp39-cp39-win32.whl", hash = "sha256:fa4537fb4a98fe8fde99626e4681cc644bdcf2a795038533f9f711513a862ae6"}, - {file = "regex-2021.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:97f29f57d5b84e73fbaf99ab3e26134e6687348e95ef6b48cfd2c06807005a07"}, - {file = "regex-2021.4.4.tar.gz", hash = "sha256:52ba3d3f9b942c49d7e4bc105bb28551c44065f139a65062ab7912bef10c9afb"}, -] -six = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] -toml = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, -] -typed-ast = [ - {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"}, - {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"}, - {file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"}, - {file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"}, - {file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"}, - {file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"}, - {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"}, - {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"}, - {file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"}, - {file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"}, - {file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"}, - {file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"}, - {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"}, - {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"}, - {file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"}, - {file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"}, - {file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"}, - {file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"}, - {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"}, - {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"}, - {file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"}, - {file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"}, - {file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"}, - {file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"}, - {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"}, - {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"}, - {file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"}, - {file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"}, - {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"}, - {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, -] -typing-extensions = [ - {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"}, - {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, - {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, -] -tzlocal = [ - {file = "tzlocal-2.1-py2.py3-none-any.whl", hash = "sha256:e2cb6c6b5b604af38597403e9852872d7f534962ae2954c7f35efcb1ccacf4a4"}, - {file = "tzlocal-2.1.tar.gz", hash = "sha256:643c97c5294aedc737780a49d9df30889321cbe1204eac2c2ec6134035a92e44"}, -] -zipp = [ - {file = "zipp-3.4.1-py3-none-any.whl", hash = "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098"}, - {file = "zipp-3.4.1.tar.gz", hash = "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76"}, -] +lock-version = "2.1" +python-versions = ">=3.9" +content-hash = "cce969278e8c90fc46b752c5bc78096991138f18184ad8416ad69fb10cfd2b23" diff --git a/pyproject.toml b/pyproject.toml index 34716b3..33bcc65 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,36 +1,60 @@ -[tool.poetry] +[project] name = "xstate" -version = "0.0.1" -description = "XState for Python" +version = "0.1.0" +description = "Statecharts and state machines for Python, inspired by XState." readme = "README.md" -authors = ["David Khourshid "] +requires-python = ">=3.9" license = "MIT" -keywords = ["Answer Set Programming", "wrapper", "clingo"] +license-files = ["LICENSE"] +authors = [ + { name = "David Khourshid", email = "davidkpiano@gmail.com" }, +] +maintainers = [ + { name = "Jovani Pink", email = "jovanipink1@gmail.com" }, +] +keywords = [ + "state machine", + "statechart", + "finite state machine", + "fsm", + "xstate", + "scxml", +] classifiers = [ - "License :: OSI Approved :: MIT License", + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Topic :: Software Development :: Libraries :: Python Modules", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9" + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ] +dependencies = [] + +[project.optional-dependencies] +# Js2Py powers SCXML `cond` (JavaScript) evaluation only. It is optional so the +# core library installs and imports cleanly on modern Python. +# TODO(0.2.0): replace with a pure-Python SCXML condition evaluator. +scxml = ["Js2Py>=0.71"] + +[project.urls] +homepage = "https://github.com/JovaniPink/xstate-python" +repository = "https://github.com/JovaniPink/xstate-python" + +[tool.poetry] packages = [ { include = "xstate" }, ] -[tool.poetry.urls] -docs = "https://github.com/davidkpiano/xstate-python" - -[tool.poetry.dependencies] -python = "^3.6.2" -Js2Py = "^0.71" - -[tool.poetry.dev-dependencies] -pytest-cov = "^2.12.0" -black = "^21.5b1" -isort = "^5.8.0" -flake8 = "^3.9.2" -mypy = "^0.812" +[tool.poetry.group.dev.dependencies] +pytest = "^7.0" +pytest-cov = "^4.0" +black = "^24.0" +isort = "^5.12" +flake8 = "^7.0" +mypy = "^1.0" [tool.isort] profile = "black" @@ -54,5 +78,5 @@ fail_under = 50 show_missing = true [build-system] -requires = ["poetry-core>=1.0.0"] +requires = ["poetry-core>=2.0.0"] build-backend = "poetry.core.masonry.api" diff --git a/tests/test_actions.py b/tests/test_actions.py index 58ceaab..7772e49 100644 --- a/tests/test_actions.py +++ b/tests/test_actions.py @@ -1,7 +1,6 @@ -from unittest.mock import Mock - from xstate.machine import Machine - +from xstate.state import State +from unittest.mock import Mock def test_action(): entry_mock = Mock() @@ -12,44 +11,47 @@ def test_action(): "id": "machine", "initial": "on", "states": { - "on": { + "on" : { "on": {"TOGGLE": "off"}, "entry": [{"type": "entry_action"}], - "exit": [{"type": "exit_action"}], + "exit": [{"type": "exit_action"}] }, - "off": {"on": {"TOGGLE": "on"}}, + "off": { + "on": {"TOGGLE": "on"} + } }, }, - actions={ + actions = { "entry_action": entry_mock, "exit_action": exit_mock, - }, + } ) - state = machine.initial_state - assert state.value == "on" + state = machine.initial_state + assert state.value is "on" + for action in state.actions: action() - + entry_mock.assert_called_with() - assert entry_mock.call_count == 1 - assert exit_mock.call_count == 0 - + assert entry_mock.call_count is 1 + assert exit_mock.call_count is 0 + # ------------------------ - + state = machine.transition(state, "TOGGLE") - - assert state.value == "off" - + + assert state.value is "off" + for action in state.actions: action() - + exit_mock.assert_called_with() - assert entry_mock.call_count == 1 - assert exit_mock.call_count == 1 - - + assert entry_mock.call_count is 1 + assert exit_mock.call_count is 1 + + def test_entry_action_inline(): mock = Mock() @@ -58,22 +60,29 @@ def test_entry_action_inline(): "id": "machine", "initial": "on", "states": { - "on": {"on": {"TOGGLE": "off"}, "entry": [lambda: mock()]}, - "off": {"on": {"TOGGLE": "on"}}, + "on" : { + "on": {"TOGGLE": "off"}, + "entry": [ + lambda: mock() + ] + }, + "off": { + "on": {"TOGGLE": "on"} + } }, } ) - state = machine.initial_state - assert state.value == "on" + state = machine.initial_state + assert state.value is "on" + for action in state.actions: action() - + mock.assert_called_with() - assert mock.call_count == 1 - - + assert mock.call_count is 1 + def test_exit_action_inline(): mock = Mock() @@ -82,23 +91,32 @@ def test_exit_action_inline(): "id": "machine", "initial": "on", "states": { - "on": {"on": {"TOGGLE": "off"}, "exit": [lambda: mock()]}, - "off": {"on": {"TOGGLE": "on"}}, + "on" : { + "on": {"TOGGLE": "off"}, + "exit": [ + lambda: mock() + ] + }, + "off": { + "on": {"TOGGLE": "on"} + } }, } ) - state = machine.initial_state - assert state.value == "on" + state = machine.initial_state + assert state.value is "on" + for action in state.actions: action() - - assert mock.call_count == 0 - + + assert mock.call_count is 0 + state = machine.transition(state, "TOGGLE") - + for action in state.actions: action() - - assert mock.call_count == 1 + + assert mock.call_count is 1 + \ No newline at end of file diff --git a/tests/test_algorithm.py b/tests/test_algorithm.py index 05f9930..3b153e9 100644 --- a/tests/test_algorithm.py +++ b/tests/test_algorithm.py @@ -1,11 +1,16 @@ from xstate.algorithm import is_parallel_state from xstate.machine import Machine - def test_is_parallel_state(): - machine = Machine( - {"id": "test", "initial": "foo", "states": {"foo": {"type": "parallel"}}} - ) + machine = Machine({ + "id": "test", + "initial": "foo", + "states": { + "foo": { + "type": "parallel" + } + } + }) foo_state_node = machine._get_by_id("test.foo") @@ -13,9 +18,15 @@ def test_is_parallel_state(): def test_is_not_parallel_state(): - machine = Machine( - {"id": "test", "initial": "foo", "states": {"foo": {"type": "atomic"}}} - ) + machine = Machine({ + "id": "test", + "initial": "foo", + "states": { + "foo": { + "type": "atomic" + } + } + }) foo_state_node = machine._get_by_id("test.foo") diff --git a/tests/test_context.py b/tests/test_context.py new file mode 100644 index 0000000..eccdcc9 --- /dev/null +++ b/tests/test_context.py @@ -0,0 +1,196 @@ +"""Tests for context, assign actions, and pure-Python guards (0.2.0).""" + +import pytest + +from xstate import Machine, assign + + +def make_counter(guards=None): + return Machine( + { + "id": "counter", + "initial": "active", + "context": {"count": 0}, + "states": { + "active": { + "on": { + "INC": { + "target": "active", + "actions": [ + assign({"count": lambda ctx, ev: ctx["count"] + 1}) + ], + }, + "ADD": { + "target": "active", + "actions": [ + assign( + lambda ctx, ev: { + "count": ctx["count"] + ev.data["by"] + } + ) + ], + }, + "RESET": { + "target": "active", + "actions": [assign({"count": 0})], + }, + } + } + }, + }, + guards=guards or {}, + ) + + +def test_initial_context_from_config(): + machine = make_counter() + state = machine.initial_state + assert state.context == {"count": 0} + + +def test_assign_with_value_function(): + machine = make_counter() + state = machine.initial_state + state = machine.transition(state, "INC") + assert state.context["count"] == 1 + state = machine.transition(state, "INC") + assert state.context["count"] == 2 + + +def test_assign_with_static_value(): + machine = make_counter() + state = machine.initial_state + state = machine.transition(state, "INC") + state = machine.transition(state, "INC") + state = machine.transition(state, "RESET") + assert state.context["count"] == 0 + + +def test_assign_function_reads_event_data(): + machine = make_counter() + state = machine.initial_state + state = machine.transition(state, {"type": "ADD", "by": 5}) + assert state.context["count"] == 5 + state = machine.transition(state, {"type": "ADD", "by": 3}) + assert state.context["count"] == 8 + + +def test_context_is_immutable_between_states(): + machine = make_counter() + first = machine.initial_state + second = machine.transition(first, "INC") + # The original state's context must not be mutated by a later transition. + assert first.context["count"] == 0 + assert second.context["count"] == 1 + + +def test_guard_callable_blocks_transition(): + machine = Machine( + { + "id": "gate", + "initial": "closed", + "context": {"allowed": False}, + "states": { + "closed": { + "on": { + "OPEN": { + "target": "open", + "cond": lambda ctx, ev: ctx["allowed"], + } + } + }, + "open": {}, + }, + } + ) + state = machine.initial_state + # Guard is False -> transition is not taken, stays closed. + state = machine.transition(state, "OPEN") + assert state.value == "closed" + + +def test_guard_callable_allows_transition(): + machine = Machine( + { + "id": "gate", + "initial": "closed", + "context": {"allowed": True}, + "states": { + "closed": { + "on": { + "OPEN": { + "target": "open", + "cond": lambda ctx, ev: ctx["allowed"], + } + } + }, + "open": {}, + }, + } + ) + state = machine.initial_state + state = machine.transition(state, "OPEN") + assert state.value == "open" + + +def test_named_guard_from_registry(): + machine = Machine( + { + "id": "gate", + "initial": "closed", + "context": {"count": 3}, + "states": { + "closed": { + "on": { + "OPEN": {"target": "open", "cond": "isReady"}, + } + }, + "open": {}, + }, + }, + guards={"isReady": lambda ctx, ev: ctx["count"] >= 3}, + ) + state = machine.initial_state + state = machine.transition(state, "OPEN") + assert state.value == "open" + + +def test_guard_uses_event_data(): + machine = Machine( + { + "id": "gate", + "initial": "closed", + "states": { + "closed": { + "on": { + "OPEN": { + "target": "open", + "cond": lambda ctx, ev: ev.data.get("key") == "secret", + } + } + }, + "open": {}, + }, + } + ) + state = machine.initial_state + state = machine.transition(state, {"type": "OPEN", "key": "wrong"}) + assert state.value == "closed" + state = machine.transition(state, {"type": "OPEN", "key": "secret"}) + assert state.value == "open" + + +def test_missing_named_guard_raises(): + machine = Machine( + { + "id": "gate", + "initial": "closed", + "states": { + "closed": {"on": {"OPEN": {"target": "open", "cond": "nope"}}}, + "open": {}, + }, + } + ) + state = machine.initial_state + with pytest.raises(ValueError, match="Guard 'nope'"): + machine.transition(state, "OPEN") diff --git a/tests/test_history.py b/tests/test_history.py new file mode 100644 index 0000000..fa8fafd --- /dev/null +++ b/tests/test_history.py @@ -0,0 +1,97 @@ +"""Tests for shallow and deep history states (0.2.0).""" + +from xstate import Machine + + +def make_shallow(): + return Machine( + { + "id": "power", + "initial": "off", + "states": { + "off": {"on": {"POWER": "#hist"}}, + "on": { + "initial": "low", + "on": {"POWER": "off"}, + "states": { + "low": {"on": {"SWITCH": "high"}}, + "high": {"on": {"SWITCH": "low"}}, + "hist": {"type": "history", "id": "hist"}, + }, + }, + }, + } + ) + + +def make_deep(): + return Machine( + { + "id": "deep", + "initial": "off", + "states": { + "off": {"on": {"POWER": "#hist"}}, + "on": { + "initial": "a", + "on": {"POWER": "off"}, + "states": { + "a": { + "initial": "a1", + "states": { + "a1": {"on": {"NEXT": "a2"}}, + "a2": {}, + }, + }, + "hist": {"type": "history", "history": "deep", "id": "hist"}, + }, + }, + }, + } + ) + + +def test_history_default_falls_back_to_parent_initial(): + machine = make_shallow() + state = machine.initial_state + assert state.value == "off" + # First entry through history with no recorded value -> parent's initial. + state = machine.transition(state, "POWER") + assert state.value == {"on": "low"} + + +def test_shallow_history_restores_last_child(): + machine = make_shallow() + state = machine.initial_state + state = machine.transition(state, "POWER") # on.low + state = machine.transition(state, "SWITCH") # on.high + assert state.value == {"on": "high"} + state = machine.transition(state, "POWER") # off (records history: high) + assert state.value == "off" + state = machine.transition(state, "POWER") # restore -> high + assert state.value == {"on": "high"} + + +def test_shallow_history_updates_on_each_exit(): + machine = make_shallow() + state = machine.initial_state + state = machine.transition(state, "POWER") # low + state = machine.transition(state, "POWER") # off (history: low) + state = machine.transition(state, "POWER") # restore -> low + assert state.value == {"on": "low"} + state = machine.transition(state, "SWITCH") # high + state = machine.transition(state, "POWER") # off (history: high) + state = machine.transition(state, "POWER") # restore -> high + assert state.value == {"on": "high"} + + +def test_deep_history_restores_nested_atomic_state(): + machine = make_deep() + state = machine.initial_state + state = machine.transition(state, "POWER") # on.a.a1 + assert state.value == {"on": {"a": "a1"}} + state = machine.transition(state, "NEXT") # on.a.a2 + assert state.value == {"on": {"a": "a2"}} + state = machine.transition(state, "POWER") # off (deep history: a2) + assert state.value == "off" + state = machine.transition(state, "POWER") # restore deep -> a2 + assert state.value == {"on": {"a": "a2"}} diff --git a/tests/test_history_extended.py b/tests/test_history_extended.py new file mode 100644 index 0000000..8def145 --- /dev/null +++ b/tests/test_history_extended.py @@ -0,0 +1,307 @@ +"""Extended history-state tests ported from XState v4 test corpus (PR #49). + +Covers behaviors beyond the basic shallow/deep tests already in test_history.py: + - Two history pseudo-states (shallow + deep) on the same compound parent + - History in parallel regions (per-region shallow and deep history) + - Multi-target transitions that restore multiple regions simultaneously +""" + +import pytest +from xstate import Machine + +# --------------------------------------------------------------------------- +# Machine: compound state with both shallow and deep history pseudo-states +# --------------------------------------------------------------------------- +# +# off --POWER--> on.shallow_hist (shallow: restores on's immediate child) +# --DEEP_POWER--> on.deep_hist (deep: restores full atomic path inside on) +# on has first -> second -> A -> B -> P -> Q chain + + +def make_dual_history(): + return Machine( + { + "id": "dual_hist", + "initial": "off", + "states": { + "off": { + "on": { + "POWER": "#dual_shallow", + "DEEP_POWER": "#dual_deep", + } + }, + "on": { + "initial": "first", + "states": { + "first": {"on": {"SWITCH": "second"}}, + "second": { + "initial": "A", + "states": { + "A": {"on": {"INNER": "B"}}, + "B": { + "initial": "P", + "states": { + "P": {"on": {"INNER": "Q"}}, + "Q": {}, + }, + }, + }, + }, + "shallow_hist": { + "type": "history", + "history": "shallow", + "id": "dual_shallow", + }, + "deep_hist": { + "type": "history", + "history": "deep", + "id": "dual_deep", + }, + }, + "on": {"POWER": "off"}, + }, + }, + } + ) + + +def test_dual_hist_no_history_uses_initial(): + machine = make_dual_history() + state = machine.initial_state + state = machine.transition(state, "POWER") + assert state.value == {"on": "first"} + + +def test_dual_hist_shallow_restores_immediate_child(): + """Shallow history records on's immediate active child (second), not B.P.""" + machine = make_dual_history() + state = machine.initial_state + state = machine.transition(state, "POWER") # on.first + state = machine.transition(state, "SWITCH") # on.second.A + state = machine.transition(state, "INNER") # on.second.B.P + assert state.value == {"on": {"second": {"B": "P"}}} + state = machine.transition(state, "POWER") # off (records shallow hist: second) + assert state.value == "off" + # POWER uses shallow history → restores second at its initial child (A) + state = machine.transition(state, "POWER") + assert state.value == {"on": {"second": "A"}} + + +def test_dual_hist_deep_restores_full_path(): + """Deep history records the full atomic descendant path (second.B.P).""" + machine = make_dual_history() + state = machine.initial_state + state = machine.transition(state, "POWER") # on.first + state = machine.transition(state, "SWITCH") # on.second.A + state = machine.transition(state, "INNER") # on.second.B.P + state = machine.transition(state, "POWER") # off (records deep hist: second.B.P) + state = machine.transition(state, "DEEP_POWER") # restore deep → second.B.P + assert state.value == {"on": {"second": {"B": "P"}}} + + +def test_dual_hist_deep_tracks_updates(): + """Deep history tracks the most recent exit, not the first.""" + machine = make_dual_history() + state = machine.initial_state + state = machine.transition(state, "POWER") # on.first + state = machine.transition(state, "SWITCH") # on.second.A + state = machine.transition(state, "INNER") # on.second.B.P + state = machine.transition(state, "INNER") # on.second.B.Q + assert state.value == {"on": {"second": {"B": "Q"}}} + state = machine.transition(state, "POWER") # off (records deep hist: second.B.Q) + state = machine.transition(state, "DEEP_POWER") + assert state.value == {"on": {"second": {"B": "Q"}}} + + +# --------------------------------------------------------------------------- +# Machine: parallel state with per-region history pseudo-states +# --------------------------------------------------------------------------- +# +# Adapted from XState's history.test.ts "parallel history states" describe block. +# +# off can re-enter `on` in several ways: +# SWITCH → on (enters all regions at initial) +# POWER → on.hist (shallow history of the parallel root) +# DEEP_POWER → on.deep_hist (deep history of the parallel root) +# PARALLEL_HIST → [A.hist, K.hist] (per-region shallow history) +# PARALLEL_DEEP → [A.deep, K.deep] (per-region deep history) +# +# Region A: B -> C(initial=D -> E) +# Region K: L -> M(initial=N -> O) + + +def make_parallel_history(): + return Machine( + { + "id": "parhist", + "initial": "off", + "states": { + "off": { + "on": { + "SWITCH": "on", + "POWER": "#ph_on_shallow", + "DEEP_POWER": "#ph_on_deep", + "PARALLEL_HIST": [ + {"target": ["#ph_A_shallow", "#ph_K_shallow"]} + ], + "PARALLEL_DEEP": [{"target": ["#ph_A_deep", "#ph_K_deep"]}], + } + }, + "on": { + "type": "parallel", + "states": { + "A": { + "initial": "B", + "states": { + "B": {"on": {"INNER_A": "C"}}, + "C": { + "initial": "D", + "states": { + "D": {"on": {"INNER_A": "E"}}, + "E": {}, + }, + }, + "hist": { + "type": "history", + "history": "shallow", + "id": "ph_A_shallow", + }, + "deep_hist": { + "type": "history", + "history": "deep", + "id": "ph_A_deep", + }, + }, + }, + "K": { + "initial": "L", + "states": { + "L": {"on": {"INNER_K": "M"}}, + "M": { + "initial": "N", + "states": { + "N": {"on": {"INNER_K": "O"}}, + "O": {}, + }, + }, + "hist": { + "type": "history", + "history": "shallow", + "id": "ph_K_shallow", + }, + "deep_hist": { + "type": "history", + "history": "deep", + "id": "ph_K_deep", + }, + }, + }, + "hist": { + "type": "history", + "history": "shallow", + "id": "ph_on_shallow", + }, + "deep_hist": { + "type": "history", + "history": "deep", + "id": "ph_on_deep", + }, + }, + "on": {"POWER": "off"}, + }, + }, + } + ) + + +def _reach_ACEKMN(machine): + """Return state with A=C.E, K=M.N.""" + s = machine.initial_state + s = machine.transition(s, "SWITCH") # on: A=B, K=L + s = machine.transition(s, "INNER_A") # A=C.D, K=L + s = machine.transition(s, "INNER_A") # A=C.E, K=L + s = machine.transition(s, "INNER_K") # A=C.E, K=M.N + return s + + +def _reach_ACEKMO(machine): + """Return state with A=C.E, K=M.O.""" + s = _reach_ACEKMN(machine) + s = machine.transition(s, "INNER_K") # A=C.E, K=M.O + return s + + +def test_parallel_switch_enters_initials(): + machine = make_parallel_history() + state = machine.initial_state + state = machine.transition(state, "SWITCH") + assert state.value == {"on": {"A": "B", "K": "L"}} + + +def test_parallel_shallow_history_of_root_ignores_region_substates(): + """Shallow history of the parallel root records only its immediate children + (the regions A and K themselves), so re-entry resets each region to initial.""" + machine = make_parallel_history() + s = machine.initial_state + s = machine.transition(s, "SWITCH") # A=B, K=L + s = machine.transition(s, "INNER_A") # A=C.D + assert s.value == {"on": {"A": {"C": "D"}, "K": "L"}} + s = machine.transition(s, "POWER") # off; shallow hist of on records {A, K} + s = machine.transition(s, "POWER") # restore via shallow: enters A→B, K→L + assert s.value == {"on": {"A": "B", "K": "L"}} + + +def test_parallel_deep_history_of_root_restores_full_path(): + """Deep history of the parallel root records the full atomic configuration.""" + machine = make_parallel_history() + s = machine.initial_state + s = machine.transition(s, "SWITCH") # A=B, K=L + s = machine.transition(s, "INNER_A") # A=C.D, K=L + assert s.value == {"on": {"A": {"C": "D"}, "K": "L"}} + s = machine.transition(s, "POWER") # off; deep hist records {C.D, L} + s = machine.transition(s, "DEEP_POWER") # restore: A→C.D, K→L + assert s.value == {"on": {"A": {"C": "D"}, "K": "L"}} + + +def test_parallel_deep_history_restores_both_regions(): + """Deep history of root restores full path in both regions.""" + machine = make_parallel_history() + s = _reach_ACEKMO(machine) + assert s.value == {"on": {"A": {"C": "E"}, "K": {"M": "O"}}} + s = machine.transition(s, "POWER") # off + s = machine.transition(s, "DEEP_POWER") # restore A=C.E, K=M.O + assert s.value == {"on": {"A": {"C": "E"}, "K": {"M": "O"}}} + + +def test_parallel_per_region_shallow_history(): + """PARALLEL_HIST targets A.hist and K.hist simultaneously. + + Shallow history of A records its immediate active child (C when A=C.E), + so re-entry enters C then C.initial=D. Same for K (M→N). + """ + machine = make_parallel_history() + s = _reach_ACEKMO(machine) # A=C.E, K=M.O + s = machine.transition(s, "POWER") # off; A_shallow={C}, K_shallow={M} + s = machine.transition(s, "PARALLEL_HIST") # enter A via A.hist, K via K.hist + assert s.value == {"on": {"A": {"C": "D"}, "K": {"M": "N"}}} + + +def test_parallel_per_region_deep_history(): + """PARALLEL_DEEP targets A.deep_hist and K.deep_hist simultaneously. + + Deep history restores the exact atomic state in each region. + """ + machine = make_parallel_history() + s = _reach_ACEKMO(machine) # A=C.E, K=M.O + s = machine.transition(s, "POWER") # off; A_deep={E}, K_deep={O} + s = machine.transition(s, "PARALLEL_DEEP") + assert s.value == {"on": {"A": {"C": "E"}, "K": {"M": "O"}}} + + +def test_parallel_history_no_recorded_uses_region_initial(): + """When no history has been recorded for a region, re-entry uses that + region's initial state.""" + machine = make_parallel_history() + state = machine.initial_state # off, no history recorded + state = machine.transition(state, "PARALLEL_HIST") + assert state.value == {"on": {"A": "B", "K": "L"}} diff --git a/tests/test_machine.py b/tests/test_machine.py index 095ba84..8a1db62 100644 --- a/tests/test_machine.py +++ b/tests/test_machine.py @@ -1,11 +1,16 @@ from xstate.machine import Machine +from xstate.state import State + lights = Machine( { "id": "lights", "initial": "green", "states": { - "green": {"on": {"TIMER": "yellow"}, "entry": [{"type": "enterGreen"}]}, + "green": { + "on": {"TIMER": "yellow"}, + "entry": [{"type": "enterGreen"}] + }, "yellow": {"on": {"TIMER": "red"}}, "red": { "initial": "walk", @@ -33,6 +38,7 @@ def test_machine(): def test_machine_initial_state(): + state = lights.initial_state assert lights.initial_state.value == "green" @@ -43,68 +49,3 @@ def test_final_state(): red_timeout_state = lights.transition(red_stop_state, "TIMEOUT") assert red_timeout_state.value == "green" - - - -fan = Machine( - { - "id": "fan", - "initial": "fanOff", - "states": { - "fanOff": { - "on": { - "POWER": "#fan.fanOn.hist", - "HIGH_POWER": "fanOn.highPowerHist", - }, - }, - "fanOn": { - "initial": "first", - "states": { - "first": {"on": {"SWITCH": "second"}}, - "second": {"on": {"SWITCH": "third"}}, - "third": {}, - "hist": {"type": "history", "history": "shallow"}, - "highPowerHist": {"type": "history", "target": "third"}, - }, - "on": {"POWER": "fanOff"}, - }, - }, - } -) - - -def test_history_state(): - on_state = fan.transition(fan.initial_state, "POWER") - - assert on_state.value == "fanOn.first" - - on_second_state = fan.transition(on_state, "SWITCH") - - assert on_second_state.value == "fanOn.second" - - off_state = fan.transition(on_second_state, "POWER") - - assert off_state.value == "fanOff" - - on_second_state = fan.transition(off_state, "POWER") - - assert on_second_state.value == "fanOn.second" - - - - -def test_top_level_final(): - final = Machine( - { - "id": "final", - "initial": "start", - "states": { - "start": {"on": {"FINISH": "end"}}, - "end": {"type": "final"}, - }, - } - ) - - end_state = final.transition(final.initial_state, "FINISH") - - assert end_state.value == "end" diff --git a/tests/test_parallel.py b/tests/test_parallel.py new file mode 100644 index 0000000..ca663ea --- /dev/null +++ b/tests/test_parallel.py @@ -0,0 +1,133 @@ +"""Tests for parallel (orthogonal) states (0.2.0).""" + +from xstate import Machine + + +def make_word(): + """A parallel-root machine with two independent formatting regions.""" + return Machine( + { + "id": "word", + "type": "parallel", + "states": { + "bold": { + "initial": "off", + "states": { + "off": {"on": {"TOGGLE_BOLD": "on", "RESET": "off"}}, + "on": {"on": {"TOGGLE_BOLD": "off", "RESET": "off"}}, + }, + }, + "italics": { + "initial": "off", + "states": { + "off": {"on": {"TOGGLE_ITALICS": "on", "RESET": "off"}}, + "on": {"on": {"TOGGLE_ITALICS": "off", "RESET": "off"}}, + }, + }, + }, + } + ) + + +def test_parallel_root_initial_enters_all_regions(): + machine = make_word() + state = machine.initial_state + assert state.value == {"bold": "off", "italics": "off"} + + +def test_parallel_regions_transition_independently(): + machine = make_word() + state = machine.initial_state + state = machine.transition(state, "TOGGLE_BOLD") + assert state.value == {"bold": "on", "italics": "off"} + state = machine.transition(state, "TOGGLE_ITALICS") + assert state.value == {"bold": "on", "italics": "on"} + state = machine.transition(state, "TOGGLE_BOLD") + assert state.value == {"bold": "off", "italics": "on"} + + +def test_event_handled_by_multiple_regions(): + machine = make_word() + state = machine.initial_state + state = machine.transition(state, "TOGGLE_BOLD") + state = machine.transition(state, "TOGGLE_ITALICS") + assert state.value == {"bold": "on", "italics": "on"} + # RESET is handled by both regions at once. + state = machine.transition(state, "RESET") + assert state.value == {"bold": "off", "italics": "off"} + + +def test_nested_parallel_state(): + machine = Machine( + { + "id": "app", + "initial": "editing", + "states": { + "editing": { + "type": "parallel", + "states": { + "mode": { + "initial": "insert", + "states": { + "insert": {"on": {"ESC": "command"}}, + "command": {"on": {"I": "insert"}}, + }, + }, + "saved": { + "initial": "clean", + "states": { + "clean": {"on": {"EDIT": "dirty"}}, + "dirty": {"on": {"SAVE": "clean"}}, + }, + }, + }, + } + }, + } + ) + state = machine.initial_state + assert state.value == {"editing": {"mode": "insert", "saved": "clean"}} + state = machine.transition(state, "ESC") + assert state.value == {"editing": {"mode": "command", "saved": "clean"}} + state = machine.transition(state, "EDIT") + assert state.value == {"editing": {"mode": "command", "saved": "dirty"}} + + +def test_parallel_on_done_when_all_regions_final(): + machine = Machine( + { + "id": "p", + "initial": "running", + "states": { + "running": { + "type": "parallel", + "onDone": "done", + "states": { + "a": { + "initial": "a1", + "states": { + "a1": {"on": {"FINISH_A": "a2"}}, + "a2": {"type": "final"}, + }, + }, + "b": { + "initial": "b1", + "states": { + "b1": {"on": {"FINISH_B": "b2"}}, + "b2": {"type": "final"}, + }, + }, + }, + }, + "done": {"type": "final"}, + }, + } + ) + state = machine.initial_state + assert state.value == {"running": {"a": "a1", "b": "b1"}} + # One region final is not enough. + state = machine.transition(state, "FINISH_A") + assert state.value == {"running": {"a": "a2", "b": "b1"}} + # Both regions final -> done.state.running -> onDone. + state = machine.transition(state, "FINISH_B") + assert state.value == "done" diff --git a/tests/test_real_world.py b/tests/test_real_world.py new file mode 100644 index 0000000..7ea5b9e --- /dev/null +++ b/tests/test_real_world.py @@ -0,0 +1,477 @@ +"""Integration tests: User Session Manager state machine. + +A real-world style authentication + session management machine inspired by +canonical XState v4 patterns (https://xstatebyexample.com/authentication/). + +Features exercised across the full suite: + - Compound states (authenticated → active / idle_session / screen_locked) + - Context + assign (user, error, attempts counter) + - Guards / conds (savedToken, canRetry / lockout threshold, pin check) + - Multi-transition event arrays with priority ordering (FAILURE) + - Targetless guard blocks (wrong-pin UNLOCK — no transition fired) + - Logical and physical separation of context update from guard evaluation + +Machine topology +---------------- + + idle ──LOGIN──► authenticating ──SUCCESS──► authenticated + │ │ + FAILURE[×3] active ◄──RESUME── idle_session + ┌────┴─────┐ │ │ + lockedOut authError TIMEOUT LONG_TIMEOUT + │ │ │ │ + ADMIN_UNLOCK RETRY idle_session screen_locked + │ │ │ + idle authenticating UNLOCK(pin) ──► active + (wrong pin: no-op) + + authenticated also handles: + LOGOUT → idle + SESSION_EXPIRED → idle +""" + +import pytest +from xstate import Machine, assign + +MAX_ATTEMPTS = 3 + + +# --------------------------------------------------------------------------- +# Factory helpers +# --------------------------------------------------------------------------- + + +def make_session_machine(saved_token: bool = False): + """Build the session machine. saved_token controls AUTO_LOGIN eligibility.""" + return Machine( + { + "id": "session", + "initial": "idle", + "context": { + "user": None, + "error": None, + "attempts": 0, + "savedToken": saved_token, + }, + "states": { + # ---- unauthenticated states -------------------------------- + "idle": { + "on": { + "LOGIN": "authenticating", + "AUTO_LOGIN": { + "target": "authenticated", + "cond": lambda ctx, _: ctx.get("savedToken", False), + }, + } + }, + "authenticating": { + "on": { + "SUCCESS": { + "target": "authenticated", + "actions": [ + assign( + { + "user": lambda ctx, ev: ev.data.get("user"), + "error": None, + "attempts": 0, + } + ) + ], + }, + # Array: first matching guard wins. + # Guard is evaluated against context BEFORE assigns run. + "FAILURE": [ + { + "target": "lockedOut", + "cond": lambda ctx, _: ( + ctx.get("attempts", 0) + 1 >= MAX_ATTEMPTS + ), + "actions": [ + assign( + { + "attempts": lambda ctx, _: ctx.get( + "attempts", 0 + ) + + 1, + "error": "Account locked after too many attempts", + } + ) + ], + }, + { + "target": "authError", + "actions": [ + assign( + { + "attempts": lambda ctx, _: ctx.get( + "attempts", 0 + ) + + 1, + "error": "Invalid credentials", + } + ) + ], + }, + ], + } + }, + # ---- authenticated compound state ------------------------- + "authenticated": { + "initial": "active", + "on": { + "LOGOUT": { + "target": "idle", + "actions": [ + assign({"user": None, "error": None, "attempts": 0}) + ], + }, + "SESSION_EXPIRED": "idle", + }, + "states": { + "active": { + "on": { + "GO_IDLE": "idle_session", + "TIMEOUT": "idle_session", + } + }, + "idle_session": { + "on": { + "RESUME": "active", + "LONG_TIMEOUT": "screen_locked", + } + }, + "screen_locked": { + "on": { + # Correct PIN → back to active; wrong PIN → no-op (guard fails) + "UNLOCK": [ + { + "target": "active", + "cond": lambda ctx, ev: ev.data.get("pin") + == "1234", + } + ] + } + }, + }, + }, + # ---- error / lockout states ------------------------------- + "authError": { + "on": { + "RETRY": "authenticating", + "RESET": { + "target": "idle", + "actions": [ + assign({"attempts": 0, "error": None}) + ], + }, + } + }, + "lockedOut": { + "on": { + "ADMIN_UNLOCK": { + "target": "idle", + "actions": [ + assign({"attempts": 0, "error": None}) + ], + } + } + }, + }, + } + ) + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _login_succeed(machine, user="alice"): + """Shortcut: idle → authenticating → authenticated.active.""" + state = machine.initial_state + state = machine.transition(state, "LOGIN") + state = machine.transition(state, {"type": "SUCCESS", "user": user}) + return state + + +def _fail_once(machine, state): + """Fire a single FAILURE event and return the new state.""" + return machine.transition(state, "FAILURE") + + +# --------------------------------------------------------------------------- +# Basic state identity +# --------------------------------------------------------------------------- + + +def test_initial_state_is_idle(): + machine = make_session_machine() + assert machine.initial_state.value == "idle" + + +def test_initial_context(): + machine = make_session_machine() + ctx = machine.initial_state.context + assert ctx["user"] is None + assert ctx["error"] is None + assert ctx["attempts"] == 0 + + +# --------------------------------------------------------------------------- +# Happy-path login flow +# --------------------------------------------------------------------------- + + +def test_login_reaches_authenticating(): + machine = make_session_machine() + state = machine.initial_state + state = machine.transition(state, "LOGIN") + assert state.value == "authenticating" + + +def test_success_enters_authenticated_active(): + machine = make_session_machine() + state = _login_succeed(machine) + assert state.value == {"authenticated": "active"} + + +def test_success_sets_user_in_context(): + machine = make_session_machine() + state = _login_succeed(machine, user="alice") + assert state.context["user"] == "alice" + + +def test_success_clears_error_and_attempts(): + """After a successful login, error and attempts are reset even if there + had been prior failures.""" + machine = make_session_machine() + state = machine.initial_state + state = machine.transition(state, "LOGIN") + state = _fail_once(machine, state) # attempts → 1, state → authError + state = machine.transition(state, "RETRY") # back to authenticating + state = machine.transition(state, {"type": "SUCCESS", "user": "bob"}) + assert state.context["attempts"] == 0 + assert state.context["error"] is None + assert state.context["user"] == "bob" + + +# --------------------------------------------------------------------------- +# Failure / retry flow +# --------------------------------------------------------------------------- + + +def test_first_failure_goes_to_auth_error(): + machine = make_session_machine() + state = machine.initial_state + state = machine.transition(state, "LOGIN") + state = _fail_once(machine, state) + assert state.value == "authError" + + +def test_failure_increments_attempts(): + machine = make_session_machine() + state = machine.initial_state + state = machine.transition(state, "LOGIN") + state = _fail_once(machine, state) + assert state.context["attempts"] == 1 + assert state.context["error"] == "Invalid credentials" + + +def test_retry_returns_to_authenticating(): + machine = make_session_machine() + state = machine.initial_state + state = machine.transition(state, "LOGIN") + state = _fail_once(machine, state) + state = machine.transition(state, "RETRY") + assert state.value == "authenticating" + + +def test_reset_from_auth_error_clears_context(): + machine = make_session_machine() + state = machine.initial_state + state = machine.transition(state, "LOGIN") + state = _fail_once(machine, state) + assert state.context["attempts"] == 1 + state = machine.transition(state, "RESET") + assert state.value == "idle" + assert state.context["attempts"] == 0 + assert state.context["error"] is None + + +# --------------------------------------------------------------------------- +# Lockout after MAX_ATTEMPTS failures +# --------------------------------------------------------------------------- + + +def _exhaust_attempts(machine): + """Drive the machine to lockedOut via MAX_ATTEMPTS failures.""" + state = machine.initial_state + state = machine.transition(state, "LOGIN") + for _ in range(MAX_ATTEMPTS - 1): + state = _fail_once(machine, state) # → authError + state = machine.transition(state, "RETRY") # → authenticating + state = _fail_once(machine, state) # final failure → lockedOut + return state + + +def test_lockout_after_max_attempts(): + machine = make_session_machine() + state = _exhaust_attempts(machine) + assert state.value == "lockedOut" + + +def test_lockout_sets_error_and_attempts(): + machine = make_session_machine() + state = _exhaust_attempts(machine) + assert state.context["attempts"] == MAX_ATTEMPTS + assert "locked" in state.context["error"].lower() + + +def test_admin_unlock_resets_to_idle(): + machine = make_session_machine() + state = _exhaust_attempts(machine) + state = machine.transition(state, "ADMIN_UNLOCK") + assert state.value == "idle" + assert state.context["attempts"] == 0 + assert state.context["error"] is None + + +# --------------------------------------------------------------------------- +# AUTO_LOGIN guard +# --------------------------------------------------------------------------- + + +def test_auto_login_blocked_without_saved_token(): + """AUTO_LOGIN is a no-op when savedToken is False.""" + machine = make_session_machine(saved_token=False) + state = machine.initial_state + state = machine.transition(state, "AUTO_LOGIN") + assert state.value == "idle" + + +def test_auto_login_fires_with_saved_token(): + """AUTO_LOGIN skips authenticating when savedToken is True.""" + machine = make_session_machine(saved_token=True) + state = machine.initial_state + state = machine.transition(state, "AUTO_LOGIN") + assert state.value == {"authenticated": "active"} + + +# --------------------------------------------------------------------------- +# Session lifecycle inside `authenticated` +# --------------------------------------------------------------------------- + + +def test_logout_from_active_returns_to_idle(): + machine = make_session_machine() + state = _login_succeed(machine) + state = machine.transition(state, "LOGOUT") + assert state.value == "idle" + + +def test_logout_clears_user(): + machine = make_session_machine() + state = _login_succeed(machine, user="alice") + assert state.context["user"] == "alice" + state = machine.transition(state, "LOGOUT") + assert state.context["user"] is None + + +def test_session_expired_returns_to_idle(): + machine = make_session_machine() + state = _login_succeed(machine) + state = machine.transition(state, "SESSION_EXPIRED") + assert state.value == "idle" + + +def test_timeout_enters_idle_session(): + machine = make_session_machine() + state = _login_succeed(machine) + state = machine.transition(state, "TIMEOUT") + assert state.value == {"authenticated": "idle_session"} + + +def test_go_idle_enters_idle_session(): + machine = make_session_machine() + state = _login_succeed(machine) + state = machine.transition(state, "GO_IDLE") + assert state.value == {"authenticated": "idle_session"} + + +def test_resume_from_idle_session(): + machine = make_session_machine() + state = _login_succeed(machine) + state = machine.transition(state, "TIMEOUT") + state = machine.transition(state, "RESUME") + assert state.value == {"authenticated": "active"} + + +def test_long_timeout_enters_screen_locked(): + machine = make_session_machine() + state = _login_succeed(machine) + state = machine.transition(state, "GO_IDLE") + state = machine.transition(state, "LONG_TIMEOUT") + assert state.value == {"authenticated": "screen_locked"} + + +# --------------------------------------------------------------------------- +# Screen-lock PIN guard +# --------------------------------------------------------------------------- + + +def test_unlock_correct_pin_returns_to_active(): + machine = make_session_machine() + state = _login_succeed(machine) + state = machine.transition(state, "GO_IDLE") + state = machine.transition(state, "LONG_TIMEOUT") + assert state.value == {"authenticated": "screen_locked"} + state = machine.transition(state, {"type": "UNLOCK", "pin": "1234"}) + assert state.value == {"authenticated": "active"} + + +def test_unlock_wrong_pin_stays_locked(): + """Guard fails on wrong PIN — machine stays in screen_locked.""" + machine = make_session_machine() + state = _login_succeed(machine) + state = machine.transition(state, "GO_IDLE") + state = machine.transition(state, "LONG_TIMEOUT") + state = machine.transition(state, {"type": "UNLOCK", "pin": "0000"}) + assert state.value == {"authenticated": "screen_locked"} + + +def test_unlock_wrong_pin_then_correct_pin(): + """Multiple wrong PINs followed by the correct one should succeed.""" + machine = make_session_machine() + state = _login_succeed(machine) + state = machine.transition(state, "GO_IDLE") + state = machine.transition(state, "LONG_TIMEOUT") + # Two wrong attempts + state = machine.transition(state, {"type": "UNLOCK", "pin": "9999"}) + state = machine.transition(state, {"type": "UNLOCK", "pin": "1111"}) + assert state.value == {"authenticated": "screen_locked"} + # Correct PIN + state = machine.transition(state, {"type": "UNLOCK", "pin": "1234"}) + assert state.value == {"authenticated": "active"} + + +# --------------------------------------------------------------------------- +# Context is preserved across nested transitions +# --------------------------------------------------------------------------- + + +def test_user_preserved_across_session_idle_and_resume(): + """User in context is unaffected by activity-timeout and resume cycle.""" + machine = make_session_machine() + state = _login_succeed(machine, user="carol") + state = machine.transition(state, "TIMEOUT") + state = machine.transition(state, "RESUME") + assert state.context["user"] == "carol" + + +def test_user_preserved_through_screen_lock_unlock(): + machine = make_session_machine() + state = _login_succeed(machine, user="dave") + state = machine.transition(state, "GO_IDLE") + state = machine.transition(state, "LONG_TIMEOUT") + state = machine.transition(state, {"type": "UNLOCK", "pin": "1234"}) + assert state.context["user"] == "dave" diff --git a/tests/test_regressions.py b/tests/test_regressions.py new file mode 100644 index 0000000..343e5e2 --- /dev/null +++ b/tests/test_regressions.py @@ -0,0 +1,183 @@ +"""Regression tests for engine bugs found in code review of the 0.2.0 work. + +Each test here corresponds to a specific defect fixed after the PR #49 +test-corpus port. They are intentionally minimal and named after the defect so +a future regression points straight at the cause. +""" + +from xstate import Machine +from xstate.algorithm import get_proper_ancestors, get_child_states, is_history_state + +# --- Fix #1: get_proper_ancestors must EXCLUDE state2 (the upper bound) ------- +# Per W3C SCXML, getProperAncestors(s, ancestor) returns ancestors up to but not +# including `ancestor`. A regression here re-fires entry actions for the +# transition domain on every internal transition. + + +def test_get_proper_ancestors_excludes_upper_bound(): + machine = Machine( + { + "id": "r", + "initial": "p", + "states": {"p": {"initial": "c", "states": {"c": {}}}}, + } + ) + p = machine.states["p"] + c = p.states["c"] + assert get_proper_ancestors(c, state2=p) == [] + # With no bound, all ancestors up to the root are returned. + keys = [a.key for a in get_proper_ancestors(c, state2=None)] + assert "p" in keys + + +# --- Fix #2: every parallel region advances via its own eventless transition -- +# The old global `loop` flag stopped after the first region matched. + + +def test_parallel_regions_each_take_eventless_transition(): + machine = Machine( + { + "id": "p", + "initial": "run", + "states": { + "run": { + "type": "parallel", + "states": { + "a": { + "initial": "a1", + "states": {"a1": {"on": {"": "a2"}}, "a2": {}}, + }, + "b": { + "initial": "b1", + "states": {"b1": {"on": {"": "b2"}}, "b2": {}}, + }, + }, + } + }, + } + ) + state = machine.initial_state + assert state.value == {"run": {"a": "a2", "b": "b2"}} + + +# --- Fix #3: get_child_states excludes history pseudo-states ------------------- +# A history child must not count as a (non-final) region, which would otherwise +# permanently block a parallel state's onDone. + + +def test_get_child_states_excludes_history(): + machine = Machine( + { + "id": "m", + "initial": "on", + "states": { + "on": { + "initial": "a", + "states": {"a": {}, "h": {"type": "history", "id": "h"}}, + } + }, + } + ) + on = machine.states["on"] + children = get_child_states(on) + assert all(not is_history_state(c) for c in children) + assert [c.key for c in children] == ["a"] + + +def test_parallel_on_done_fires_with_history_child_present(): + machine = Machine( + { + "id": "p", + "initial": "running", + "states": { + "running": { + "type": "parallel", + "onDone": "done", + "states": { + "a": { + "initial": "a1", + "states": { + "a1": {"on": {"FA": "a2"}}, + "a2": {"type": "final"}, + "ahist": {"type": "history", "id": "ahist"}, + }, + }, + "b": { + "initial": "b1", + "states": { + "b1": {"on": {"FB": "b2"}}, + "b2": {"type": "final"}, + }, + }, + }, + }, + "done": {"type": "final"}, + }, + } + ) + state = machine.initial_state + state = machine.transition(state, "FA") + state = machine.transition(state, "FB") + assert state.value == "done" + + +# --- Fix #4: a dict transition without "target" is an internal self-loop ------- + + +def test_targetless_dict_transition_does_not_crash(): + machine = Machine( + { + "id": "m", + "initial": "idle", + "states": {"idle": {"on": {"PING": {"cond": lambda c, e: True}}}}, + } + ) + state = machine.initial_state + state = machine.transition(state, "PING") + assert state.value == "idle" + + +# --- Fix #5 (with #1): deep history restores a 3-level-deep nested path -------- +# Requires both get_proper_ancestors excluding state2 AND ancestor=state.parent +# so every intermediate ancestor between the restored atomic state and the +# history node's parent is entered. + + +def test_deep_history_restores_three_level_nested_path(): + machine = Machine( + { + "id": "d", + "initial": "off", + "states": { + "off": {"on": {"POWER": "#dhist"}}, + "on": { + "initial": "a", + "on": {"POWER": "off"}, + "states": { + "a": { + "initial": "a1", + "states": { + "a1": {"on": {"NEXT": "a2"}}, + "a2": { + "initial": "x", + "states": {"x": {}}, + }, + }, + }, + "dhist": { + "type": "history", + "history": "deep", + "id": "dhist", + }, + }, + }, + }, + } + ) + state = machine.initial_state + state = machine.transition(state, "POWER") # on.a.a1 + state = machine.transition(state, "NEXT") # on.a.a2.x + assert state.value == {"on": {"a": {"a2": "x"}}} + state = machine.transition(state, "POWER") # off (deep history: x) + state = machine.transition(state, "POWER") # restore -> on.a.a2.x + assert state.value == {"on": {"a": {"a2": "x"}}} diff --git a/tests/test_scxml.py b/tests/test_scxml.py index 8e85b21..da674d9 100644 --- a/tests/test_scxml.py +++ b/tests/test_scxml.py @@ -1,10 +1,9 @@ +import xml.etree.ElementTree as ET +from typing import Optional, Dict, List import json -from pprint import PrettyPrinter -from typing import Dict, List - import pytest - from xstate.scxml import scxml_to_machine +from pprint import PrettyPrinter pp = PrettyPrinter(indent=2) @@ -31,6 +30,36 @@ "hierarchy": ["hier0", "hier1", "hier2"], "hierarchy+documentOrder": ["test0", "test1"], "parallel": ["test0", "test1", "test2", "test3"], + "parallel+interrupt": [ + "test0", + "test1", + "test2", + "test3", + "test4", + "test5", + "test6", + "test7", + "test8", + "test9", + "test10", + ], + "more-parallel": [ + "test0", + "test1", + "test2", + "test2b", + "test3", + "test3b", + "test4", + "test5", + "test6", + "test6b", + "test7", + "test8", + "test9", + # "test10", # TODO: needs assign() + # "test10b", # TODO: needs assign() + ], } test_files = [ @@ -63,6 +92,6 @@ def test_scxml(scxml_source, scxml_test_source): assert sorted( [sn.key for sn in state.configuration if sn.type == "atomic"] ) == sorted(next_configuration) - except Exception: + except: pp.pprint(machine.config) raise diff --git a/tests/test_state_in.py b/tests/test_state_in.py new file mode 100644 index 0000000..8b052f5 --- /dev/null +++ b/tests/test_state_in.py @@ -0,0 +1,273 @@ +"""Tests for `in`-state guards on transitions (0.2.0). + +An `in` guard on a transition makes it conditional on the machine currently +being in a specific (sub)state. Three matching forms are supported: + + * dict ``{"b": "b2"}`` — parent.key == "b" and child.key == "b2" + * string ``"b.b2"`` — same, dotted path + * string ``"#id"`` — any active state with that id + +Based on the XState v4 statein.test.ts describe block. +""" + +import pytest +from xstate import Machine + +# --------------------------------------------------------------------------- +# Machine: parallel root with two regions that have cross-region `in` guards +# --------------------------------------------------------------------------- +# +# Region A (initial a1): +# a1 --EVENT_DICT--> a2 only if b is in b2 (dict form) +# a1 --EVENT_STR--> a2 only if b is in b2 (dotted-string form) +# a1 --EVENT_ID--> a2 only if b is in b2 (#id form) +# +# Region B (initial b1): +# b1 --GO_B2--> b2 (unconditional, used to set up state) + + +def make_cross_region(): + return Machine( + { + "id": "cross", + "type": "parallel", + "states": { + "a": { + "initial": "a1", + "states": { + "a1": { + "on": { + "EVENT_DICT": {"target": "a2", "in": {"b": "b2"}}, + "EVENT_STR": {"target": "a2", "in": "b.b2"}, + "EVENT_ID": { + "target": "a2", + "in": "#cross.b.b2", + }, + } + }, + "a2": {}, + }, + }, + "b": { + "initial": "b1", + "states": { + "b1": {"on": {"GO_B2": "b2"}}, + "b2": {}, + }, + }, + }, + } + ) + + +# -- dict form --------------------------------------------------------------- + + +def test_in_dict_fires_when_condition_met(): + """Transition fires when the `in` dict condition is satisfied.""" + machine = make_cross_region() + state = machine.initial_state + assert state.value == {"a": "a1", "b": "b1"} + state = machine.transition(state, "GO_B2") # b → b2 + assert state.value == {"a": "a1", "b": "b2"} + state = machine.transition(state, "EVENT_DICT") # a1→a2 because b==b2 + assert state.value == {"a": "a2", "b": "b2"} + + +def test_in_dict_blocks_when_condition_not_met(): + """Transition is blocked when the `in` dict condition is not satisfied.""" + machine = make_cross_region() + state = machine.initial_state # a=a1, b=b1 + state = machine.transition(state, "EVENT_DICT") # b≠b2 → no-op + assert state.value == {"a": "a1", "b": "b1"} + + +# -- dotted-string form ------------------------------------------------------- + + +def test_in_str_fires_when_condition_met(): + """Transition fires when the `in` dotted-string condition is satisfied.""" + machine = make_cross_region() + state = machine.initial_state + state = machine.transition(state, "GO_B2") + state = machine.transition(state, "EVENT_STR") + assert state.value == {"a": "a2", "b": "b2"} + + +def test_in_str_blocks_when_condition_not_met(): + """Transition is blocked when the `in` dotted-string condition fails.""" + machine = make_cross_region() + state = machine.initial_state + state = machine.transition(state, "EVENT_STR") + assert state.value == {"a": "a1", "b": "b1"} + + +# -- #id form ---------------------------------------------------------------- + + +def test_in_id_fires_when_condition_met(): + """Transition fires when the `in` #id condition is satisfied.""" + machine = make_cross_region() + state = machine.initial_state + state = machine.transition(state, "GO_B2") + state = machine.transition(state, "EVENT_ID") + assert state.value == {"a": "a2", "b": "b2"} + + +def test_in_id_blocks_when_condition_not_met(): + """Transition is blocked when the `in` #id condition fails.""" + machine = make_cross_region() + state = machine.initial_state + state = machine.transition(state, "EVENT_ID") + assert state.value == {"a": "a1", "b": "b1"} + + +# --------------------------------------------------------------------------- +# Machine: compound state with `in` guard restricting compound-child access +# --------------------------------------------------------------------------- +# +# red has substates walk/wait/stop. +# red --TIMER--> green ONLY if red is in "stop" (in: {red: "stop"}) +# +# This is the classic traffic-light "forbid early green" example from XState docs. + + +def make_traffic_light(): + return Machine( + { + "id": "light", + "initial": "green", + "states": { + "green": {"on": {"TIMER": "yellow"}}, + "yellow": {"on": {"TIMER": "red"}}, + "red": { + "initial": "walk", + "states": { + "walk": {"on": {"STEP": "wait"}}, + "wait": {"on": {"STEP": "stop"}}, + "stop": {}, + }, + "on": { + "TIMER": [ + {"target": "green", "in": {"red": "stop"}}, + ] + }, + }, + }, + } + ) + + +def test_traffic_light_blocked_at_walk(): + """TIMER from red.walk is blocked — 'in {red: stop}' is not satisfied.""" + machine = make_traffic_light() + state = machine.initial_state + state = machine.transition(state, "TIMER") # green → yellow + state = machine.transition(state, "TIMER") # yellow → red (walk) + assert state.value == {"red": "walk"} + state = machine.transition(state, "TIMER") # no-op (in: red.stop not met) + assert state.value == {"red": "walk"} + + +def test_traffic_light_blocked_at_wait(): + """TIMER from red.wait is also blocked.""" + machine = make_traffic_light() + state = machine.initial_state + state = machine.transition(state, "TIMER") # → yellow + state = machine.transition(state, "TIMER") # → red.walk + state = machine.transition(state, "STEP") # → red.wait + assert state.value == {"red": "wait"} + state = machine.transition(state, "TIMER") # no-op + assert state.value == {"red": "wait"} + + +def test_traffic_light_fires_from_stop(): + """TIMER from red.stop fires because 'in {red: stop}' IS satisfied.""" + machine = make_traffic_light() + state = machine.initial_state + state = machine.transition(state, "TIMER") # → yellow + state = machine.transition(state, "TIMER") # → red.walk + state = machine.transition(state, "STEP") # → red.wait + state = machine.transition(state, "STEP") # → red.stop + assert state.value == {"red": "stop"} + state = machine.transition(state, "TIMER") # guard satisfied → green + assert state.value == "green" + + +# --------------------------------------------------------------------------- +# `in` combined with `cond` +# --------------------------------------------------------------------------- +# +# Both the `in` guard AND the `cond` must be true for the transition to fire. + + +def make_combined(): + return Machine( + { + "id": "combined", + "type": "parallel", + "states": { + "a": { + "initial": "a1", + "states": { + "a1": { + "on": { + "GO": { + "target": "a2", + "in": {"b": "b2"}, + "cond": lambda ctx, _: ctx.get("allowed", False), + } + } + }, + "a2": {}, + }, + }, + "b": { + "initial": "b1", + "states": { + "b1": {"on": {"GO_B2": "b2"}}, + "b2": {}, + }, + }, + }, + }, + context={"allowed": False}, + ) + + +def test_combined_both_must_pass(): + """Transition fires only when both `in` and `cond` are satisfied.""" + machine = Machine( + { + "id": "combo", + "type": "parallel", + "states": { + "a": { + "initial": "a1", + "states": { + "a1": { + "on": { + "GO": { + "target": "a2", + "in": {"b": "b2"}, + "cond": lambda ctx, _: ctx.get("ok", False), + } + } + }, + "a2": {}, + }, + }, + "b": { + "initial": "b1", + "states": {"b1": {"on": {"GO_B2": "b2"}}, "b2": {}}, + }, + }, + "context": {"ok": True}, + } + ) + state = machine.initial_state # a=a1, b=b1; ctx.ok=True + state = machine.transition(state, "GO") # in fails (b≠b2) + assert state.value == {"a": "a1", "b": "b1"} + state = machine.transition(state, "GO_B2") # b→b2 + state = machine.transition(state, "GO") # in ✓, cond ✓ → a2 + assert state.value == {"a": "a2", "b": "b2"} diff --git a/xstate/__init__.py b/xstate/__init__.py index fbaa702..086bd74 100644 --- a/xstate/__init__.py +++ b/xstate/__init__.py @@ -1 +1,2 @@ -from xstate.machine import Machine # noqa +from xstate.machine import Machine # noqa +from xstate.action import assign # noqa diff --git a/xstate/action.py b/xstate/action.py index 6d6e5f4..c8edb9e 100644 --- a/xstate/action.py +++ b/xstate/action.py @@ -1,4 +1,7 @@ -from typing import Any, Callable, Dict, Optional +from typing import Callable, Optional, Dict, Any + +# Action type markers used internally by the engine. +ASSIGN_TYPE = "xstate.assign" def not_implemented(): @@ -22,3 +25,23 @@ def __init__( def __repr__(self): return repr({"type": self.type}) + + +def assign(assignment): + """Create an ``assign`` action that updates a machine's ``context``. + + ``assignment`` is either: + + - a callable ``(context, event) -> dict`` returning the updates to merge, or + - a dict mapping context keys to either a static value or a callable + ``(context, event) -> value``. + + Example:: + + from xstate import assign + + increment = assign({"count": lambda ctx, ev: ctx["count"] + 1}) + reset = assign({"count": 0}) + merge = assign(lambda ctx, ev: {"count": ctx["count"] + ev.data["by"]}) + """ + return {"type": ASSIGN_TYPE, "assignment": assignment} diff --git a/xstate/algorithm.py b/xstate/algorithm.py index 67af1d9..db5eb2e 100644 --- a/xstate/algorithm.py +++ b/xstate/algorithm.py @@ -1,13 +1,53 @@ -from typing import Dict, List, Optional, Set, Tuple, Union - -from xstate.action import Action -from xstate.event import Event -from xstate.state_node import StateNode +from __future__ import annotations +import inspect +from typing import Any, List, Set, Dict, Optional, Tuple, Union, TYPE_CHECKING from xstate.transition import Transition +from xstate.state_node import StateNode +from xstate.action import Action, ASSIGN_TYPE +from xstate.event import Event HistoryValue = Dict[str, Set[StateNode]] +def _invoke(fn, context: Optional[Dict], event: Optional[Event]) -> Any: + """Call ``fn`` with as many of ``(context, event)`` as its signature accepts. + + This lets guards and ``assign`` expressions be written as ``() ->``, + ``(context) ->`` or ``(context, event) ->`` (and keeps SCXML's zero-arg + JavaScript conditions working) without callers needing to know the arity. + """ + try: + params = list(inspect.signature(fn).parameters.values()) + except (TypeError, ValueError): + return fn(context, event) + + if any(p.kind == p.VAR_POSITIONAL for p in params): + return fn(context, event) + + positional = [ + p for p in params if p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD) + ] + if len(positional) == 0: + return fn() + if len(positional) == 1: + return fn(context) + return fn(context, event) + + +def _apply_assignment(action: Action, context: Optional[Dict], event: Optional[Event]): + """Mutate ``context`` in place with the updates produced by an assign action.""" + if context is None: + return + assignment = action.data.get("assignment", {}) + if callable(assignment): + updates = _invoke(assignment, context, event) or {} + else: + updates = {} + for key, value in assignment.items(): + updates[key] = _invoke(value, context, event) if callable(value) else value + context.update(updates) + + def compute_entry_set( transitions: List[Transition], states_to_enter: Set[StateNode], @@ -36,7 +76,7 @@ def compute_entry_set( ) -def add_descendent_states_to_enter( # noqa C901 too complex. TODO: simplify function +def add_descendent_states_to_enter( state: StateNode, states_to_enter: Set[StateNode], states_for_default_entry: Set[StateNode], @@ -54,33 +94,43 @@ def add_descendent_states_to_enter( # noqa C901 too complex. TODO: simplify fun history_value=history_value, ) for s in history_value.get(state.id): + # Per SCXML the ancestor bound is the history node's parent, not + # each restored state's parent. For deep history `s` can be a + # nested atomic descendant, so using `s.parent` would drop the + # intermediate ancestors between `s` and `state.parent`. add_ancestor_states_to_enter( s, - ancestor=s.parent, + ancestor=state.parent, states_to_enter=states_to_enter, states_for_default_entry=states_for_default_entry, default_history_content=default_history_content, history_value=history_value, ) else: - default_history_content[state.parent.id] = state.transition.content - # for s in state.transition.target: - # add_descendent_states_to_enter( - # s, - # states_to_enter=states_to_enter, - # states_for_default_entry=states_for_default_entry, - # default_history_content=default_history_content, - # history_value=history_value, - # ) - # for s in state.transition.target: - # add_ancestor_states_to_enter( - # s, - # ancestor=s.parent, - # states_to_enter=states_to_enter, - # states_for_default_entry=states_for_default_entry, - # default_history_content=default_history_content, - # history_value=history_value, - # ) + # No history recorded yet: enter the history state's default target + # (or, if none is configured, fall back to the parent's initial). + default_transition = state.transition or state.parent.initial + default_history_content[state.parent.id] = default_transition + for s in default_transition.target: + add_descendent_states_to_enter( + s, + states_to_enter=states_to_enter, + states_for_default_entry=states_for_default_entry, + default_history_content=default_history_content, + history_value=history_value, + ) + for s in default_transition.target: + # Ancestor bound is the history node's parent (see note above): + # a deeply-nested default target must still enter every ancestor + # up to `state.parent`. + add_ancestor_states_to_enter( + s, + ancestor=state.parent, + states_to_enter=states_to_enter, + states_for_default_entry=states_for_default_entry, + default_history_content=default_history_content, + history_value=history_value, + ) else: states_to_enter.add(state) if is_compound_state(state): @@ -138,9 +188,17 @@ def is_descendent(state: StateNode, state2: StateNode) -> bool: return marker.parent == state2 +# function getTransitionDomain(t) +# tstates = getEffectiveTargetStates(t) +# if not tstates: +# return null +# elif t.type == "internal" and isCompoundState(t.source) and tstates.every(lambda s: isDescendant(s,t.source)): +# return t.source +# else: +# return findLCCA([t.source].append(tstates)) def get_transition_domain( transition: Transition, history_value: HistoryValue -) -> StateNode: +) -> Optional[StateNode]: tstates = get_effective_target_states(transition, history_value=history_value) if not tstates: return None @@ -170,9 +228,12 @@ def get_effective_target_states( if history_value.get(s.id): targets.update(history_value.get(s.id)) else: + # No recorded history: resolve the default target, falling back + # to the parent's initial transition when none is configured. + default_transition = s.transition or s.parent.initial targets.update( get_effective_target_states( - s.transition, history_value=history_value + default_transition, history_value=history_value ) ) else: @@ -181,6 +242,13 @@ def get_effective_target_states( return targets +# procedure addAncestorStatesToEnter(state, ancestor, statesToEnter, statesForDefaultEntry, defaultHistoryContent) +# for anc in getProperAncestors(state,ancestor): +# statesToEnter.add(anc) +# if isParallelState(anc): +# for child in getChildStates(anc): +# if not statesToEnter.some(lambda s: isDescendant(s,child)): +# addDescendantStatesToEnter(child,statesToEnter,statesForDefaultEntry, defaultHistoryContent) def add_ancestor_states_to_enter( state: StateNode, ancestor: StateNode, @@ -206,6 +274,10 @@ def add_ancestor_states_to_enter( def get_proper_ancestors( state1: StateNode, state2: Optional[StateNode] ) -> List[StateNode]: + # Per W3C SCXML getProperAncestors: return the ancestors of state1 in + # ancestry order, up to *but not including* state2 (state2 is the exclusive + # upper bound / transition domain). When state2 is None, return all + # ancestors up to the root. ancestors: List[StateNode] = [] marker = state1.parent while marker and marker != state2: @@ -219,16 +291,17 @@ def is_final_state(state_node: StateNode) -> bool: return state_node.type == "final" -def is_parallel_state(state_node: StateNode) -> bool: - # should return whether state_node.type is parallel - if state_node.type == "parallel": - return True - else: - return False +def is_parallel_state(state_node: Optional[StateNode]) -> bool: + # A null node (e.g. the root's absent parent) is never parallel. + return state_node is not None and state_node.type == "parallel" def get_child_states(state_node: StateNode) -> List[StateNode]: - return [state_node.states.get(key) for key in state_node.states.keys()] + # Per W3C SCXML, getChildStates returns the real // + # children and explicitly excludes (and ) pseudo-states. + # Excluding history here keeps every region-enumeration caller correct: the + # parallel entry fan-out, is_in_final_state, and the parallel onDone check. + return [s for s in state_node.states.values() if not is_history_state(s)] def is_in_final_state(state: StateNode, configuration: Set[StateNode]) -> bool: @@ -252,6 +325,8 @@ def enter_states( history_value: HistoryValue, actions: List[Action], internal_queue: List[Event], + context: Optional[Dict] = None, + event: Optional[Event] = None, ) -> Tuple[Set[StateNode], List[Action], List[Event]]: states_to_enter: Set[StateNode] = set() states_for_default_entry: Set[StateNode] = set() @@ -277,7 +352,13 @@ def enter_states( # TODO: sort for action in s.entry: - execute_content(action, actions=actions, internal_queue=internal_queue) + execute_content( + action, + actions=actions, + internal_queue=internal_queue, + context=context, + event=event, + ) if s in states_for_default_entry: # executeContent(s.initial.transition) continue @@ -289,7 +370,7 @@ def enter_states( grandparent = parent.parent internal_queue.append(Event(f"done.state.{parent.id}", s.donedata)) - if grandparent and is_parallel_state(grandparent): + if is_parallel_state(grandparent): if all( is_in_final_state(parent_state, configuration) for parent_state in get_child_states(grandparent) @@ -306,18 +387,39 @@ def exit_states( history_value: HistoryValue, actions: List[Action], internal_queue: List[Event], + context: Optional[Dict] = None, + event: Optional[Event] = None, ): states_to_exit = compute_exit_set( enabled_transitions, configuration=configuration, history_value=history_value ) for s in states_to_exit: states_to_invoke.discard(s) - # statesToExit = statesToExit.toList().sort(exitOrder) - # for s in states_to_exit: - # for h in s.history + + # Record history before exiting: for each history child of a state being + # exited, snapshot the part of the current configuration it should restore. + # "deep" remembers the full atomic descendant path; "shallow" remembers only + # the state's immediate active children. + for s in states_to_exit: + for h in s.history_states: + if h.history == "deep": + history_value[h.id] = { + s0 + for s0 in configuration + if is_atomic_state(s0) and is_descendent(s0, state2=s) + } + else: + history_value[h.id] = {s0 for s0 in configuration if s0.parent == s} + for s in states_to_exit: for action in s.exit: - execute_content(action, actions=actions, internal_queue=internal_queue) + execute_content( + action, + actions=actions, + internal_queue=internal_queue, + context=context, + event=event, + ) # for inv in s.invoke: # cancelInvoke(inv) configuration.remove(s) @@ -348,11 +450,86 @@ def name_match(event: str, specific_event: str) -> bool: return event == specific_event -def condition_match(transition: Transition) -> bool: - return transition.cond() if transition.cond else True +def _matches_in_state(in_spec, configuration: Set[StateNode]) -> bool: + """Return True if ``in_spec`` matches the current configuration. + + ``in_spec`` is the value of an XState ``in`` transition guard: + - str ``"#id"`` — true if a state with that id is active + - str ``"a.b"`` — true if the state keyed "b" whose parent is keyed "a" is active + - dict ``{k: v}`` — true if all k/v pairs are satisfied (each a one-level path) + """ + if isinstance(in_spec, str): + if in_spec.startswith("#"): + target_id = in_spec[1:] + return any(s.id == target_id for s in configuration) + parts = in_spec.split(".") + if len(parts) == 1: + return any(s.key == parts[0] for s in configuration) + + # Walk parts as an ancestor chain: parts[-1] must be active and its + # ancestors must match the remaining parts in order. + def _path_active(parts: List[str]) -> bool: + leaf_key = parts[-1] + for s in configuration: + if s.key != leaf_key: + continue + node = s + matched = True + for ancestor_key in reversed(parts[:-1]): + node = node.parent + if node is None or node.key != ancestor_key: + matched = False + break + if matched: + return True + return False + + return _path_active(parts) + if isinstance(in_spec, dict): + return all( + _matches_in_state(f"{parent_key}.{child_key}", configuration) + for parent_key, child_key in in_spec.items() + ) + return True + + +def condition_match( + transition: Transition, + context: Optional[Dict] = None, + event: Optional[Event] = None, + configuration: Optional[Set[StateNode]] = None, +) -> bool: + cond = transition.cond + if cond is None: + pass + elif isinstance(cond, str): + guards = getattr(transition.source.machine, "guards", {}) or {} + if cond not in guards: + raise ValueError( + f"Guard '{cond}' is referenced by a transition on " + f"'#{transition.source.id}' but is not implemented. " + f"Pass it via Machine(config, guards={{'{cond}': ...}})." + ) + cond = guards[cond] + + if cond is not None and not bool(_invoke(cond, context, event)): + return False + + in_spec = getattr(transition, "in_state", None) + if in_spec is not None and configuration is not None: + return _matches_in_state(in_spec, configuration) + + return True -def select_transitions(event: Event, configuration: Set[StateNode]): +def select_transitions( + event: Event, + configuration: Set[StateNode], + context: Optional[Dict] = None, + history_value: Optional[HistoryValue] = None, +): + if history_value is None: + history_value = {} enabled_transitions: Set[Transition] = set() atomic_states = [s for s in configuration if is_atomic_state(s)] for state_node in atomic_states: @@ -361,35 +538,53 @@ def select_transitions(event: Event, configuration: Set[StateNode]): if break_loop: break for t in sorted(s.transitions, key=lambda t: t.order): - if t.event and name_match(t.event, event.name) and condition_match(t): + if ( + t.event + and name_match(t.event, event.name) + and condition_match(t, context, event, configuration) + ): enabled_transitions.add(t) break_loop = True - enabled_transitions = remove_conflicting_transitions( - enabled_transitions, configuration=configuration, history_value={} # TODO + enabled_transitions, + configuration=configuration, + history_value=history_value, ) - return enabled_transitions + return sorted(enabled_transitions, key=lambda t: t.order) -def select_eventless_transitions(configuration: Set[StateNode]): +def select_eventless_transitions( + configuration: Set[StateNode], + context: Optional[Dict] = None, + event: Optional[Event] = None, + history_value: Optional[HistoryValue] = None, +): + if history_value is None: + history_value = {} enabled_transitions: Set[Transition] = set() - atomic_states = filter(is_atomic_state, configuration) + atomic_states = [s for s in configuration if is_atomic_state(s)] - loop = True + # For each atomic state, select the first (innermost, document-order) + # matching eventless transition, then move on to the next atomic state. + # `break_loop` stops scanning ancestors of the *current* atomic state once a + # match is found; it must not stop us from visiting the remaining atomic + # states (parallel regions each contribute their own eventless transition). for state in atomic_states: - if not loop: - break + break_loop = False for s in [state] + get_proper_ancestors(state, None): + if break_loop: + break for t in sorted(s.transitions, key=lambda t: t.order): - if not t.event and condition_match(t): + if not t.event and condition_match(t, context, event, configuration): enabled_transitions.add(t) - loop = False + break_loop = True + break enabled_transitions = remove_conflicting_transitions( enabled_transitions=enabled_transitions, configuration=configuration, - history_value={}, # TODO + history_value=history_value, ) return enabled_transitions @@ -433,50 +628,81 @@ def remove_conflicting_transitions( def main_event_loop( - configuration: Set[StateNode], event: Event + configuration: Set[StateNode], + event: Event, + context: Optional[Dict] = None, + history_value: Optional[HistoryValue] = None, ) -> Tuple[Set[StateNode], List[Action]]: states_to_invoke: Set[StateNode] = set() - history_value = {} - enabled_transitions = select_transitions(event=event, configuration=configuration) - - (configuration, actions, internal_queue) = microstep( + if history_value is None: + history_value = {} + enabled_transitions = select_transitions( + event=event, + configuration=configuration, + context=context, + history_value=history_value, + ) + configuration, actions, internal_queue = microstep( enabled_transitions, configuration=configuration, states_to_invoke=states_to_invoke, history_value=history_value, + context=context, + event=event, ) - - (configuration, actions) = macrostep( - configuration=configuration, actions=actions, internal_queue=internal_queue + configuration, actions = main_event_loop2( + configuration=configuration, + actions=actions, + internal_queue=internal_queue, + context=context, + event=event, + history_value=history_value, ) return (configuration, actions) -def macrostep( - configuration: Set[StateNode], actions: List[Action], internal_queue: List[Event] +def main_event_loop2( + configuration: Set[StateNode], + actions: List[Action], + internal_queue: List[Event], + context: Optional[Dict] = None, + event: Optional[Event] = None, + history_value: Optional[HistoryValue] = None, ) -> Tuple[Set[StateNode], List[Action]]: enabled_transitions = set() macrostep_done = False + if history_value is None: + history_value = {} while not macrostep_done: - enabled_transitions = select_eventless_transitions(configuration=configuration) + enabled_transitions = select_eventless_transitions( + configuration=configuration, + context=context, + event=event, + history_value=history_value, + ) if not enabled_transitions: if not internal_queue: macrostep_done = True else: internal_event = internal_queue.pop() + event = internal_event enabled_transitions = select_transitions( event=internal_event, configuration=configuration, + context=context, + history_value=history_value, ) if enabled_transitions: - (configuration, actions, internal_queue) = microstep( + configuration, actions, internal_queue = microstep( enabled_transitions=enabled_transitions, configuration=configuration, states_to_invoke=set(), # TODO - history_value={}, # TODO + history_value=history_value, + context=context, + event=event, ) return (configuration, actions) @@ -486,15 +712,25 @@ def execute_transition_content( enabled_transitions: List[Transition], actions: List[Action], internal_queue: List[Event], + context: Optional[Dict] = None, + event: Optional[Event] = None, ): for transition in enabled_transitions: for action in transition.actions: - execute_content(action, actions, internal_queue) + execute_content(action, actions, internal_queue, context, event) -def execute_content(action: Action, actions: List[Action], internal_queue: List[Event]): +def execute_content( + action: Action, + actions: List[Action], + internal_queue: List[Event], + context: Optional[Dict] = None, + event: Optional[Event] = None, +): if action.type == "xstate:raise": internal_queue.append(Event(action.data.get("event"))) + elif action.type == ASSIGN_TYPE: + _apply_assignment(action, context, event) else: actions.append(action) @@ -504,6 +740,8 @@ def microstep( configuration: Set[StateNode], states_to_invoke: Set[StateNode], history_value: HistoryValue, + context: Optional[Dict] = None, + event: Optional[Event] = None, ) -> Tuple[Set[StateNode], List[Action], List[Event]]: actions: List[Action] = [] internal_queue: List[Event] = [] @@ -515,10 +753,15 @@ def microstep( history_value=history_value, actions=actions, internal_queue=internal_queue, + context=context, + event=event, ) - execute_transition_content( - enabled_transitions, actions=actions, internal_queue=internal_queue + enabled_transitions, + actions=actions, + internal_queue=internal_queue, + context=context, + event=event, ) enter_states( @@ -528,8 +771,9 @@ def microstep( history_value=history_value, actions=actions, internal_queue=internal_queue, + context=context, + event=event, ) - return (configuration, actions, internal_queue) diff --git a/xstate/event.py b/xstate/event.py index 83ad1cc..6252f34 100644 --- a/xstate/event.py +++ b/xstate/event.py @@ -1,4 +1,4 @@ -from typing import Dict, Optional +from typing import Dict, Optional, Union class Event: diff --git a/xstate/machine.py b/xstate/machine.py index 50ccf49..25b3587 100644 --- a/xstate/machine.py +++ b/xstate/machine.py @@ -1,14 +1,14 @@ from typing import Dict, List - +from xstate.state_node import StateNode +from xstate.state import State from xstate.algorithm import ( enter_states, get_configuration_from_state, - macrostep, + get_state_value, main_event_loop, + main_event_loop2, ) from xstate.event import Event -from xstate.state import State -from xstate.state_node import StateNode class Machine: @@ -18,28 +18,56 @@ class Machine: config: object states: Dict[str, StateNode] actions: List[lambda: None] + _order: int - def __init__(self, config: object, actions={}): + def __init__(self, config: object, actions={}, guards={}): self.id = config["id"] self._id_map = {} + self._order = 0 self.root = StateNode( config, machine=self, key=config.get("id", "(machine)"), parent=None ) self.states = self.root.states self.config = config self.actions = actions - - def transition(self, state: State, event: str): + self.guards = guards + self.context = config.get("context", {}) or {} + + def _get_order(self) -> int: + order = self._order + self._order += 1 + return order + + def _to_event(self, event) -> Event: + if isinstance(event, Event): + return event + if isinstance(event, str): + return Event(event) + if isinstance(event, dict): + return Event(event.get("type"), event) + return Event(event) + + def transition(self, state: State, event): + event = self._to_event(event) configuration = get_configuration_from_state( from_node=self.root, state_value=state.value, partial_configuration=set() ) - (configuration, _actions) = main_event_loop(configuration, Event(event)) + context = dict(state.context) if state.context else {} + history_value = dict(state.history_value) if state.history_value else {} + configuration, _actions = main_event_loop( + configuration, event, context, history_value + ) actions, warnings = self._get_actions(_actions) for w in warnings: print(w) - return State(configuration=configuration, context={}, actions=actions) + return State( + configuration=configuration, + context=context, + actions=actions, + history_value=history_value, + ) def _get_actions(self, actions) -> List[lambda: None]: result = [] @@ -91,21 +119,36 @@ def _get_configuration(self, state_value, parent=None) -> List[StateNode]: @property def initial_state(self) -> State: - (configuration, _actions, internal_queue) = enter_states( + context = dict(self.context) + history_value = {} + init_event = Event("xstate.init") + configuration, _actions, internal_queue = enter_states( [self.root.initial], configuration=set(), states_to_invoke=set(), - history_value={}, + history_value=history_value, actions=[], internal_queue=[], + context=context, + event=init_event, ) - (configuration, _actions) = macrostep( - configuration=configuration, actions=_actions, internal_queue=internal_queue + configuration, _actions = main_event_loop2( + configuration=configuration, + actions=_actions, + internal_queue=internal_queue, + context=context, + event=init_event, + history_value=history_value, ) actions, warnings = self._get_actions(_actions) for w in warnings: print(w) - return State(configuration=configuration, context={}, actions=actions) + return State( + configuration=configuration, + context=context, + actions=actions, + history_value=history_value, + ) diff --git a/xstate/scxml.py b/xstate/scxml.py index 3724068..55799c9 100644 --- a/xstate/scxml.py +++ b/xstate/scxml.py @@ -1,17 +1,43 @@ import xml.etree.ElementTree as ET -from typing import Optional - -import js2py +from typing import List, Optional +import json from xstate.machine import Machine ns = {"scxml": "http://www.w3.org/2005/07/scxml"} +def _eval_scxml_cond(event_cond_str: str): + """Compile an SCXML ``cond`` (a JavaScript boolean expression) into a callable. + + JavaScript evaluation is an optional feature; ``js2py`` is imported lazily so + that importing :mod:`xstate` and running native (Python-config) machines never + requires it. A pure-Python SCXML condition evaluator is planned to replace this. + """ + try: + import js2py + except ImportError as exc: # pragma: no cover - depends on optional extra + raise ImportError( + "Evaluating SCXML 'cond' expressions requires the optional 'js2py' " + "dependency. Install it with `pip install xstate[scxml]`." + ) from exc + + return js2py.eval_js("function cond() { return %s }" % event_cond_str) + + +def get_all_state_els(element: ET.Element) -> List[ET.Element]: + all_state_els = [ + e for e in element if get_tag(e) == "state" or get_tag(e) == "parallel" + ] + + return all_state_els + + def convert_scxml(element: ET.Element, parent): + states = element.findall("scxml:state", namespaces=ns) state_els = element.findall("scxml:state", namespaces=ns) parallel_els = element.findall("scxml:parallel", namespaces=ns) - all_state_els = state_els + parallel_els + all_state_els = get_all_state_els(element) initial_state_key = element.attrib.get( "initial", @@ -25,10 +51,15 @@ def convert_scxml(element: ET.Element, parent): } +def get_tag(element: ET.Element) -> str: + _, _, tag = element.tag.rpartition("}") + return tag + + def accumulate_states(element: ET.Element, parent: ET.Element): - state_els = element.findall("scxml:state", namespaces=ns) - parallel_els = element.findall("scxml:parallel", namespaces=ns) - all_state_els = state_els + parallel_els + all_state_els = [ + e for e in element if get_tag(e) == "state" or get_tag(e) == "parallel" + ] states = [convert_state(state_el, element) for state_el in all_state_els] states_dict = {} @@ -40,6 +71,7 @@ def accumulate_states(element: ET.Element, parent: ET.Element): def convert_state(element: ET.Element, parent: ET.Element): + parent_id = parent.attrib.get("id", "") if parent else None id = element.attrib.get("id") transition_els = element.findall("scxml:transition", namespaces=ns) transitions = [convert_transition(el, element) for el in transition_els] @@ -53,21 +85,14 @@ def convert_state(element: ET.Element, parent: ET.Element): onentry_el = element.find("scxml:onentry", namespaces=ns) onentry = convert_onentry(onentry_el, parent=element) if onentry_el else None - _, _, tag = element.tag.rpartition("}") - - initial_state_key = element.attrib.get( - "initial", - convert_state(state_els[0], parent=element).get("key") if state_els else None, - ) - result = { - "type": "parallel" if tag == "parallel" else None, + "type": "parallel" if get_tag(element) == "parallel" else None, "id": f"{id}", "key": id, "exit": onexit, "entry": onentry, "states": states, - "initial": initial_state_key, + "initial": state_els[0].attrib.get("id") if state_els else None, } if len(transitions) > 0: @@ -84,14 +109,10 @@ def convert_state(element: ET.Element, parent: ET.Element): def convert_transition(element: ET.Element, parent: ET.Element): event_type = element.attrib.get("event") - event_target = element.attrib.get("target") + event_targets = element.attrib.get("target").split(" ") event_cond_str = element.attrib.get("cond") - event_cond = ( - js2py.eval_js("function cond() { return %s }" % event_cond_str) - if event_cond_str - else None - ) + event_cond = _eval_scxml_cond(event_cond_str) if event_cond_str else None raise_els = element.findall("scxml:raise", namespaces=ns) @@ -99,7 +120,7 @@ def convert_transition(element: ET.Element, parent: ET.Element): return { "event": event_type, - "target": ["#%s" % event_target], + "target": ["#%s" % t for t in event_targets], "actions": actions, "cond": event_cond, } diff --git a/xstate/state.py b/xstate/state.py index ffcdd1c..6c218d9 100644 --- a/xstate/state.py +++ b/xstate/state.py @@ -1,29 +1,32 @@ -from typing import TYPE_CHECKING, Any, Dict, List, Set - +from __future__ import annotations +from typing import Any, Dict, List, Set, TYPE_CHECKING from xstate.algorithm import get_state_value if TYPE_CHECKING: - from xstate.action import Action from xstate.state_node import StateNode + from xstate.action import Action class State: - configuration: Set["StateNode"] + configuration: Set[StateNode] value: str context: Dict[str, Any] - actions: List["Action"] + actions: List[Action] + history_value: Dict[str, Set[StateNode]] def __init__( self, - configuration: Set["StateNode"], + configuration: Set[StateNode], context: Dict[str, Any], - actions: List["Action"] = [], + actions: List[Action] = [], + history_value: Dict[str, Set[StateNode]] = None, ): root = next(iter(configuration)).machine.root self.configuration = configuration self.value = get_state_value(root, configuration) self.context = context self.actions = actions + self.history_value = history_value if history_value is not None else {} def __repr__(self): return repr( diff --git a/xstate/state_node.py b/xstate/state_node.py index 87da4c3..b9bced0 100644 --- a/xstate/state_node.py +++ b/xstate/state_node.py @@ -1,7 +1,9 @@ -from typing import TYPE_CHECKING, Dict, List, Optional, Union +from __future__ import annotations +from typing import Dict, TYPE_CHECKING, Optional, Union, List +from enum import Enum -from xstate.action import Action from xstate.transition import Transition +from xstate.action import Action if TYPE_CHECKING: from xstate.machine import Machine @@ -9,8 +11,8 @@ class StateNode: on: Dict[str, List[Transition]] - machine: "Machine" - parent: Optional["StateNode"] + machine: Machine + parent: Optional[StateNode] initial: Optional[Transition] entry: List[Action] exit: List[Action] @@ -19,24 +21,21 @@ class StateNode: transitions: List[Transition] id: str key: str - states: Dict[str, "StateNode"] - - def get_actions(self, action): - if callable(action): - return Action(action) - else: - return Action(action.get("type"), exec=None, data=action) + states: Dict[str, StateNode] + order: int def __init__( self, # { "type": "compound", "states": { ... } } config, - machine: "Machine", + machine: Machine, key: str, - parent: Union["StateNode", "Machine"] = None, + parent: Union[StateNode, Machine] = None, ): + self.order = machine._get_order() self.config = config self.parent = parent + self.machine = machine self.id = ( config.get("id", parent.id + "." + key) if parent @@ -70,7 +69,7 @@ def __init__( transition_config, source=self, event=k, - order=len(self.transitions), + order=self.machine._get_order(), ) self.on[k].append(transition) self.transitions.append(transition) @@ -80,6 +79,20 @@ def __init__( if self.type is None: self.type = "atomic" if not self.states else "compound" + # History state support. A history state restores the most recent + # configuration of its parent; "shallow" (default) restores only the + # parent's immediate child, "deep" restores the full descendant path. + # `transition` holds the default target used on the first entry, before + # any history has been recorded. + self.history = ( + config.get("history", "shallow") if self.type == "history" else None + ) + self.transition = None + if self.type == "history" and config.get("target") is not None: + self.transition = Transition( + config.get("target"), source=self, event=None, order=-1 + ) + if self.type == "final": self.donedata = config.get("data") @@ -89,13 +102,24 @@ def __init__( config.get("onDone"), source=self, event=done_event, - order=len(self.transitions), + order=self.machine._get_order(), ) self.on[done_event] = done_transition self.transitions.append(done_transition) machine._register(self) + def get_actions(self, action): + if callable(action): + return Action(action) + else: + return Action(action.get("type"), exec=None, data=action) + + @property + def history_states(self) -> List[StateNode]: + """Child states of this node that are history pseudo-states.""" + return [s for s in self.states.values() if s.type == "history"] + @property def initial(self): initial_key = self.config.get("initial") @@ -105,12 +129,16 @@ def initial(self): return Transition( next(iter(self.states.values())), source=self, event=None, order=-1 ) + if self.type == "parallel": + # Entering a parallel state enters all of its regions; target the + # node itself so add_descendent_states_to_enter fans out to them. + return Transition(self, source=self, event=None, order=-1) else: return Transition( self.states.get(initial_key), source=self, event=None, order=-1 ) - def _get_relative(self, target: str) -> "StateNode": + def _get_relative(self, target: str) -> StateNode: if target.startswith("#"): return self.machine._get_by_id(target[1:]) @@ -118,7 +146,7 @@ def _get_relative(self, target: str) -> "StateNode": if not state_node: raise ValueError( - f"Relative state node '{target}' does not exist on state node '#{self.id}'" # noqa + f"Relative state node '{target}' does not exist on state node '#{self.id}'" ) return state_node diff --git a/xstate/transition.py b/xstate/transition.py index 45fa354..d2e3b68 100644 --- a/xstate/transition.py +++ b/xstate/transition.py @@ -1,5 +1,5 @@ -from typing import TYPE_CHECKING, Any, Callable, List, NamedTuple, Optional, Union - +from __future__ import annotations +from typing import List, Optional, Union, Any, NamedTuple, Callable, TYPE_CHECKING from xstate.action import Action from xstate.event import Event @@ -15,8 +15,8 @@ class TransitionConfig(NamedTuple): class Transition: event: str - source: "StateNode" - config: Union[str, "StateNode", TransitionConfig] + source: StateNode + config: Union[str, StateNode, TransitionConfig] actions: List[Action] cond: Optional[CondFunction] order: int @@ -26,7 +26,7 @@ class Transition: def __init__( self, config, - source: "StateNode", + source: StateNode, event: str, order: int, cond: Optional[CondFunction] = None, @@ -36,6 +36,7 @@ def __init__( self.source = source self.type = "external" self.cond = config.get("cond", None) if isinstance(config, dict) else None + self.in_state = config.get("in", None) if isinstance(config, dict) else None self.order = order self.actions = ( @@ -50,14 +51,20 @@ def __init__( ) @property - def target(self) -> List["StateNode"]: + def target(self) -> List[StateNode]: if isinstance(self.config, str): return [self.source._get_relative(self.config)] elif isinstance(self.config, dict): - if isinstance(self.config["target"], str): - return [self.source._get_relative(self.config["target"])] + # A dict transition may omit "target" entirely — e.g. a guarded + # internal self-loop like {"cond": fn} or {"actions": [...]}. Treat + # the absence of a target as "no state change" rather than crashing. + target = self.config.get("target") + if target is None: + return [] + if isinstance(target, str): + return [self.source._get_relative(target)] - return [self.source._get_relative(v) for v in self.config["target"]] + return [self.source._get_relative(v) for v in target] else: return [self.config] if self.config else []