{"id":38617669,"url":"https://github.com/opsani/statesman","last_synced_at":"2026-01-17T08:50:56.028Z","repository":{"id":38214048,"uuid":"308531733","full_name":"opsani/statesman","owner":"opsani","description":"A delightful state machine library for Python.","archived":false,"fork":false,"pushed_at":"2024-05-08T14:31:19.000Z","size":191,"stargazers_count":84,"open_issues_count":12,"forks_count":12,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-11-27T17:15:37.679Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/opsani.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-10-30T05:14:30.000Z","updated_at":"2025-11-20T13:39:11.000Z","dependencies_parsed_at":"2023-11-14T21:31:52.580Z","dependency_job_id":"62f13bcd-720f-4184-ab27-e412bd76c98b","html_url":"https://github.com/opsani/statesman","commit_stats":{"total_commits":70,"total_committers":3,"mean_commits":"23.333333333333332","dds":"0.18571428571428572","last_synced_commit":"0677492880ce111d9280b963982b94698bae715c"},"previous_names":[],"tags_count":8,"template":false,"template_full_name":null,"purl":"pkg:github/opsani/statesman","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/opsani%2Fstatesman","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/opsani%2Fstatesman/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/opsani%2Fstatesman/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/opsani%2Fstatesman/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/opsani","download_url":"https://codeload.github.com/opsani/statesman/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/opsani%2Fstatesman/sbom","scorecard":{"id":711094,"data":{"date":"2025-08-11","repo":{"name":"github.com/opsani/statesman","commit":"e898d36eeb5c67a71ee2614c54a2482fd9178f6c"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":2.9,"checks":[{"name":"Code-Review","score":0,"reason":"Found 1/17 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/pre-commit.yaml:12: update your workflow using https://app.stepsecurity.io/secureworkflow/opsani/statesman/pre-commit.yaml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/pre-commit.yaml:13: update your workflow using https://app.stepsecurity.io/secureworkflow/opsani/statesman/pre-commit.yaml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/pre-commit.yaml:14: update your workflow using https://app.stepsecurity.io/secureworkflow/opsani/statesman/pre-commit.yaml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/release.yml:9: update your workflow using https://app.stepsecurity.io/secureworkflow/opsani/statesman/release.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/release.yml:14: update your workflow using https://app.stepsecurity.io/secureworkflow/opsani/statesman/release.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/release.yml:23: update your workflow using https://app.stepsecurity.io/secureworkflow/opsani/statesman/release.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/test.yaml:15: update your workflow using https://app.stepsecurity.io/secureworkflow/opsani/statesman/test.yaml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/test.yaml:16: update your workflow using https://app.stepsecurity.io/secureworkflow/opsani/statesman/test.yaml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/test.yaml:25: update your workflow using https://app.stepsecurity.io/secureworkflow/opsani/statesman/test.yaml/main?enable=pin","Warn: pipCommand not pinned by hash: .github/workflows/release.yml:20","Warn: pipCommand not pinned by hash: .github/workflows/test.yaml:22","Info:   0 out of   8 GitHub-owned GitHubAction dependencies pinned","Info:   0 out of   1 third-party GitHubAction dependencies pinned","Info:   0 out of   2 pipCommand dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: no topLevel permission defined: .github/workflows/pre-commit.yaml:1","Warn: no topLevel permission defined: .github/workflows/release.yml:1","Warn: no topLevel permission defined: .github/workflows/test.yaml:1","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: Apache License 2.0: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'main'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 14 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Vulnerabilities","score":4,"reason":"6 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: PYSEC-2023-117 / GHSA-mrwq-x4v8-fh7p","Warn: Project is vulnerable to: GHSA-34jh-p97f-mpxf","Warn: Project is vulnerable to: PYSEC-2023-212 / GHSA-g4mx-q9vg-27p4","Warn: Project is vulnerable to: GHSA-pq67-6m6q-mj2v","Warn: Project is vulnerable to: PYSEC-2023-192 / GHSA-v845-jxx5-vc9f","Warn: Project is vulnerable to: PYSEC-2024-187 / GHSA-rqc4-2hc7-8c8v"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-22T08:13:20.486Z","repository_id":38214048,"created_at":"2025-08-22T08:13:20.486Z","updated_at":"2025-08-22T08:13:20.486Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28504553,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-17T06:57:29.758Z","status":"ssl_error","status_checked_at":"2026-01-17T06:56:03.931Z","response_time":85,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-01-17T08:50:55.915Z","updated_at":"2026-01-17T08:50:56.018Z","avatar_url":"https://github.com/opsani.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# statesman\n\n![Run Tests](https://github.com/opsani/statesman/workflows/Run%20Tests/badge.svg)\n[![license](https://img.shields.io/github/license/opsani/statesman.svg)](https://github.com/opsani/statesman/blob/master/LICENSE)\n[![PyPI](https://img.shields.io/pypi/v/statesman.svg)](https://pypi.org/project/statesman/)\n[![release](https://img.shields.io/github/release/opsani/statesman.svg)](https://github.com/opsani/statesman/releases/latest)\n[![GitHub release\ndate](https://img.shields.io/github/release-date/opsani/statesman.svg)](https://github.com/opsani/statesman/releases)\n\n![Statesman Logo](./docs/statesman_logo.png)\n\n**The diplomatic path to building state machines in modern Python.**\n\nstatesman is a library that provides an elegant and expressive API for\nimplementing state machines in asynchronous Python 3.8+. It will negotiate\nwith complexity on your behalf and broker a clear, concise agreement about\nhow state is to be managed going forward.\n\n## Features\n\n* A lightweight, but fully featured implementation of the usual suspects in\n  finite state machine libraries (states, events, transitions, actions).\n* A declarative, simple API utilizing type hints, decorators, and enums.\n* Provides a rich set of actions and callbacks for states and events. States\n  support entry and exit actions, events have guard, before, on, and after\n  actions. State machine wide callbacks are provided via overridable methods.\n* Designed and built async native. Callbacks are dispatched asynchronously,\n  making it easy to integrate with long running, event driven processes.\n* Guard actions can cancel transition before any state changes are applied.\n* Data can be modeled directly on the state machine subclass compliments of\n  [Pydantic](https://pydantic-docs.helpmanual.io/).\n* Events support the use of arbitrary associated parameter data that is made\n  available to all actions. Parameters are matched against the signature of the\n  receiving callable, enabling compartmentalization of concerns.\n* Solid test coverage and documentation.\n\n## Example\n\nSo... what's it look like? Glad you asked.\n\n```python\nfrom typing import Optional, List\nimport statesman\n\n\nclass ProcessLifecycle(statesman.StateMachine):\n    class States(statesman.StateEnum):\n        starting = \"Starting...\"\n        running = \"Running...\"\n        stopping = \"Stopping...\"\n        stopped = \"Terminated.\"\n\n    # Track state about the process we are running\n    command: Optional[str] = None\n    pid: Optional[int] = None\n    logs: List[str] = []\n\n    # initial state entry point\n    @statesman.event(None, States.starting)\n    async def start(self, command: str) -\u003e None:\n        \"\"\"\"Start a process.\"\"\"\n        self.command = command\n        self.pid = 31337\n        self.logs.clear()  # Flush logs between runs\n\n    @statesman.event(source=States.starting, target=States.running)\n    async def run(self, transition: statesman.Transition) -\u003e None:\n        \"\"\"Mark the process as running.\"\"\"\n        self.logs.append(f\"Process pid {self.pid} is now running (command=\\\"{self.command}\\\")\")\n\n    @statesman.event(source=States.running, target=States.stopping)\n    async def stop(self) -\u003e None:\n        \"\"\"Stop a running process.\"\"\"\n        self.logs.append(f\"Shutting down pid {self.pid} (command=\\\"{self.command}\\\")\")\n\n    @statesman.event(source=States.stopping, target=States.stopped)\n    async def terminate(self) -\u003e None:\n        \"\"\"Terminate a running process.\"\"\"\n        self.logs.append(f\"Terminated pid {self.pid} (\\\"{self.command}\\\")\")\n        self.command = None\n        self.pid = None\n\n    @statesman.enter_state(States.stopping)\n    async def _print_status(self) -\u003e None:\n        print(\"Entering stopped status!\")\n\n    @statesman.after_event(\"run\")\n    async def _after_run(self) -\u003e None:\n        print(\"running...\")\n\n    async def after_transition(self, transition: statesman.Transition) -\u003e None:\n        if transition.event and transition.event.name == \"stop\":\n            await self.terminate()\n\n\nasync def _examples():\n    # Let's play.\n    state_machine = ProcessLifecycle()\n    await state_machine.start(\"ls -al\")\n    assert state_machine.command == \"ls -al\"\n    assert state_machine.pid == 31337\n    assert state_machine.state == ProcessLifecycle.States.starting\n\n    await state_machine.run()\n    assert state_machine.logs == ['Process pid 31337 is now running (command=\"ls -al\")']\n\n    await state_machine.stop()\n    assert state_machine.logs == [\n        'Process pid 31337 is now running (command=\"ls -al\")',\n        'Shutting down pid 31337 (command=\"ls -al\")',\n        'Terminated pid 31337 (\"ls -al\")',\n    ]\n\n    # Or start in a specific state\n    state_machine = ProcessLifecycle(state=ProcessLifecycle.States.running)\n\n    # Transition to a specific state\n    await state_machine.enter_state(ProcessLifecycle.States.stopping)\n\n    # Trigger an event\n    await state_machine.trigger_event(\"stop\", key=\"value\")\n```\n\nStates are defined as Python enum classes. The name of the enum item defines a\nsymbolic name for the state and the value provides a human readable description.\nA class named `States` embedded within a state machine subclass is automatically\nbound to the state machine.\n\nEvents are declared using the event decorator and the define the source and\ntarget states of a transition. A source state of `None` defines an initial state\ntransition.\n\nOnce a method is decorated as an event action, the original method body is\nattached to the new event as an on event action and the method is replaced with\na implementation that triggers the newly created event.\n\nActions can be attached to events at declaration time or later on via the\n`guard_event`, `before_event`, `on_event`, and `after_event` decorators. Actions\ncan likewise be attached to states via the `enter_state` and `exit_state`\ndecorators.\n\nThere is an extensive API for working with the state machine and its components\nprogrammatically.\n\n## Why statesman?\n\nStatesman was developed because we couldn't find anything like it. While there\nis an embarassment of riches in the Python community with regard to FSM\nlibraries, many have legacy ties back to the Python 2 era and address our first\nclass requirements as add-ons rather than core functionality.\n\nOur design goals were roughly:\n\n* Utilize type hints extensively as both a documentation and design by contract\n    tool to provide an expressive, readable API.\n* Embrace asyncio as a first class citizen.\n* Implement state machines as plain old Python objects with an easily understood\n    method and input/output external API surface. Callers shouldn't need to know\n    or care about FSM minutiae.\n* Deliver an API closely aligned with the [UML State Machine](https://en.wikipedia.org/wiki/UML_state_machine)\nconceptual framework that most developers will have exposure to.\n* Shun famous string values in favor of Python [`enum`](https://docs.python.org/3/library/enum.html)\nsubclasses to facilitate type enforcement, refactoring, and IDE completions.\n* Enable state machines to be modeled programmatically or declaratively.\n* Provide robust support for modeling data within the state machine class itself\n    and passing external data into transitions.\n* Disallow implicit \"magical\" behaviors such as automatically creating states,\n    events, and actions objects or dynamically defining/dispatching methods.\n\nUltimately, we really wanted something that would fit right in with our existing\ncoding style and API aesthetics. Maybe statesman matches yours too.\n\n## Transition Lifecycle\n\nStatesman executes actions during a transition in a defined order. The\n`statesman.Transition` class is a callable that is responsible for modeling\na state change and coordinating its execution. The table below describes the\norder of operations performed when a transition is called.\n\n| Target | Current State | Comments |\n|--------|---------------|----------|\n| `StateMachine.guard_transition` | `source` | Method dispatch for subclasses. Can cancel the transition. |\n| `StateMachine.before_transition` | `source` | Method dispatch for subclasses. |\n| `Event.actions.guard` | `source` | Can cancel the transition. |\n| `Event.actions.before` | `source` | |\n| `State.actions.exit` | `source` | |\n| `StateMachine.on_transition` | `target` | Method dispatch for subclasses. |\n| `Event.actions.on` | `target` | |\n| `State.actions.entry` | `target` | |\n| `Event.actions.after` | `target` | |\n| `StateMachine.after_transition` | `target` | Method dispatch for subclasses. |\n\n## API Overview\n\nStatesman is extensively covered with docstrings and automated tests. The\nproceding subsections present a non-exhaustive task oriented overview of the\nAPI. Check the docstrings and look through the tests if you don't find exactly\nwhat you are looking for.\n\n### Initial States\n\nStatesman considers a transition in which the source state is `None` to describe\nan initial state transition. There are a couple of ways to describe initial\nstates within statesman:\n\n```python\nimport statesman\n\n\n# Describe the initial state with `statesman.InitialState`\nclass StateMachine(statesman.StateMachine):\n    class States(statesman.StateEnum):\n        starting = 'Starting...'\n        running = 'Running...'\n        stopping = 'Stopping...'\n        stopped = statesman.InitialState('Terminated.')\n\n    @statesman.event(None, States.starting)\n    def start(self) -\u003e None:\n        ...\n\n\nasync def _example() -\u003e None:\n    # Set at initialization time\n    state_machine = StateMachine(state=StateMachine.States.stopping)\n\n    # Enter a state directly\n    state_machine = StateMachine()\n    await state_machine.enter_state(StateMachine.States.running)\n\n    # Via an event\n    state_machine = StateMachine()\n    await state_machine.start()\n```\n\n### Introspecting State\n\nEach state machine instance has a `state` attribute that is the source of truth\nfor the state machine. The state can be compared to `statesman.State` object\ninstances or string values.\n\n```python\nimport statesman\n\n\nclass StateMachine(statesman.StateMachine):\n    class States(statesman.StateEnum):\n        starting = 'Starting...'\n        running = 'Running...'\n        stopping = 'Stopping...'\n        stopped = 'Terminated.'\n\n\nasync def _example() -\u003e None:\n    state_machine = StateMachine(state=StateMachine.States.stopping)\n    state_machine.state == StateMachine.States.stopping  # =\u003e True\n    state_machine.state == \"stopping\"  # =\u003e True\n    state_machine.state == StateMachine.States.running  # =\u003e False\n    state_machine.state == \"stopped\"  # =\u003e False\n```\n\n### Entering States\n\nStates can be directly entered via the `statesman.StateMachine.enter_state`\nmethod. States can be referenced by name or by `stateman.State` object instance.\nWhen a state is entered directly, a transition is triggered between the source\nand target states:\n\n```python\nimport statesman\n\n\nclass StateMachine(statesman.HistoryMixin, statesman.StateMachine):\n    class States(statesman.StateEnum):\n        first = \"1\"\n        second = \"2\"\n        third = \"3\"\n\n\nasync def _example() -\u003e None:\n    state_machine = StateMachine()\n    await state_machine.enter_state(StateMachine.States.first)\n    await state_machine.enter_state(StateMachine.States.second)\n    await state_machine.enter_state(StateMachine.States.third)\n```\n\nThe type of transition performed is configurable (see below).\n\nNote that `enter_state` should be used thoughtfully as it enables transitions\nthat may not be expressible via events. Its behavior can also be constrained\nand customized.\n\n### Transition Types\n\nThere are three types of transitions that can be performed by statesman. The\nmost common type is an external transition in which the machine moves between\ntwo distinct states. When a transition occurs in which the source and target\nstates are the same, there two other possible modes: `internal` and `self`.\n\n* `statesman.Transition.Types.external`: A transition in which the state is\n    changed from one value to another.\n* `statesman.Transition.Types.internal`: A transition in which the source and\n    target states are the same but are not exited and reentered during the transition.\n* `statesman.Transition.Types.self`: A transition in which the source and\n    target states are the same and are exited and reentered during the transition.\n\n### Transition Return Values\n\nTo enable external consumers to interact with the state machine without being\nexposed to its implementation details, Statesman supports a flexible set of\nreturn values from transitions.\n\nA default return type can be configured when defining an event via the\ndecorators or programmatic interface. An explicit return value type can also be\nrequested when a transition is dispatched through the `statesman.StateMachine.enter_state`\nor `statesman.StateMachine.trigger_event` methods.\n\nTransitions can invoke an arbitrary number of actions and the desired return\nvalue semantics vary from case to case. Think about the API you wish to present\nto the developer and carefully consider if/how `None` and `False` values are\nutilized within the state machine.\n\nThe available return types are:\n\n* `bool`: A boolean value that indicates if the transition completed\n    successfully.\n* `object`: An arbitrary output value returned by the transition.\n* `tuple`:  A tuple value containing a boolean and output object value.\n* `list`: A list of results returned by all actions invoked by the transition.\n* `statesman.Transition`: The transition object itself, containing all details\n    about the transition.\n\nNote that return types are configured as **type** arguments:\n\n```python\nimport statesman\n\n\nclass StateMachine(statesman.StateMachine):\n    class States(statesman.StateEnum):\n        starting = 'Starting'\n        running = 'Running'\n        stopping = 'Stopping'\n        stopped = 'Stopped'\n\n    @statesman.event(None, States.starting, return_type=bool)\n    async def start(self) -\u003e int:\n        return 31337\n\n\nasync def _example() -\u003e None:\n    state_machine = await StateMachine.create()\n    bool_result = await state_machine.trigger_event(\"start\")\n    int_result = await state_machine.start(return_type=object)\n    transition = await state_machine.enter_state(\n        StateMachine.States.stopped,\n        return_type=statesman.Transition\n    )\n    print(\n        f\"Return Values: state_machine={state_machine}\\n\"\n        f\"bool_result={bool_result}\\n\"\n        f\"int_result={int_result}\\n\"\n        f\"transition={transition}\"\n    )\n```\n\n### Defining and Triggering Events\n\nEvents are typically defined using the `statesman.event` decorator.\nEach event has a **source** state and a **target** state. Typically these are\ndistinct states and describe an event that triggers an **external** transition.\nWhen the source and target state are the same, the event describes an\n**internal** or **self** transition (see above for details).\n\nThe source and target states can be described by string name, enum member\nvalue, or the special sentinel values of `statesman.StateEnum.__any__` or\n`statesman.StateEnum.__active__`. The `__any__` sentinel describes a source\nstate of any member of the state enumeration (but not `None`). The `__active__`\nsentinel resolves dynamically to the currently active state of the state\nmachine and is useful in cases where you want to define a reflexive internal or\nself transition that can be triggered from several states without duplicating\nlogic.\n\nEvents can be triggered programmatically by name, method, or by\ncalling a decorated event method.\n\n```python\nimport statesman\n\n\nclass StateMachine(statesman.StateMachine):\n    class States(statesman.StateEnum):\n        waiting = 'Waiting'\n        running = 'Running'\n        stopped = 'Stopped'\n        aborted = 'Aborted'\n\n    @statesman.event(None, States.waiting)\n    async def start(self) -\u003e None:\n        ...\n\n    @statesman.event(States.waiting, States.running)\n    async def run(self) -\u003e None:\n        ...\n\n    @statesman.event(States.running, States.stopped)\n    async def stop(self) -\u003e None:\n        ...\n\n    @statesman.event(States.__any__, States.aborted)\n    async def abort(self) -\u003e None:\n        ...\n\n    @statesman.event(\n        States.__any__,\n        States.__active__,\n        type=statesman.Transition.Types.self\n    )\n    async def check(self) -\u003e None:\n        print(\"Exiting and reentering active state!\")\n\n\nasync def _example() -\u003e None:\n    state_machine = await StateMachine.create()\n    await state_machine.trigger_event(\"start\")\n    await state_machine.run()\n    await state_machine.trigger_event(state_machine.stop)\n```\n\n### State and Event Actions\n\nStates and Events support the attachment of an arbitrary number of actions. An\naction is an object that wraps a callable that called at a designated moment in\nthe lifecycle of the state machine.\n\nState objects support the following action types:\n\n* `statesman.Action.Types.entry`: Called when a state is entered during a\n    transition.\n* `statesman.Action.Types.exit`: Called when a state is exited during a\n    transition.\n\nEvent actions support the following action types:\n\n* `statesman.Action.Types.guard`: Called to determine if the event can be\n    executed.\n* `statesman.Action.Types.before`: Called before the state transition described\n    by the event is applied. The state of the machine is the source state.\n* `statesman.Action.Types.on`: Called when the state transition described by the\n    event is applied. The state of the machine is the target state.\n* `statesman.Action.Types.after`: Called after the state transition described by\n    the event has been applied. The state of the machine is the target state.\n\nState and Event actions can be defined in several ways. The `statesman.state`\nand `statesman.event` decorators accept keyword arguments named after the action\ntypes that they support. These arguments support method object references,\nnames, or callables (e.g., lambdas). Additionally, there are standalone\ndecorators that enable a declarative style of action definition.\n\n```python\nimport random\nimport statesman\n\n\nclass StateMachine(statesman.StateMachine):\n    class States(statesman.StateEnum):\n        starting = 'Starting...'\n        running = 'Running...'\n        stopping = 'Stopping...'\n        stopped = statesman.InitialState('Terminated.')\n\n    @statesman.event(States.stopped, States.starting)\n    async def start(self) -\u003e None:\n        ...\n\n    @statesman.enter_state(States.starting)\n    async def _announce_start(self) -\u003e None:\n        print(\"enter:starting\")\n\n    def _can_run(self) -\u003e bool:\n        return False\n\n    @statesman.event(States.starting, States.running, guard=_can_run)\n    async def run(self) -\u003e None:\n        ...\n\n    @statesman.event(States.running, States.stopping)\n    async def stop(self) -\u003e None:\n        ...\n\n    @statesman.after_event(stop)\n    async def _announce_stop(self) -\u003e None:\n        print(\"after:stop\")\n\n    @statesman.event(\n        States.stopping,\n        States.stopped,\n        guard=lambda: random.choice([True, False])\n    )\n    async def terminate(self) -\u003e None:\n        ...\n```\n\n### Guard Callbacks and Actions\n\nGuard callbacks and actions are handled differently from other behavioral hooks.\nBecause guards can be used to cancel/reject an event, they are executed\nsequentially in the order that they were added to the state machine.\n\nUpon encountering a failing guard that has returned `False` or raised an\nexception of type `AssertionError`, the state machine consults the `guard_with`\nbehavior configured on the `Config` class nested within the state machine class.\n\nThere are three behaviors available for configuration via `guard_with`:\n\n* `statesman.Guard.silence` (Default): The transition is aborted and an empty\n    result set is returned.\n* `statesman.Guard.warning`: The transition is aborted, an empty result set is\n    returned, and a warning is logged.\n* `statesman.Guard.exception`: The transition is aborted and a `RuntimeError` is\n    raised.\n\n```python\nimport statesman\n\n\nclass StateMachine(statesman.StateMachine):\n    class States(statesman.StateEnum):\n        starting = 'Starting...'\n        running = 'Running...'\n        stopping = 'Stopping...'\n        stopped = statesman.InitialState('Terminated.')\n\n    class Config:\n        guard_with = statesman.Guard.exception\n```\n\n### Passing Data to Transitions\n\nStatesman is designed to model and manage arbitrary data that is bound to the\ncurrent state of the state machine. Such data can be provided as positional and\nkeyword arguments when a transition is triggered via the `statesman.StateMachine.enter_state`\nor `statesman.StateMachine.trigger_event` methods.\n\nStatesman utilizes method argument list introspection and type-hinting to invoke\ncallbacks and actions with the specific arguments that they are interested in.\nThis facilitates cleaner encapsulation, separation of concerns, and\nmaintainability by ensuring that each callback and method declares its\nexpectations. The `*args` and `**kwargs` variadic arguments can be used as\nnecessary.\n\n```python\nimport statesman\n\n\nclass StateMachine(statesman.StateMachine):\n    class States(statesman.StateEnum):\n        starting = 'Starting...'\n        running = 'Running...'\n        stopping = 'Stopping...'\n        stopped = statesman.InitialState('Terminated.')\n\n    @statesman.event(States.stopped, States.starting)\n    async def start(self, process_name: str) -\u003e None:\n        ...\n\n    @statesman.event(States.starting, States.running)\n    async def run(self, uid: int, gid: int) -\u003e None:\n        ...\n\n    @statesman.event(States.running, States.stopping)\n    async def stop(self, *args, all: bool = False, **kwargs) -\u003e None:\n        ...\n\n\nasync def _example() -\u003e None:\n    state_machine = await StateMachine.create()\n    await state_machine.start(\"servox\")\n    await state_machine.run(0, 31337)\n    await state_machine.stop(\"one\", \"two\", all=True, this=\"That\")\n```\n\n### State Machine Inheritable Callbacks\n\nThe `statesman.StateMachine` class is typically subclassed. The base class\nprovides inheritable method implementations of transition lifecycle events.\nAnalogous functionality can be implemented through callbacks as through the\nState and Event actions except that perspective is rooted on the transition\nas opposed to the State or Event.\n\nSee the [Transition Lifecycle](#transition-lifecycle) for specifics on execution\norder and precedence between callbacks and actions.\n\n```python\nimport statesman\n\n\nclass InheritableStateMachine(statesman.StateMachine):\n    async def guard_transition(self, transition: statesman.Transition, *args, **kwargs) -\u003e bool:\n        return True\n\n    async def before_transition(self, transition: statesman.Transition, *args, **kwargs) -\u003e None:\n        ...\n\n    async def on_transition(self, transition: statesman.Transition, *args, **kwargs) -\u003e None:\n        ...\n\n    async def after_transition(self, transition: statesman.Transition, *args, **kwargs) -\u003e None:\n        ...\n```\n\n### Async Initialization\n\nState machines can be asynchronously constructed and initialized:\n\n```python\nimport statesman\n\n\nclass States(statesman.StateEnum):\n    starting = 'Starting'\n    running = 'Running'\n    stopping = 'Stopping'\n    stopped = 'Stopped'\n\n\nasync def _example() -\u003e None:\n    state_machine = await statesman.StateMachine.create(\n        states=statesman.State.from_enum(States)\n    )\n    print(f\"Initialized state machine: {repr(state_machine)}\")\n```\n\n### Restricting State Entry\n\nThe `statesman.StateMachine.enter_state` method provides great flexibility for\nputting the state machine into a specific state without regard for the event\ntransition constraints that have been defined. Depending on the specifics of the\nstate being modeled, this can become a potential liability as it can enable\nprogrammer errors by coercing an initialized state machine into an inconsistent\nor unexpected state. It can be desirable to restrict the use of `enter_state` to\nestablishing an initial state or forbidding its use entirely in favor of initial\nstate transition events.\n\nAs such, statesman provides a set of behaviors that govern how the `enter_state`\nmethod behaves. The behavior is configured via the `state_entry` attribute of\nthe `Config` class nested within the state machine class.\n\nThere are four behaviors available for configuration via `state_entry`:\n\n* `statesman.Entry.allow` (Default): The `enter_state` method can be called at\n    any time.\n* `statesman.Entry.initial`: The `enter_state` method can be called to\n    establish an initial state and thereafter is forbidden.\n* `statesman.Entry.ignore`: The `enter_state` method can never succeed and\n    will fail and return silently when called.\n* `statesman.Entry.forbid`: The `enter_state` method can never succeed and will\n    raise an exception when called.\n\n```python\nimport statesman\n\n\nclass StateMachine(statesman.StateMachine):\n    class States(statesman.StateEnum):\n        starting = 'Starting...'\n        running = 'Running...'\n        stopping = 'Stopping...'\n        stopped = statesman.InitialState('Terminated.')\n\n    class Config:\n        state_entry = statesman.Entry.forbid\n```\n\n### Tracking History\n\nIt can be interesting to maintain a history of transitions that have occurred\nwithin a state machine. Statesman provides out of the box support for history\ntracking via the `statesman.HistoryMixin` class:\n\n```python\nimport statesman\n\n\nclass StateMachine(statesman.HistoryMixin, statesman.StateMachine):\n    class States(statesman.StateEnum):\n        ready = statesman.InitialState(\"Ready\")\n        analyzing = \"Analyzing\"\n\n        awaiting_description = \"Awaiting Description\"\n        awaiting_measurement = \"Awaiting Measurement\"\n        awaiting_adjustment = \"Awaiting Adjustment\"\n\n        done = \"Done\"\n        failed = \"Failed\"\n\n\nasync def _example() -\u003e None:\n    state_machine = StateMachine()\n    await state_machine.enter_state(StateMachine.States.analyzing)\n    await state_machine.enter_state(StateMachine.States.awaiting_measurement)\n    await state_machine.enter_state(StateMachine.States.done)\n    await state_machine.enter_state(StateMachine.States.failed)\n\n    print(f\"The history is: {state_machine.history}\")\n```\n\n### Sequencing Transitions\n\nThere are use cases such as testing and protocol development in which it can\nbecome desirable for the state machine to follow a defined linear sequence of\ntransitions.\n\nStatesman supports such use cases via the `statesman.SequencingMixin` class.\nTransitions can be sequenced by passing **coroutine** objects into the\n`sequence` method. Coroutine objects are returned when you call an async\nfunction but do not await its execution. The coroutine objects freeze your\ndesired state transition, which are queued for iterative execution by invoking\nthe `next_transition` method.\n\n```python\nfrom typing import List\nimport statesman\n\n\nclass StateMachine(statesman.SequencingMixin, statesman.StateMachine):\n    class States(statesman.StateEnum):\n        ready = statesman.InitialState(\"Ready\")\n        analyzing = \"Analyzing\"\n\n        awaiting_description = \"Awaiting Description\"\n        awaiting_measurement = \"Awaiting Measurement\"\n        awaiting_adjustment = \"Awaiting Adjustment\"\n\n        done = \"Done\"\n        failed = \"Failed\"\n\n    @statesman.event([States.ready, States.analyzing], States.awaiting_description)\n    async def request_description(self) -\u003e None:\n        \"\"\"Request a Description of application state from the servo.\"\"\"\n        ...\n\n    @statesman.event([States.ready, States.analyzing], States.awaiting_measurement)\n    async def request_measurement(self, metrics: List[str]) -\u003e None:\n        \"\"\"Request a Measurement from the servo.\"\"\"\n        ...\n\n    @statesman.event([States.ready, States.analyzing], States.awaiting_adjustment)\n    async def recommend_adjustments(self, adjustments: List[str]) -\u003e None:\n        \"\"\"Recommend Adjustments to the Servo.\"\"\"\n        ...\n\n\nasync def _example() -\u003e None:\n    state_machine = StateMachine()\n    state_machine.sequence(\n        state_machine.request_description(),\n        state_machine.request_measurement(metrics=[...]),\n        state_machine.recommend_adjustments([...]),\n        state_machine.request_measurement(metrics=[...]),\n        state_machine.recommend_adjustments([...]),\n    )\n\n    while True:\n        transition = state_machine.next_transition()\n        print(f\"Executed transition: {repr(transition)}\")\n        if not transition:\n            break\n```\n\n## Future Directions\n\nThere is active work underway exploring alternate syntaxes for working with\nstatesman via a state table syntax/interface.\n\nIt would also be interesting to follow in the tradition of other FSM libraries\nthat have come before and support graphing/visualizations of statesman machines.\n\nIt seems inevitable that hierarchical state machine support will eventually\nland.\n\n## Acknowledgements\n\n[Klemen Verdnik](https://github.com/chipxsd) contributed the statesman logo.\n\n## License\n\nstatesman is licensed under the terms of the Apache 2.0 license.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fopsani%2Fstatesman","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fopsani%2Fstatesman","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fopsani%2Fstatesman/lists"}