pax_global_header 0000666 0000000 0000000 00000000064 15000007224 0014500 g ustar 00root root 0000000 0000000 52 comment=ef18293c905dd9fde902eb20c718a0a465a9d33b automat-25.4.16/ 0000775 0000000 0000000 00000000000 15000007224 0013331 5 ustar 00root root 0000000 0000000 automat-25.4.16/.coveragerc 0000664 0000000 0000000 00000000223 15000007224 0015447 0 ustar 00root root 0000000 0000000 [report] precision = 2 ignore_errors = True exclude_lines = pragma: no cover if TYPE_CHECKING \s*\.\.\.$ raise NotImplementedError automat-25.4.16/.github/ 0000775 0000000 0000000 00000000000 15000007224 0014671 5 ustar 00root root 0000000 0000000 automat-25.4.16/.github/workflows/ 0000775 0000000 0000000 00000000000 15000007224 0016726 5 ustar 00root root 0000000 0000000 automat-25.4.16/.github/workflows/ci.yml 0000664 0000000 0000000 00000002552 15000007224 0020050 0 ustar 00root root 0000000 0000000 name: ci on: push: branches: - trunk pull_request: branches: - trunk jobs: build: name: python/${{ matrix.python }} tox/${{ matrix.TOX_ENV }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: python: ["3.9", "3.10", "3.11", "3.12", "3.13"] TOX_ENV: ["extras", "noextras", "mypy"] include: - python: "3.13" TOX_ENV: "lint" steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v3 with: python-version: ${{ matrix.python }} - name: Install graphviz run: | sudo apt-get install -y graphviz dot -V if: ${{ matrix.TOX_ENV == 'extras' }} - name: Tox Run run: | pip install tox; TOX_ENV="py$(echo ${{ matrix.python }} | sed -e 's/\.//g')-${{ matrix.TOX_ENV }}"; echo "Starting: ${TOX_ENV} ${PUSH_DOCS}" if [[ -n "${TOX_ENV}" ]]; then tox -e "$TOX_ENV"; if [[ "${{ matrix.TOX_ENV }}" != "mypy" && "${{ matrix.TOX_ENV }}" != "lint" ]]; then tox -e coverage-report; fi; fi; - name: Upload coverage report if: ${{ matrix.TOX_ENV != 'mypy' }} uses: codecov/codecov-action@v4.5.0 with: token: ${{ secrets.CODECOV_TOKEN }} automat-25.4.16/.gitignore 0000664 0000000 0000000 00000000126 15000007224 0015320 0 ustar 00root root 0000000 0000000 .tox/ .coverage.* .eggs/ *.egg-info/ *.py[co] build/ dist/ docs/_build/ coverage.xml automat-25.4.16/.pydoctor.cfg 0000664 0000000 0000000 00000001645 15000007224 0015741 0 ustar 00root root 0000000 0000000 [tool:pydoctor] quiet=1 warnings-as-errors=true project-name=Automat project-url=../index.html docformat=epytext theme=readthedocs intersphinx= https://graphviz.readthedocs.io/en/stable/objects.inv https://docs.python.org/3/objects.inv https://cryptography.io/en/latest/objects.inv https://pyopenssl.readthedocs.io/en/stable/objects.inv https://hyperlink.readthedocs.io/en/stable/objects.inv https://twisted.org/constantly/docs/objects.inv https://twisted.org/incremental/docs/objects.inv https://python-hyper.org/projects/hyper-h2/en/stable/objects.inv https://priority.readthedocs.io/en/stable/objects.inv https://zopeinterface.readthedocs.io/en/latest/objects.inv https://automat.readthedocs.io/en/latest/objects.inv https://docs.twisted.org/en/stable/objects.inv project-base-dir=automat html-output=docs/_build/api html-viewsource-base=https://github.com/glyph/automat/tree/trunk automat-25.4.16/.readthedocs.yaml 0000664 0000000 0000000 00000002004 15000007224 0016554 0 ustar 00root root 0000000 0000000 # Read the Docs configuration file for Sphinx projects # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Set the OS, Python version and other tools you might need build: os: ubuntu-22.04 tools: python: "3.12" # You can also specify other tool versions: # nodejs: "20" # rust: "1.70" # golang: "1.20" # Build documentation in the "docs/" directory with Sphinx sphinx: configuration: docs/conf.py # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs # builder: "dirhtml" # Fail on all warnings to avoid broken references # fail_on_warning: true # Optionally build your docs in additional formats such as PDF and ePub # formats: # - pdf # - epub # Optional but recommended, declare the Python requirements required # to build your documentation # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html python: install: - requirements: docs/requirements.txt automat-25.4.16/LICENSE 0000664 0000000 0000000 00000002035 15000007224 0014336 0 ustar 00root root 0000000 0000000 Copyright (c) 2014 Rackspace Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. automat-25.4.16/README.md 0000664 0000000 0000000 00000013712 15000007224 0014614 0 ustar 00root root 0000000 0000000 # Automat # [](http://automat.readthedocs.io/en/latest/) [](https://github.com/glyph/automat/actions/workflows/ci.yml?query=branch%3Atrunk) [](http://codecov.io/github/glyph/automat?branch=trunk) ## Self-service finite-state machines for the programmer on the go. ## Automat is a library for concise, idiomatic Python expression of finite-state automata (particularly deterministic finite-state transducers). Read more here, or on [Read the Docs](https://automat.readthedocs.io/), or watch the following videos for an overview and presentation ### Why use state machines? ### Sometimes you have to create an object whose behavior varies with its state, but still wishes to present a consistent interface to its callers. For example, let's say you're writing the software for a coffee machine. It has a lid that can be opened or closed, a chamber for water, a chamber for coffee beans, and a button for "brew". There are a number of possible states for the coffee machine. It might or might not have water. It might or might not have beans. The lid might be open or closed. The "brew" button should only actually attempt to brew coffee in one of these configurations, and the "open lid" button should only work if the coffee is not, in fact, brewing. With diligence and attention to detail, you can implement this correctly using a collection of attributes on an object; `hasWater`, `hasBeans`, `isLidOpen` and so on. However, you have to keep all these attributes consistent. As the coffee maker becomes more complex - perhaps you add an additional chamber for flavorings so you can make hazelnut coffee, for example - you have to keep adding more and more checks and more and more reasoning about which combinations of states are allowed. Rather than adding tedious `if` checks to every single method to make sure that each of these flags are exactly what you expect, you can use a state machine to ensure that if your code runs at all, it will be run with all the required values initialized, because they have to be called in the order you declare them. You can read about state machines and their advantages for Python programmers in more detail [in this excellent article by Jean-Paul Calderone](https://web.archive.org/web/20160507053658/https://clusterhq.com/2013/12/05/what-is-a-state-machine/). ### What makes Automat different? ### There are [dozens of libraries on PyPI implementing state machines](https://pypi.org/search/?q=finite+state+machine). So it behooves me to say why yet another one would be a good idea. Automat is designed around this principle: while organizing your code around state machines is a good idea, your callers don't, and shouldn't have to, care that you've done so. In Python, the "input" to a stateful system is a method call; the "output" may be a method call, if you need to invoke a side effect, or a return value, if you are just performing a computation in memory. Most other state-machine libraries require you to explicitly create an input object, provide that object to a generic "input" method, and then receive results, sometimes in terms of that library's interfaces and sometimes in terms of classes you define yourself. For example, a snippet of the coffee-machine example above might be implemented as follows in naive Python: ```python class CoffeeMachine(object): def brewButton(self) -> None: if self.hasWater and self.hasBeans and not self.isLidOpen: self.heatTheHeatingElement() # ... ``` With Automat, you'd begin with a `typing.Protocol` that describes all of your inputs: ```python from typing import Protocol class CoffeeBrewer(Protocol): def brewButton(self) -> None: "The user pressed the 'brew' button." def putInBeans(self) -> None: "The user put in some beans." ``` We'll then need a concrete class to contain the shared core of state shared among the different states: ```python from dataclasses import dataclass @dataclass class BrewerCore: heatingElement: HeatingElement ``` Next, we need to describe our state machine, including all of our states. For simplicity's sake let's say that the only two states are `noBeans` and `haveBeans`: ```python from automat import TypeMachineBuilder builder = TypeMachineBuilder(CoffeeBrewer, BrewerCore) noBeans = builder.state("noBeans") haveBeans = builder.state("haveBeans") ``` Next we can describe a simple transition; when we put in beans, we move to the `haveBeans` state, with no other behavior. ```python # When we don't have beans, upon putting in beans, we will then have beans noBeans.upon(CoffeeBrewer.putInBeans).to(haveBeans).returns(None) ``` And then another transition that we describe with a decorator, one that *does* have some behavior, that needs to heat up the heating element to brew the coffee: ```python @haveBeans.upon(CoffeeBrewer.brewButton).to(noBeans) def heatUp(inputs: CoffeeBrewer, core: BrewerCore) -> None: """ When we have beans, upon pressing the brew button, we will then not have beans any more (as they have been entered into the brewing chamber) and our output will be heating the heating element. """ print("Brewing the coffee...") core.heatingElement.turnOn() ``` Then we finalize the state machine by building it, which gives us a callable that takes a `BrewerCore` and returns a synthetic `CoffeeBrewer` ```python newCoffeeMachine = builder.build() ``` ```python >>> coffee = newCoffeeMachine(BrewerCore(HeatingElement())) >>> machine.putInBeans() >>> machine.brewButton() Brewing the coffee... ``` All of the *inputs* are provided by calling them like methods, all of the *output behaviors* are automatically invoked when they are produced according to the outputs specified to `upon` and all of the states are simply opaque tokens. automat-25.4.16/SECURITY.md 0000664 0000000 0000000 00000004464 15000007224 0015132 0 ustar 00root root 0000000 0000000 ## Security contact information To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. ## Security supported versions Automat is a [CalVer](https://calver.org) project that issues time-based releases. This means its version format is `YEAR.MONTH.PATCH`. Users are expected to upgrade in a timely manner when new releases of automat are issued; within about 3 months, so that we do not need to maintain an arbitrarily large legacy support window for old versions. This means that a version is “current” until 3 months after the last day of the `YEAR.MONTH` of the *next* released version. This means at least one version is always “current”, regardless of how long ago it was released. The simple rule is this: **upgrade within 3 months of a release, and your current version will always be security-supported**. Automat releases are also largely intended to be compatible, following [Twisted's compatibility policy](https://docs.twisted.org/en/stable/development/compatibility-policy.html) of R+2 for any removals. Thus, “security support” is a function of breaking changes and time. If a vulnerability is discovered, all versions that were *current on that date* will receive a security update. A “security update” is a release with no removals from its previous version, and thus will be installable without breaking compatibility. Some examples may be helpful to understand the nuances here. Let's say it's August 9, 2027. A vulnerability, V1, is discovered, that affects many versions of automat. The previous two versions of Automat were 2025.5.0 and 2026.1.0. Because it is more than 3 months after january 2026, only 2026.1.0 is current. Thus, a security update of 2026.1.1 will be issued. Alternately, let's say it's December 5th, 2029. Another vulnerability, V2, is discovered. It's been an active year for automat: there were lots of deprecations in 2028, and there has been a removal (a breaking change) in every release in 2029, of which there has been one every month. This means that `2029.9.0`, `2029.10.0`, and `2029.11.0` will all be receiving `.1` security updates, with no changes besides the security patch. Once again, just upgrade within 3 months of a release, and you will have no issues. automat-25.4.16/benchmark/ 0000775 0000000 0000000 00000000000 15000007224 0015263 5 ustar 00root root 0000000 0000000 automat-25.4.16/benchmark/test_transitions.py 0000664 0000000 0000000 00000001251 15000007224 0021250 0 ustar 00root root 0000000 0000000 # https://github.com/glyph/automat/issues/60 import automat class Simple(object): """ """ _m = automat.MethodicalMachine() @_m.input() def one(self, data): "some input data" @_m.state(initial=True) def waiting(self): "patiently" @_m.output() def boom(self, data): pass waiting.upon( one, enter=waiting, outputs=[boom], ) def simple_one(machine, data): machine.one(data) def test_simple_machine_transitions(benchmark): benchmark(simple_one, Simple(), 0) automat-25.4.16/docs/ 0000775 0000000 0000000 00000000000 15000007224 0014261 5 ustar 00root root 0000000 0000000 automat-25.4.16/docs/Makefile 0000664 0000000 0000000 00000001137 15000007224 0015723 0 ustar 00root root 0000000 0000000 # Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = python -msphinx SPHINXPROJ = automat SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) automat-25.4.16/docs/_static/ 0000775 0000000 0000000 00000000000 15000007224 0015707 5 ustar 00root root 0000000 0000000 automat-25.4.16/docs/_static/garage_door.machineFactory.dot.png 0000664 0000000 0000000 00000140543 15000007224 0024415 0 ustar 00root root 0000000 0000000 PNG IHDR 4 za~ bKGD IDATxwXTG.P"(`a!-c-QcF%b!cQl M,{qww8sX8)ˢ( A`3-@ 41' sB$D +EA__X TUT}-/Oա--oyy7A0Er2ddf"3ٵJJhZZ 02WA EllW\P^:ed /