././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1693476054.0342357 automaton-3.2.0/0000775000175000017500000000000000000000000013546 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/.coveragerc0000664000175000017500000000015500000000000015670 0ustar00zuulzuul00000000000000[run] branch = True source = automaton omit = automaton/tests/* [report] ignore_errors = True precision = 2 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/.pre-commit-config.yaml0000664000175000017500000000252200000000000020030 0ustar00zuulzuul00000000000000# We from the Oslo project decided to pin repos based on the # commit hash instead of the version tag to prevend arbitrary # code from running in developer's machines. To update to a # newer version, run `pre-commit autoupdate` and then replace # the newer versions with their commit hash. default_language_version: python: python3 repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: 9136088a246768144165fcc3ecc3d31bb686920a # v3.3.0 hooks: - id: trailing-whitespace # Replaces or checks mixed line ending - id: mixed-line-ending args: ['--fix', 'lf'] exclude: '.*\.(svg)$' # Forbid files which have a UTF-8 byte-order marker - id: check-byte-order-marker # Checks that non-binary executables have a proper shebang - id: check-executables-have-shebangs # Check for files that contain merge conflict strings. - id: check-merge-conflict # Check for debugger imports and py37+ breakpoint() # calls in python source - id: debug-statements - id: check-yaml files: .*\.(yaml|yml)$ - repo: local hooks: - id: flake8 name: flake8 additional_dependencies: - hacking>=3.1.0,<4.0 language: python entry: flake8 files: '^.*\.py$' exclude: '^(doc|releasenotes|tools)/.*$' ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/.stestr.conf0000664000175000017500000000006200000000000016015 0ustar00zuulzuul00000000000000[DEFAULT] test_path=./automaton/tests top_dir=./ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/.zuul.yaml0000664000175000017500000000034400000000000015510 0ustar00zuulzuul00000000000000- project: templates: - check-requirements - lib-forward-testing-python3 - openstack-python3-antelope-jobs - periodic-stable-jobs - publish-openstack-docs-pti - release-notes-jobs-python3 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476053.0 automaton-3.2.0/AUTHORS0000664000175000017500000000242300000000000014617 0ustar00zuulzuul00000000000000Andreas Jaeger Cao Xuan Hoang ChangBo Guo(gcb) Chuck Short Corey Bryant Daniel Bengtsson Davanum Srinivas Doug Hellmann Elod Illes Hervé Beraud Janonymous Joshua Harlow Joshua Harlow Le Hou Ondřej Nový OpenStack Release Bot Riccardo Pittau Ronald Bradford Ruby Loo Sean McGinnis Stephen Finucane Swapnil Kulkarni (coolsvap) Tom Cocozzello Tony Breeds Vu Cong Tuan gecong1973 jacky06 kangyufei melissaml nizam pengyuesheng sonu.kumar venkatamahesh wu.shiming ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/CONTRIBUTING.rst0000664000175000017500000000165200000000000016213 0ustar00zuulzuul00000000000000If you would like to contribute to the development of OpenStack, you must follow the steps in this page: https://docs.openstack.org/infra/manual/developers.html If you already have a good understanding of how the system works and your OpenStack accounts are set up, you can skip to the development workflow section of this documentation to learn how changes to OpenStack should be submitted for review via the Gerrit tool: https://docs.openstack.org/infra/manual/developers.html#development-workflow The code is hosted at: https://opendev.org/openstack/automaton. Pull requests submitted through GitHub will be ignored. Bugs should be filed on Launchpad, not GitHub: https://bugs.launchpad.net/automaton The mailing list is (prefix subjects with "[Oslo][Automaton]"): https://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-discuss Questions and discussions take place in #openstack-oslo on irc.OFTC.net. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476053.0 automaton-3.2.0/ChangeLog0000664000175000017500000002130000000000000015314 0ustar00zuulzuul00000000000000CHANGES ======= 3.2.0 ----- * Update master for stable/2023.1 3.1.0 ----- * Add Python3 antelope unit tests * Update master for stable/zed 3.0.1 ----- * Fix formatting of release list 3.0.0 ----- * Drop python3.6/3.7 support in testing runtime * Remove unnecessary unicode prefixes * Add Python3 zed unit tests * Update master for stable/yoga 2.5.0 ----- * doc: Avoid duplicate entry warning * Add Python3 yoga unit tests * Update master for stable/xena 2.4.0 ----- * Moving to OFTC * setup.cfg: Replace dashes with underscores * Move flake8 as a pre-commit local target * Add Python3 xena unit tests * Update master for stable/wallaby * tox: Remove references to testr * Remove lower-constraints remnants 2.3.0 ----- * Uncap PrettyTable * Dropping lower constraints testing * Remove six dependency * Use TOX\_CONSTRAINTS\_FILE * Use py3 as the default runtime for tox * Adding pre-commit * Add Python3 wallaby unit tests * Update master for stable/victoria 2.2.0 ----- * Update lower-constraints list * drop mock from lower-constraints 2.1.0 ----- * Stop to use the \_\_future\_\_ module * Small cleanup * Switch to newer openstackdocstheme and reno versions * Bump default tox env from py37 to py38 * Add py38 package metadata * Add Python3 victoria unit tests * Update master for stable/ussuri 2.0.1 ----- * Update hacking for Python3 2.0.0 ----- * Add python3 classifiers * Ignore releasenote artifacts files * [ussuri][goal] Drop python 2.7 support and testing * trivial: Remove noise * Move doc requirements into dedicated file * Switch to Ussuri jobs * Blacklist sphinx 2.1.0 (autodoc bug) * Update the constraints url * Update master for stable/train 1.17.0 ------ * Add Python 3 Train unit tests * Add local bindep.txt * Sync Sphinx requirement * Update to opendev * OpenDev Migration Patch * Update master for stable/stein * add python 3.7 unit test job 1.16.0 ------ * Use template for lower-constraints * Change openstack-dev to openstack-discuss * Change openstack-dev to openstack-discuss * add lib-forward-testing-python3 test job * add python 3.6 unit test job * import zuul job settings from project-config * Update reno for stable/rocky 1.15.0 ------ * Switch to stestr * fix tox python3 overrides * Trivial: Update pypi url to new url * fix list of default virtualenvs * set default python to python3 * add lower-constraints job * Updated from global requirements * Update reno for stable/queens * Updated from global requirements * Updated from global requirements * Updated from global requirements 1.14.0 ------ 1.13.1 ------ * Avoid tox\_install.sh for constraints support * Remove setting of version/release from releasenotes * Updated from global requirements 1.13.0 ------ * Updated from global requirements * Remove kwarg default\_start\_state in the machine constructor * Updated from global requirements * Update reno for stable/pike * Updated from global requirements 1.12.0 ------ * Update URLs in documents according to document migration * update link to docs in readme 1.11.0 ------ * switch from oslosphinx to openstackdocstheme * rearrange existing documentation according to the new standard layout * Updated from global requirements 1.10.0 ------ * Remove pbr warnerrors in favor of sphinx check * Remove support for py34 * Updated from global requirements * Updated from global requirements 1.9.0 ----- * Updated from global requirements * Remove unused dependecy testscenarios 1.8.0 ----- * Updated from global requirements * Updated from global requirements * Update reno for stable/ocata 1.7.0 ----- * Don't include openstack/common in flake8 exclude list * Removes unnecessary utf-8 encoding * Add Constraints support * Replace six.iteritems() with .items() 1.6.0 ----- * Updated from global requirements * Fix release notes gate failure * Updated from global requirements * Add reno for release notes management 1.5.0 ----- * Changed the home-page link * Updated from global requirements * Updated from global requirements 1.4.0 ----- * Remove discover from test-requirements * Add Python 3.5 classifier and venv 1.3.0 ----- * Updated from global requirements 1.2.0 ----- * Add a state-space machine building example * Ensure state space can also pass on\_enter/exit callbacks * Updated from global requirements * Updated from global requirements * Ensure machine special method(s) include in generated docs * Put py34 first in the envlist order of tox ,remove py33 1.1.0 ----- * Removes MANIFEST.in as it is not needed explicitely by PBR * Deprecated tox -downloadcache option removed 1.0.0 ----- * Updated from global requirements * Remove python 2.6 and cleanup tox.ini 0.8.0 ----- * Added code coverage section to tox * No need for Oslo Incubator Sync * Ignore generated files * docs - Set pbr 'warnerrors' option for doc build * Remove dummy/placeholder 'ChangeLog' as its not needed * Enhance the README * Fix the build path in .gitignore file * Updated from global requirements * Provide a finite machine build() method * Allow for raising on duplicate transition registration 0.7.0 ----- * Updated from global requirements * Updated from global requirements * Updated from global requirements 0.6.0 ----- * Activate pep8 check that \_ is imported * Updated from global requirements 0.5.0 ----- * Ensure doctesting and doc8 testing done in py27 env * Updated from global requirements * Remove setup.cfg 'requires-python' incorrect entry * Document \`process\_event\` return and expose return namedtuple type * Allow providing and using a 'nested\_start\_state\_fetcher' * Allow providing a state-name translation dict 0.4.0 ----- * Add a bigger CD-player state-machine doctest(ed) example * Add \`is\_actionable\_event\` checking function * Disallow adding transitions from terminal states * Add and use a callback name fetching utility function * Add runners to features.rst & add a runner base & update docstrings 0.3.0 ----- * Add badge support to README.rst * Add code repo, mail list, and IRC to CONTRIBUTING * Remove 3.3 classifier * Remove 2.6 classifier + 2.6 compatibility code * Add history.rst that uses generated 'ChangeLog' file * Add base exception class for this library * Updated from global requirements * Add optional machine conversion into a pydot graph * Updated from global requirements * Updated from global requirements * When a state has no transitions show its own \`on\_exit\` and \`on\_enter\` * Add a more complex doctest(ed) example * Add simple machine doctest(ed) example into docs 0.2.0 ----- * Split the state machine runners off into own file * Use debtcollector removals function instead of warnings.warn * Revamp repo to match openstack repos * Allow the hierarchical machine to provide back the nested machines * Retain & deprecate default\_start\_state via constructor * Amend the unittest due to more on\_exit being triggered * Have the start state 'on\_exit' be called when exit occurs * Use a property setter instead of a method * Require using set\_default\_start\_state to set the default * Add more checks on setting a alternative start state default * Rename start\_state to default\_start\_state * Use type(self) instead of self.\_\_class\_\_ * Correctly copy derived classes * Allow initialize to take an alternative start\_state * Update message when processing event and not initialized * Add pre and post event processing methods * Share the same not found template between machines * Avoid having a \_generate\_runner method when inheritance is ok * Allow frozen to be set/unset * Add testrepository to testing requirements * Fixup the classifiers * Fix the tox to install the right requirements * Just use \_generate\_runner to generate the different runner types * Use quoting in the machine code documentation * Adjust pformat() + add examples * Remove version caps * Split the requirements file into py2/py3 variations * Move process event to be a static method * Add a HierarchicalFiniteMachine + Runner * Use a helper classmethod to create machines * Rename \_Runner -> \_FiniteRunner * Move to top level machines module, seems cleaner this way * Fix the pformat() example * Move the fsm -> machines/finite.py and split off the running methods * Allow copies to be unfrozen (if the parent is frozen) * Make frozen a non-settable attribute and copy it correctly * Allow machines to be shallow or deep copied * Three is the number for alpha * Change beta to alpha (for now) 0.1 --- * Add the travis badge * Add a travis testing file * Also install the main requirements.txt when using tox * Move over the fsm test * Add testtools testing requirement * Use the test-requirements.txt for tox.ini deps * Add needed testing requirement and tox.ini file * Don't forget the requirements.txt file * Fixup the README.rst and setup.cfg * Use prettytable * Move a bunch of files into there rightful places * Initial commit ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/HACKING.rst0000664000175000017500000000021500000000000015342 0ustar00zuulzuul00000000000000Automaton Style Commandments ============================ Read the OpenStack Style Commandments https://docs.openstack.org/hacking/latest/ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/LICENSE0000664000175000017500000002607500000000000014565 0ustar00zuulzuul00000000000000Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1693476054.0342357 automaton-3.2.0/PKG-INFO0000664000175000017500000000355200000000000014650 0ustar00zuulzuul00000000000000Metadata-Version: 1.2 Name: automaton Version: 3.2.0 Summary: Friendly state machines for python. Home-page: https://docs.openstack.org/automaton/latest/ Author: OpenStack Author-email: openstack-discuss@lists.openstack.org License: UNKNOWN Description: ========= Automaton ========= .. image:: https://img.shields.io/pypi/v/automaton.svg :target: https://pypi.org/project/automaton/ :alt: Latest Version .. image:: https://img.shields.io/pypi/dm/automaton.svg :target: https://pypi.org/project/automaton/ :alt: Downloads Friendly state machines for python. The goal of this library is to provide well documented state machine classes and associated utilities. The state machine pattern (or the implemented variation there-of) is a commonly used pattern and has a multitude of various usages. Some of the usages for this library include providing state & transition validation and running/scheduling/analyzing the execution of tasks. * Free software: Apache license * Documentation: https://docs.openstack.org/automaton/latest/ * Source: https://opendev.org/openstack/automaton * Bugs: https://bugs.launchpad.net/automaton Platform: UNKNOWN Classifier: Development Status :: 3 - Alpha Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Topic :: Software Development :: Libraries Requires-Python: >=3.8 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/README.rst0000664000175000017500000000157100000000000015241 0ustar00zuulzuul00000000000000========= Automaton ========= .. image:: https://img.shields.io/pypi/v/automaton.svg :target: https://pypi.org/project/automaton/ :alt: Latest Version .. image:: https://img.shields.io/pypi/dm/automaton.svg :target: https://pypi.org/project/automaton/ :alt: Downloads Friendly state machines for python. The goal of this library is to provide well documented state machine classes and associated utilities. The state machine pattern (or the implemented variation there-of) is a commonly used pattern and has a multitude of various usages. Some of the usages for this library include providing state & transition validation and running/scheduling/analyzing the execution of tasks. * Free software: Apache license * Documentation: https://docs.openstack.org/automaton/latest/ * Source: https://opendev.org/openstack/automaton * Bugs: https://bugs.launchpad.net/automaton ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1693476054.026235 automaton-3.2.0/automaton/0000775000175000017500000000000000000000000015555 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/automaton/__init__.py0000664000175000017500000000000000000000000017654 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/automaton/_utils.py0000664000175000017500000000325200000000000017430 0ustar00zuulzuul00000000000000# Copyright (C) 2015 Yahoo! Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import inspect def get_callback_name(cb): """Tries to get a callbacks fully-qualified name. If no name can be produced ``repr(cb)`` is called and returned. """ segments = [] try: segments.append(cb.__qualname__) except AttributeError: try: segments.append(cb.__name__) if inspect.ismethod(cb): try: # This attribute doesn't exist on py3.x or newer, so # we optionally ignore it... (on those versions of # python `__qualname__` should have been found anyway). segments.insert(0, cb.im_class.__name__) except AttributeError: pass except AttributeError: pass if not segments: return repr(cb) else: try: # When running under sphinx it appears this can be none? if cb.__module__: segments.insert(0, cb.__module__) except AttributeError: pass return ".".join(segments) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1693476054.0302355 automaton-3.2.0/automaton/converters/0000775000175000017500000000000000000000000017747 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/automaton/converters/__init__.py0000664000175000017500000000000000000000000022046 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/automaton/converters/pydot.py0000664000175000017500000001151200000000000021460 0ustar00zuulzuul00000000000000# Copyright (C) 2015 Yahoo! Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. try: import pydot PYDOT_AVAILABLE = True except ImportError: PYDOT_AVAILABLE = False def convert(machine, graph_name, graph_attrs=None, node_attrs_cb=None, edge_attrs_cb=None, add_start_state=True, name_translations=None): """Translates the state machine into a pydot graph. :param machine: state machine to convert :type machine: FiniteMachine :param graph_name: name of the graph to be created :type graph_name: string :param graph_attrs: any initial graph attributes to set (see http://www.graphviz.org/doc/info/attrs.html for what these can be) :type graph_attrs: dict :param node_attrs_cb: a callback that takes one argument ``state`` and is expected to return a dict of node attributes (see http://www.graphviz.org/doc/info/attrs.html for what these can be) :type node_attrs_cb: callback :param edge_attrs_cb: a callback that takes three arguments ``start_state, event, end_state`` and is expected to return a dict of edge attributes (see http://www.graphviz.org/doc/info/attrs.html for what these can be) :type edge_attrs_cb: callback :param add_start_state: when enabled this creates a *private* start state with the name ``__start__`` that will be a point node that will have a dotted edge to the ``default_start_state`` that your machine may have defined (if your machine has no actively defined ``default_start_state`` then this does nothing, even if enabled) :type add_start_state: bool :param name_translations: a dict that provides alternative ``state`` string names for each state :type name_translations: dict """ if not PYDOT_AVAILABLE: raise RuntimeError("pydot (or pydot2 or equivalent) is required" " to convert a state machine into a pydot" " graph") if not name_translations: name_translations = {} graph_kwargs = { 'rankdir': 'LR', 'nodesep': '0.25', 'overlap': 'false', 'ranksep': '0.5', 'size': "11x8.5", 'splines': 'true', 'ordering': 'in', } if graph_attrs is not None: graph_kwargs.update(graph_attrs) graph_kwargs['graph_name'] = graph_name g = pydot.Dot(**graph_kwargs) node_attrs = { 'fontsize': '11', } nodes = {} for (start_state, event, end_state) in machine: if start_state not in nodes: start_node_attrs = node_attrs.copy() if node_attrs_cb is not None: start_node_attrs.update(node_attrs_cb(start_state)) pretty_start_state = name_translations.get(start_state, start_state) nodes[start_state] = pydot.Node(pretty_start_state, **start_node_attrs) g.add_node(nodes[start_state]) if end_state not in nodes: end_node_attrs = node_attrs.copy() if node_attrs_cb is not None: end_node_attrs.update(node_attrs_cb(end_state)) pretty_end_state = name_translations.get(end_state, end_state) nodes[end_state] = pydot.Node(pretty_end_state, **end_node_attrs) g.add_node(nodes[end_state]) edge_attrs = {} if edge_attrs_cb is not None: edge_attrs.update(edge_attrs_cb(start_state, event, end_state)) g.add_edge(pydot.Edge(nodes[start_state], nodes[end_state], **edge_attrs)) if add_start_state and machine.default_start_state: start = pydot.Node("__start__", shape="point", width="0.1", xlabel='start', fontcolor='green', **node_attrs) g.add_node(start) g.add_edge(pydot.Edge(start, nodes[machine.default_start_state], style='dotted')) return g ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/automaton/exceptions.py0000664000175000017500000000254500000000000020316 0ustar00zuulzuul00000000000000# Copyright (C) 2014 Yahoo! Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. class AutomatonException(Exception): """Base class for *most* exceptions emitted from this library.""" class InvalidState(AutomatonException): """Raised when a invalid state transition is attempted while executing.""" class NotInitialized(AutomatonException): """Error raised when an action is attempted on a not inited machine.""" class NotFound(AutomatonException): """Raised when some entry in some object doesn't exist.""" class Duplicate(AutomatonException): """Raised when a duplicate entry is found.""" class FrozenMachine(AutomatonException): """Exception raised when a frozen machine is modified.""" def __init__(self): super(FrozenMachine, self).__init__("Frozen machine can't be modified") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/automaton/machines.py0000664000175000017500000005612600000000000017730 0ustar00zuulzuul00000000000000# Copyright (C) 2014 Yahoo! Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import collections import prettytable from automaton import _utils as utils from automaton import exceptions as excp class State(object): """Container that defines needed components of a single state. Usage of this and the :meth:`~.FiniteMachine.build` make creating finite state machines that much easier. :ivar name: The name of the state. :ivar is_terminal: Whether this state is terminal (or not). :ivar next_states: Dictionary of 'event' -> 'next state name' (or none). :ivar on_enter: callback that will be called when the state is entered. :ivar on_exit: callback that will be called when the state is exited. """ def __init__(self, name, is_terminal=False, next_states=None, on_enter=None, on_exit=None): self.name = name self.is_terminal = bool(is_terminal) self.next_states = next_states self.on_enter = on_enter self.on_exit = on_exit def _convert_to_states(state_space): # NOTE(harlowja): if provided dicts, convert them... for state in state_space: if isinstance(state, dict): state = State(**state) yield state def _orderedkeys(data, sort=True): if sort: return sorted(data) else: return list(data) class _Jump(object): """A FSM transition tracks this data while jumping.""" def __init__(self, name, on_enter, on_exit): self.name = name self.on_enter = on_enter self.on_exit = on_exit class FiniteMachine(object): """A finite state machine. This state machine can be used to automatically run a given set of transitions and states in response to events (either from callbacks or from generator/iterator send() values, see PEP 342). On each triggered event, a ``on_enter`` and ``on_exit`` callback can also be provided which will be called to perform some type of action on leaving a prior state and before entering a new state. NOTE(harlowja): reactions will *only* be called when the generator/iterator from :py:meth:`~automaton.runners.Runner.run_iter` does *not* send back a new event (they will always be called if the :py:meth:`~automaton.runners.Runner.run` method is used). This allows for two unique ways (these ways can also be intermixed) to use this state machine when using :py:meth:`~automaton.runners.Runner.run`; one where *external* event trigger the next state transition and one where *internal* reaction callbacks trigger the next state transition. The other way to use this state machine is to skip using :py:meth:`~automaton.runners.Runner.run` or :py:meth:`~automaton.runners.Runner.run_iter` completely and use the :meth:`~.FiniteMachine.process_event` method explicitly and trigger the events via some *external* functionality/triggers... """ #: The result of processing an event (cause and effect...) Effect = collections.namedtuple('Effect', 'reaction,terminal') @classmethod def _effect_builder(cls, new_state, event): return cls.Effect(new_state['reactions'].get(event), new_state["terminal"]) def __init__(self): self._transitions = {} self._states = collections.OrderedDict() self._default_start_state = None self._current = None self.frozen = False @property def default_start_state(self): """Sets the *default* start state that the machine should use. NOTE(harlowja): this will be used by ``initialize`` but only if that function is not given its own ``start_state`` that overrides this default. """ return self._default_start_state @default_start_state.setter def default_start_state(self, state): if self.frozen: raise excp.FrozenMachine() if state not in self._states: raise excp.NotFound("Can not set the default start state to" " undefined state '%s'" % (state)) self._default_start_state = state @classmethod def build(cls, state_space): """Builds a machine from a state space listing. Each element of this list must be an instance of :py:class:`.State` or a ``dict`` with equivalent keys that can be used to construct a :py:class:`.State` instance. """ state_space = list(_convert_to_states(state_space)) m = cls() for state in state_space: m.add_state(state.name, terminal=state.is_terminal, on_enter=state.on_enter, on_exit=state.on_exit) for state in state_space: if state.next_states: for event, next_state in state.next_states.items(): if isinstance(next_state, State): next_state = next_state.name m.add_transition(state.name, next_state, event) return m @property def current_state(self): """The current state the machine is in (or none if not initialized).""" if self._current is not None: return self._current.name return None @property def terminated(self): """Returns whether the state machine is in a terminal state.""" if self._current is None: return False return self._states[self._current.name]['terminal'] def add_state(self, state, terminal=False, on_enter=None, on_exit=None): """Adds a given state to the state machine. The ``on_enter`` and ``on_exit`` callbacks, if provided will be expected to take two positional parameters, these being the state being exited (for ``on_exit``) or the state being entered (for ``on_enter``) and a second parameter which is the event that is being processed that caused the state transition. """ if self.frozen: raise excp.FrozenMachine() if state in self._states: raise excp.Duplicate("State '%s' already defined" % state) if on_enter is not None: if not callable(on_enter): raise ValueError("On enter callback must be callable") if on_exit is not None: if not callable(on_exit): raise ValueError("On exit callback must be callable") self._states[state] = { 'terminal': bool(terminal), 'reactions': {}, 'on_enter': on_enter, 'on_exit': on_exit, } self._transitions[state] = collections.OrderedDict() def is_actionable_event(self, event): """Check whether the event is actionable in the current state.""" current = self._current if current is None: return False if event not in self._transitions[current.name]: return False return True def add_reaction(self, state, event, reaction, *args, **kwargs): """Adds a reaction that may get triggered by the given event & state. Reaction callbacks may (depending on how the state machine is ran) be used after an event is processed (and a transition occurs) to cause the machine to react to the newly arrived at stable state. These callbacks are expected to accept three default positional parameters (although more can be passed in via *args and **kwargs, these will automatically get provided to the callback when it is activated *ontop* of the three default). The three default parameters are the last stable state, the new stable state and the event that caused the transition to this new stable state to be arrived at. The expected result of a callback is expected to be a new event that the callback wants the state machine to react to. This new event may (depending on how the state machine is ran) get processed (and this process typically repeats) until the state machine reaches a terminal state. """ if self.frozen: raise excp.FrozenMachine() if state not in self._states: raise excp.NotFound("Can not add a reaction to event '%s' for an" " undefined state '%s'" % (event, state)) if not callable(reaction): raise ValueError("Reaction callback must be callable") if event not in self._states[state]['reactions']: self._states[state]['reactions'][event] = (reaction, args, kwargs) else: raise excp.Duplicate("State '%s' reaction to event '%s'" " already defined" % (state, event)) def add_transition(self, start, end, event, replace=False): """Adds an allowed transition from start -> end for the given event. :param start: starting state :param end: ending state :param event: event that causes start state to transition to end state :param replace: replace existing event instead of raising a :py:class:`~automaton.exceptions.Duplicate` exception when the transition already exists. """ if self.frozen: raise excp.FrozenMachine() if start not in self._states: raise excp.NotFound("Can not add a transition on event '%s' that" " starts in a undefined state '%s'" % (event, start)) if end not in self._states: raise excp.NotFound("Can not add a transition on event '%s' that" " ends in a undefined state '%s'" % (event, end)) if self._states[start]['terminal']: raise excp.InvalidState("Can not add a transition on event '%s'" " that starts in the terminal state '%s'" % (event, start)) if event in self._transitions[start] and not replace: target = self._transitions[start][event] if target.name != end: raise excp.Duplicate("Cannot add transition from" " '%(start_state)s' to '%(end_state)s'" " on event '%(event)s' because a" " transition from '%(start_state)s'" " to '%(existing_end_state)s' on" " event '%(event)s' already exists." % {'existing_end_state': target.name, 'end_state': end, 'event': event, 'start_state': start}) else: target = _Jump(end, self._states[end]['on_enter'], self._states[start]['on_exit']) self._transitions[start][event] = target def _pre_process_event(self, event): current = self._current if current is None: raise excp.NotInitialized("Can not process event '%s'; the state" " machine hasn't been initialized" % event) if self._states[current.name]['terminal']: raise excp.InvalidState("Can not transition from terminal" " state '%s' on event '%s'" % (current.name, event)) if event not in self._transitions[current.name]: raise excp.NotFound("Can not transition from state '%s' on" " event '%s' (no defined transition)" % (current.name, event)) def _post_process_event(self, event, result): return result def process_event(self, event): """Trigger a state change in response to the provided event. :returns: Effect this is either a :py:class:`.FiniteMachine.Effect` or an ``Effect`` from a subclass of :py:class:`.FiniteMachine`. See the appropriate named tuple for a description of the actual items in the tuple. For example, :py:class:`.FiniteMachine.Effect`'s first item is ``reaction``: one could invoke this reaction's callback to react to the new stable state. :rtype: namedtuple """ self._pre_process_event(event) current = self._current replacement = self._transitions[current.name][event] if current.on_exit is not None: current.on_exit(current.name, event) if replacement.on_enter is not None: replacement.on_enter(replacement.name, event) self._current = replacement result = self._effect_builder(self._states[replacement.name], event) return self._post_process_event(event, result) def initialize(self, start_state=None): """Sets up the state machine (sets current state to start state...). :param start_state: explicit start state to use to initialize the state machine to. If ``None`` is provided then the machine's default start state will be used instead. """ if start_state is None: start_state = self._default_start_state if start_state not in self._states: raise excp.NotFound("Can not start from a undefined" " state '%s'" % (start_state)) if self._states[start_state]['terminal']: raise excp.InvalidState("Can not start from a terminal" " state '%s'" % (start_state)) # No on enter will be called, since we are priming the state machine # and have not really transitioned from anything to get here, we will # though allow on_exit to be called on the event that causes this # to be moved from... self._current = _Jump(start_state, None, self._states[start_state]['on_exit']) def copy(self, shallow=False, unfreeze=False): """Copies the current state machine. NOTE(harlowja): the copy will be left in an *uninitialized* state. NOTE(harlowja): when a shallow copy is requested the copy will share the same transition table and state table as the source; this can be advantageous if you have a machine and transitions + states that is defined somewhere and want to use copies to run with (the copies have the current state that is different between machines). """ c = type(self)() c._default_start_state = self._default_start_state if unfreeze and self.frozen: c.frozen = False else: c.frozen = self.frozen if not shallow: for state, data in self._states.items(): copied_data = data.copy() copied_data['reactions'] = copied_data['reactions'].copy() c._states[state] = copied_data for state, data in self._transitions.items(): c._transitions[state] = data.copy() else: c._transitions = self._transitions c._states = self._states return c def __contains__(self, state): """Returns if this state exists in the machines known states.""" return state in self._states def freeze(self): """Freezes & stops addition of states, transitions, reactions...""" self.frozen = True @property def states(self): """Returns the state names.""" return list(self._states) @property def events(self): """Returns how many events exist.""" c = 0 for state in self._states: c += len(self._transitions[state]) return c def __iter__(self): """Iterates over (start, event, end) transition tuples.""" for state in self._states: for event, target in self._transitions[state].items(): yield (state, event, target.name) def pformat(self, sort=True, empty='.'): """Pretty formats the state + transition table into a string. NOTE(harlowja): the sort parameter can be provided to sort the states and transitions by sort order; with it being provided as false the rows will be iterated in addition order instead. """ tbl = prettytable.PrettyTable(["Start", "Event", "End", "On Enter", "On Exit"]) for state in _orderedkeys(self._states, sort=sort): prefix_markings = [] if self.current_state == state: prefix_markings.append("@") postfix_markings = [] if self.default_start_state == state: postfix_markings.append("^") if self._states[state]['terminal']: postfix_markings.append("$") pretty_state = "%s%s" % ("".join(prefix_markings), state) if postfix_markings: pretty_state += "[%s]" % "".join(postfix_markings) if self._transitions[state]: for event in _orderedkeys(self._transitions[state], sort=sort): target = self._transitions[state][event] row = [pretty_state, event, target.name] if target.on_enter is not None: row.append(utils.get_callback_name(target.on_enter)) else: row.append(empty) if target.on_exit is not None: row.append(utils.get_callback_name(target.on_exit)) else: row.append(empty) tbl.add_row(row) else: on_enter = self._states[state]['on_enter'] if on_enter is not None: on_enter = utils.get_callback_name(on_enter) else: on_enter = empty on_exit = self._states[state]['on_exit'] if on_exit is not None: on_exit = utils.get_callback_name(on_exit) else: on_exit = empty tbl.add_row([pretty_state, empty, empty, on_enter, on_exit]) return tbl.get_string() class HierarchicalFiniteMachine(FiniteMachine): """A fsm that understands how to run in a hierarchical mode.""" #: The result of processing an event (cause and effect...) Effect = collections.namedtuple('Effect', 'reaction,terminal,machine') def __init__(self): super(HierarchicalFiniteMachine, self).__init__() self._nested_machines = {} @classmethod def _effect_builder(cls, new_state, event): return cls.Effect(new_state['reactions'].get(event), new_state["terminal"], new_state.get('machine')) def add_state(self, state, terminal=False, on_enter=None, on_exit=None, machine=None): """Adds a given state to the state machine. :param machine: the nested state machine that will be transitioned into when this state is entered :type machine: :py:class:`.FiniteMachine` Further arguments are interpreted as for :py:meth:`.FiniteMachine.add_state`. """ if machine is not None and not isinstance(machine, FiniteMachine): raise ValueError( "Nested state machines must themselves be state machines") super(HierarchicalFiniteMachine, self).add_state( state, terminal=terminal, on_enter=on_enter, on_exit=on_exit) if machine is not None: self._states[state]['machine'] = machine self._nested_machines[state] = machine def copy(self, shallow=False, unfreeze=False): c = super(HierarchicalFiniteMachine, self).copy(shallow=shallow, unfreeze=unfreeze) if shallow: c._nested_machines = self._nested_machines else: c._nested_machines = self._nested_machines.copy() return c def initialize(self, start_state=None, nested_start_state_fetcher=None): """Sets up the state machine (sets current state to start state...). :param start_state: explicit start state to use to initialize the state machine to. If ``None`` is provided then the machine's default start state will be used instead. :param nested_start_state_fetcher: A callback that can return start states for any nested machines **only**. If not ``None`` then it will be provided a single argument, the machine to provide a starting state for and it is expected to return a starting state (or ``None``) for each machine called with. Do note that this callback will also be passed to other nested state machines as well, so it will also be used to initialize any state machines they contain (recursively). """ super(HierarchicalFiniteMachine, self).initialize( start_state=start_state) for data in self._states.values(): if 'machine' in data: nested_machine = data['machine'] nested_start_state = None if nested_start_state_fetcher is not None: nested_start_state = nested_start_state_fetcher( nested_machine) if isinstance(nested_machine, HierarchicalFiniteMachine): nested_machine.initialize( start_state=nested_start_state, nested_start_state_fetcher=nested_start_state_fetcher) else: nested_machine.initialize(start_state=nested_start_state) @property def nested_machines(self): """Dictionary of **all** nested state machines this machine may use.""" return self._nested_machines ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/automaton/runners.py0000664000175000017500000001640100000000000017625 0ustar00zuulzuul00000000000000# Copyright (C) 2015 Yahoo! Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import abc from automaton import exceptions as excp from automaton import machines _JUMPER_NOT_FOUND_TPL = ("Unable to progress since no reaction (or" " sent event) has been made available in" " new state '%s' (moved to from state '%s'" " in response to event '%s')") class Runner(metaclass=abc.ABCMeta): """Machine runner used to run a state machine. Only **one** runner per machine should be active at the same time (aka there should not be multiple runners using the same machine instance at the same time). """ def __init__(self, machine): self._machine = machine @abc.abstractmethod def run(self, event, initialize=True): """Runs the state machine, using reactions only.""" @abc.abstractmethod def run_iter(self, event, initialize=True): """Returns a iterator/generator that will run the state machine. NOTE(harlowja): only one runner iterator/generator should be active for a machine, if this is not observed then it is possible for initialization and other local state to be corrupted and cause issues when running... """ class FiniteRunner(Runner): """Finite machine runner used to run a finite machine. Only **one** runner per machine should be active at the same time (aka there should not be multiple runners using the same machine instance at the same time). """ def __init__(self, machine): """Create a runner for the given machine.""" if not isinstance(machine, (machines.FiniteMachine,)): raise TypeError("FiniteRunner only works with FiniteMachine(s)") super(FiniteRunner, self).__init__(machine) def run(self, event, initialize=True): for transition in self.run_iter(event, initialize=initialize): pass def run_iter(self, event, initialize=True): if initialize: self._machine.initialize() while True: old_state = self._machine.current_state reaction, terminal = self._machine.process_event(event) new_state = self._machine.current_state try: sent_event = yield (old_state, new_state) except GeneratorExit: break if terminal: break if reaction is None and sent_event is None: raise excp.NotFound(_JUMPER_NOT_FOUND_TPL % (new_state, old_state, event)) elif sent_event is not None: event = sent_event else: cb, args, kwargs = reaction event = cb(old_state, new_state, event, *args, **kwargs) class HierarchicalRunner(Runner): """Hierarchical machine runner used to run a hierarchical machine. Only **one** runner per machine should be active at the same time (aka there should not be multiple runners using the same machine instance at the same time). """ def __init__(self, machine): """Create a runner for the given machine.""" if not isinstance(machine, (machines.HierarchicalFiniteMachine,)): raise TypeError("HierarchicalRunner only works with" " HierarchicalFiniteMachine(s)") super(HierarchicalRunner, self).__init__(machine) def run(self, event, initialize=True): for transition in self.run_iter(event, initialize=initialize): pass @staticmethod def _process_event(machines, event): """Matches a event to the machine hierarchy. If the lowest level machine does not handle the event, then the parent machine is referred to and so on, until there is only one machine left which *must* handle the event. The machine whose ``process_event`` does not throw invalid state or not found exceptions is expected to be the machine that should continue handling events... """ while True: machine = machines[-1] try: result = machine.process_event(event) except (excp.InvalidState, excp.NotFound): if len(machines) == 1: raise else: current = machine._current if current is not None and current.on_exit is not None: current.on_exit(current.name, event) machine._current = None machines.pop() else: return result def run_iter(self, event, initialize=True): """Returns a iterator/generator that will run the state machine. This will keep a stack (hierarchy) of machines active and jumps through them as needed (depending on which machine handles which event) during the running lifecycle. NOTE(harlowja): only one runner iterator/generator should be active for a machine hierarchy, if this is not observed then it is possible for initialization and other local state to be corrupted and causes issues when running... """ machines = [self._machine] if initialize: machines[-1].initialize() while True: old_state = machines[-1].current_state effect = self._process_event(machines, event) new_state = machines[-1].current_state try: machine = effect.machine except AttributeError: pass else: if machine is not None and machine is not machines[-1]: machine.initialize() machines.append(machine) try: sent_event = yield (old_state, new_state) except GeneratorExit: break if len(machines) == 1 and effect.terminal: # Only allow the top level machine to actually terminate the # execution, the rest of the nested machines must not handle # events if they wish to have the root machine terminate... break if effect.reaction is None and sent_event is None: raise excp.NotFound(_JUMPER_NOT_FOUND_TPL % (new_state, old_state, event)) elif sent_event is not None: event = sent_event else: cb, args, kwargs = effect.reaction event = cb(old_state, new_state, event, *args, **kwargs) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1693476054.0302355 automaton-3.2.0/automaton/tests/0000775000175000017500000000000000000000000016717 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/automaton/tests/__init__.py0000664000175000017500000000000000000000000021016 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/automaton/tests/test_fsm.py0000664000175000017500000004161600000000000021125 0ustar00zuulzuul00000000000000# Copyright (C) 2014 Yahoo! Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import collections import functools import random from automaton import exceptions as excp from automaton import machines from automaton import runners from testtools import testcase class FSMTest(testcase.TestCase): @staticmethod def _create_fsm(start_state, add_start=True, add_states=None): m = machines.FiniteMachine() if add_start: m.add_state(start_state) m.default_start_state = start_state if add_states: for s in add_states: if s in m: continue m.add_state(s) return m def setUp(self): super(FSMTest, self).setUp() # NOTE(harlowja): this state machine will never stop if run() is used. self.jumper = self._create_fsm("down", add_states=['up', 'down']) self.jumper.add_transition('down', 'up', 'jump') self.jumper.add_transition('up', 'down', 'fall') self.jumper.add_reaction('up', 'jump', lambda *args: 'fall') self.jumper.add_reaction('down', 'fall', lambda *args: 'jump') def test_build(self): space = [] for a in 'abc': space.append(machines.State(a)) m = machines.FiniteMachine.build(space) for a in 'abc': self.assertIn(a, m) def test_build_transitions(self): space = [ machines.State('down', is_terminal=False, next_states={'jump': 'up'}), machines.State('up', is_terminal=False, next_states={'fall': 'down'}), ] m = machines.FiniteMachine.build(space) m.default_start_state = 'down' expected = [('down', 'jump', 'up'), ('up', 'fall', 'down')] self.assertEqual(expected, list(m)) def test_build_transitions_with_callbacks(self): entered = collections.defaultdict(list) exitted = collections.defaultdict(list) def on_enter(state, event): entered[state].append(event) def on_exit(state, event): exitted[state].append(event) space = [ machines.State('down', is_terminal=False, next_states={'jump': 'up'}, on_enter=on_enter, on_exit=on_exit), machines.State('up', is_terminal=False, next_states={'fall': 'down'}, on_enter=on_enter, on_exit=on_exit), ] m = machines.FiniteMachine.build(space) m.default_start_state = 'down' expected = [('down', 'jump', 'up'), ('up', 'fall', 'down')] self.assertEqual(expected, list(m)) m.initialize() m.process_event('jump') self.assertEqual({'down': ['jump']}, dict(exitted)) self.assertEqual({'up': ['jump']}, dict(entered)) m.process_event('fall') self.assertEqual({'down': ['jump'], 'up': ['fall']}, dict(exitted)) self.assertEqual({'up': ['jump'], 'down': ['fall']}, dict(entered)) def test_build_transitions_dct(self): space = [ { 'name': 'down', 'is_terminal': False, 'next_states': {'jump': 'up'}, }, { 'name': 'up', 'is_terminal': False, 'next_states': {'fall': 'down'}, }, ] m = machines.FiniteMachine.build(space) m.default_start_state = 'down' expected = [('down', 'jump', 'up'), ('up', 'fall', 'down')] self.assertEqual(expected, list(m)) def test_build_terminal(self): space = [ machines.State('down', is_terminal=False, next_states={'jump': 'fell_over'}), machines.State('fell_over', is_terminal=True), ] m = machines.FiniteMachine.build(space) m.default_start_state = 'down' m.initialize() m.process_event('jump') self.assertTrue(m.terminated) def test_actionable(self): self.jumper.initialize() self.assertTrue(self.jumper.is_actionable_event('jump')) self.assertFalse(self.jumper.is_actionable_event('fall')) def test_bad_start_state(self): m = self._create_fsm('unknown', add_start=False) r = runners.FiniteRunner(m) self.assertRaises(excp.NotFound, r.run, 'unknown') def test_contains(self): m = self._create_fsm('unknown', add_start=False) self.assertNotIn('unknown', m) m.add_state('unknown') self.assertIn('unknown', m) def test_no_add_transition_terminal(self): m = self._create_fsm('up') m.add_state('down', terminal=True) self.assertRaises(excp.InvalidState, m.add_transition, 'down', 'up', 'jump') def test_duplicate_state(self): m = self._create_fsm('unknown') self.assertRaises(excp.Duplicate, m.add_state, 'unknown') def test_duplicate_transition(self): m = self.jumper m.add_state('side_ways') self.assertRaises(excp.Duplicate, m.add_transition, 'up', 'side_ways', 'fall') def test_duplicate_transition_replace(self): m = self.jumper m.add_state('side_ways') m.add_transition('up', 'side_ways', 'fall', replace=True) def test_duplicate_transition_same_transition(self): m = self.jumper m.add_transition('up', 'down', 'fall') def test_duplicate_reaction(self): self.assertRaises( # Currently duplicate reactions are not allowed... excp.Duplicate, self.jumper.add_reaction, 'down', 'fall', lambda *args: 'skate') def test_bad_transition(self): m = self._create_fsm('unknown') m.add_state('fire') self.assertRaises(excp.NotFound, m.add_transition, 'unknown', 'something', 'boom') self.assertRaises(excp.NotFound, m.add_transition, 'something', 'unknown', 'boom') def test_bad_reaction(self): m = self._create_fsm('unknown') self.assertRaises(excp.NotFound, m.add_reaction, 'something', 'boom', lambda *args: 'cough') def test_run(self): m = self._create_fsm('down', add_states=['up', 'down']) m.add_state('broken', terminal=True) m.add_transition('down', 'up', 'jump') m.add_transition('up', 'broken', 'hit-wall') m.add_reaction('up', 'jump', lambda *args: 'hit-wall') self.assertEqual(['broken', 'down', 'up'], sorted(m.states)) self.assertEqual(2, m.events) m.initialize() self.assertEqual('down', m.current_state) self.assertFalse(m.terminated) r = runners.FiniteRunner(m) r.run('jump') self.assertTrue(m.terminated) self.assertEqual('broken', m.current_state) self.assertRaises(excp.InvalidState, r.run, 'jump', initialize=False) def test_on_enter_on_exit(self): enter_transitions = [] exit_transitions = [] def on_exit(state, event): exit_transitions.append((state, event)) def on_enter(state, event): enter_transitions.append((state, event)) m = self._create_fsm('start', add_start=False) m.add_state('start', on_exit=on_exit) m.add_state('down', on_enter=on_enter, on_exit=on_exit) m.add_state('up', on_enter=on_enter, on_exit=on_exit) m.add_transition('start', 'down', 'beat') m.add_transition('down', 'up', 'jump') m.add_transition('up', 'down', 'fall') m.initialize('start') m.process_event('beat') m.process_event('jump') m.process_event('fall') self.assertEqual([('down', 'beat'), ('up', 'jump'), ('down', 'fall')], enter_transitions) self.assertEqual([('start', 'beat'), ('down', 'jump'), ('up', 'fall')], exit_transitions) def test_run_iter(self): up_downs = [] runner = runners.FiniteRunner(self.jumper) for (old_state, new_state) in runner.run_iter('jump'): up_downs.append((old_state, new_state)) if len(up_downs) >= 3: break self.assertEqual([('down', 'up'), ('up', 'down'), ('down', 'up')], up_downs) self.assertFalse(self.jumper.terminated) self.assertEqual('up', self.jumper.current_state) self.jumper.process_event('fall') self.assertEqual('down', self.jumper.current_state) def test_run_send(self): up_downs = [] runner = runners.FiniteRunner(self.jumper) it = runner.run_iter('jump') while True: up_downs.append(it.send(None)) if len(up_downs) >= 3: it.close() break self.assertEqual('up', self.jumper.current_state) self.assertFalse(self.jumper.terminated) self.assertEqual([('down', 'up'), ('up', 'down'), ('down', 'up')], up_downs) self.assertRaises(StopIteration, next, it) def test_run_send_fail(self): up_downs = [] runner = runners.FiniteRunner(self.jumper) it = runner.run_iter('jump') up_downs.append(next(it)) self.assertRaises(excp.NotFound, it.send, 'fail') it.close() self.assertEqual([('down', 'up')], up_downs) def test_not_initialized(self): self.assertRaises(excp.NotInitialized, self.jumper.process_event, 'jump') def test_copy_states(self): c = self._create_fsm('down', add_start=False) self.assertEqual(0, len(c.states)) d = c.copy() c.add_state('up') c.add_state('down') self.assertEqual(2, len(c.states)) self.assertEqual(0, len(d.states)) def test_copy_reactions(self): c = self._create_fsm('down', add_start=False) d = c.copy() c.add_state('down') c.add_state('up') c.add_reaction('down', 'jump', lambda *args: 'up') c.add_transition('down', 'up', 'jump') self.assertEqual(1, c.events) self.assertEqual(0, d.events) self.assertNotIn('down', d) self.assertNotIn('up', d) self.assertEqual([], list(d)) self.assertEqual([('down', 'jump', 'up')], list(c)) def test_copy_initialized(self): j = self.jumper.copy() self.assertIsNone(j.current_state) r = runners.FiniteRunner(self.jumper) for i, transition in enumerate(r.run_iter('jump')): if i == 4: break self.assertIsNone(j.current_state) self.assertIsNotNone(self.jumper.current_state) def test_iter(self): transitions = list(self.jumper) self.assertEqual(2, len(transitions)) self.assertIn(('up', 'fall', 'down'), transitions) self.assertIn(('down', 'jump', 'up'), transitions) def test_freeze(self): self.jumper.freeze() self.assertRaises(excp.FrozenMachine, self.jumper.add_state, 'test') self.assertRaises(excp.FrozenMachine, self.jumper.add_transition, 'test', 'test', 'test') self.assertRaises(excp.FrozenMachine, self.jumper.add_reaction, 'test', 'test', lambda *args: 'test') def test_freeze_copy_unfreeze(self): self.jumper.freeze() self.assertTrue(self.jumper.frozen) cp = self.jumper.copy(unfreeze=True) self.assertTrue(self.jumper.frozen) self.assertFalse(cp.frozen) def test_invalid_callbacks(self): m = self._create_fsm('working', add_states=['working', 'broken']) self.assertRaises(ValueError, m.add_state, 'b', on_enter=2) self.assertRaises(ValueError, m.add_state, 'b', on_exit=2) class HFSMTest(FSMTest): @staticmethod def _create_fsm(start_state, add_start=True, hierarchical=False, add_states=None): if hierarchical: m = machines.HierarchicalFiniteMachine() else: m = machines.FiniteMachine() if add_start: m.add_state(start_state) m.default_start_state = start_state if add_states: for s in add_states: if s not in m: m.add_state(s) return m def _make_phone_call(self, talk_time=1.0): def phone_reaction(old_state, new_state, event, chat_iter): try: next(chat_iter) except StopIteration: return 'finish' else: # Talk until the iterator expires... return 'chat' talker = self._create_fsm("talk") talker.add_transition("talk", "talk", "pickup") talker.add_transition("talk", "talk", "chat") talker.add_reaction("talk", "pickup", lambda *args: 'chat') chat_iter = iter(list(range(0, 10))) talker.add_reaction("talk", "chat", phone_reaction, chat_iter) handler = self._create_fsm('begin', hierarchical=True) handler.add_state("phone", machine=talker) handler.add_state('hangup', terminal=True) handler.add_transition("begin", "phone", "call") handler.add_reaction("phone", 'call', lambda *args: 'pickup') handler.add_transition("phone", "hangup", "finish") return handler def _make_phone_dialer(self): dialer = self._create_fsm("idle", hierarchical=True) digits = self._create_fsm("idle") dialer.add_state("pickup", machine=digits) dialer.add_transition("idle", "pickup", "dial") dialer.add_reaction("pickup", "dial", lambda *args: 'press') dialer.add_state("hangup", terminal=True) def react_to_press(last_state, new_state, event, number_calling): if len(number_calling) >= 10: return 'call' else: return 'press' digit_maker = functools.partial(random.randint, 0, 9) number_calling = [] digits.add_state( "accumulate", on_enter=lambda *args: number_calling.append(digit_maker())) digits.add_transition("idle", "accumulate", "press") digits.add_transition("accumulate", "accumulate", "press") digits.add_reaction("accumulate", "press", react_to_press, number_calling) digits.add_state("dial", terminal=True) digits.add_transition("accumulate", "dial", "call") digits.add_reaction("dial", "call", lambda *args: 'ringing') dialer.add_state("talk") dialer.add_transition("pickup", "talk", "ringing") dialer.add_reaction("talk", "ringing", lambda *args: 'hangup') dialer.add_transition("talk", "hangup", 'hangup') return dialer, number_calling def test_nested_machines(self): dialer, _number_calling = self._make_phone_dialer() self.assertEqual(1, len(dialer.nested_machines)) def test_nested_machine_initializers(self): dialer, _number_calling = self._make_phone_dialer() queried_for = [] def init_with(nested_machine): queried_for.append(nested_machine) return None dialer.initialize(nested_start_state_fetcher=init_with) self.assertEqual(1, len(queried_for)) def test_phone_dialer_iter(self): dialer, number_calling = self._make_phone_dialer() self.assertEqual(0, len(number_calling)) r = runners.HierarchicalRunner(dialer) transitions = list(r.run_iter('dial')) self.assertEqual(('talk', 'hangup'), transitions[-1]) self.assertEqual(len(number_calling), sum(1 if new_state == 'accumulate' else 0 for (old_state, new_state) in transitions)) self.assertEqual(10, len(number_calling)) def test_phone_call(self): handler = self._make_phone_call() r = runners.HierarchicalRunner(handler) r.run('call') self.assertTrue(handler.terminated) def test_phone_call_iter(self): handler = self._make_phone_call() r = runners.HierarchicalRunner(handler) transitions = list(r.run_iter('call')) self.assertEqual(('talk', 'hangup'), transitions[-1]) self.assertEqual(("begin", 'phone'), transitions[0]) talk_talk = 0 for transition in transitions: if transition == ("talk", "talk"): talk_talk += 1 self.assertGreater(talk_talk, 0) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1693476054.026235 automaton-3.2.0/automaton.egg-info/0000775000175000017500000000000000000000000017247 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476053.0 automaton-3.2.0/automaton.egg-info/PKG-INFO0000664000175000017500000000355200000000000020351 0ustar00zuulzuul00000000000000Metadata-Version: 1.2 Name: automaton Version: 3.2.0 Summary: Friendly state machines for python. Home-page: https://docs.openstack.org/automaton/latest/ Author: OpenStack Author-email: openstack-discuss@lists.openstack.org License: UNKNOWN Description: ========= Automaton ========= .. image:: https://img.shields.io/pypi/v/automaton.svg :target: https://pypi.org/project/automaton/ :alt: Latest Version .. image:: https://img.shields.io/pypi/dm/automaton.svg :target: https://pypi.org/project/automaton/ :alt: Downloads Friendly state machines for python. The goal of this library is to provide well documented state machine classes and associated utilities. The state machine pattern (or the implemented variation there-of) is a commonly used pattern and has a multitude of various usages. Some of the usages for this library include providing state & transition validation and running/scheduling/analyzing the execution of tasks. * Free software: Apache license * Documentation: https://docs.openstack.org/automaton/latest/ * Source: https://opendev.org/openstack/automaton * Bugs: https://bugs.launchpad.net/automaton Platform: UNKNOWN Classifier: Development Status :: 3 - Alpha Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Topic :: Software Development :: Libraries Requires-Python: >=3.8 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476054.0 automaton-3.2.0/automaton.egg-info/SOURCES.txt0000664000175000017500000000305400000000000021135 0ustar00zuulzuul00000000000000.coveragerc .pre-commit-config.yaml .stestr.conf .zuul.yaml AUTHORS CONTRIBUTING.rst ChangeLog HACKING.rst LICENSE README.rst bindep.txt requirements.txt setup.cfg setup.py test-requirements.txt tox.ini automaton/__init__.py automaton/_utils.py automaton/exceptions.py automaton/machines.py automaton/runners.py automaton.egg-info/PKG-INFO automaton.egg-info/SOURCES.txt automaton.egg-info/dependency_links.txt automaton.egg-info/not-zip-safe automaton.egg-info/pbr.json automaton.egg-info/requires.txt automaton.egg-info/top_level.txt automaton/converters/__init__.py automaton/converters/pydot.py automaton/tests/__init__.py automaton/tests/test_fsm.py doc/requirements.txt doc/source/conf.py doc/source/index.rst doc/source/contributor/index.rst doc/source/install/index.rst doc/source/reference/index.rst doc/source/user/examples.rst doc/source/user/features.rst doc/source/user/history.rst doc/source/user/index.rst releasenotes/notes/.placeholder releasenotes/notes/drop-python-2-7-73d3113c69d724d6.yaml releasenotes/source/2023.1.rst releasenotes/source/conf.py releasenotes/source/index.rst releasenotes/source/ocata.rst releasenotes/source/pike.rst releasenotes/source/queens.rst releasenotes/source/rocky.rst releasenotes/source/stein.rst releasenotes/source/train.rst releasenotes/source/unreleased.rst releasenotes/source/ussuri.rst releasenotes/source/victoria.rst releasenotes/source/wallaby.rst releasenotes/source/xena.rst releasenotes/source/yoga.rst releasenotes/source/zed.rst releasenotes/source/_static/.placeholder releasenotes/source/_templates/.placeholder././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476053.0 automaton-3.2.0/automaton.egg-info/dependency_links.txt0000664000175000017500000000000100000000000023315 0ustar00zuulzuul00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476053.0 automaton-3.2.0/automaton.egg-info/not-zip-safe0000664000175000017500000000000100000000000021475 0ustar00zuulzuul00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476053.0 automaton-3.2.0/automaton.egg-info/pbr.json0000664000175000017500000000005600000000000020726 0ustar00zuulzuul00000000000000{"git_version": "9255778", "is_release": true}././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476053.0 automaton-3.2.0/automaton.egg-info/requires.txt0000664000175000017500000000004600000000000021647 0ustar00zuulzuul00000000000000PrettyTable>=0.7.2 pbr!=2.1.0,>=2.0.0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476053.0 automaton-3.2.0/automaton.egg-info/top_level.txt0000664000175000017500000000001200000000000021772 0ustar00zuulzuul00000000000000automaton ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/bindep.txt0000664000175000017500000000035300000000000015551 0ustar00zuulzuul00000000000000# This is a cross-platform list tracking distribution packages needed for install and tests; # see https://docs.openstack.org/infra/bindep/ for additional information. graphviz [!platform:gentoo] media-gfx/graphviz [platform:gentoo] ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1693476054.0302355 automaton-3.2.0/doc/0000775000175000017500000000000000000000000014313 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/doc/requirements.txt0000664000175000017500000000052500000000000017601 0ustar00zuulzuul00000000000000# The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. # For generating sphinx documentation doc8>=0.6.0 # Apache-2.0 sphinx>=2.0.0,!=2.1.0 # BSD openstackdocstheme>=2.2.1 # Apache-2.0 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1693476054.0302355 automaton-3.2.0/doc/source/0000775000175000017500000000000000000000000015613 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/doc/source/conf.py0000664000175000017500000000510400000000000017112 0ustar00zuulzuul00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2020 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import os import sys sys.path.insert(0, os.path.abspath('../..')) # -- General configuration ---------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.inheritance_diagram', 'sphinx.ext.viewcode', 'openstackdocstheme', ] # openstackdocstheme options openstackdocs_repo_name = 'openstack/automaton' openstackdocs_auto_name = False openstackdocs_bug_project = 'automaton' openstackdocs_bug_tag = '' # autodoc generation is a bit aggressive and a nuisance when doing heavy # text edit cycles. # execute "export SPHINX_DEBUG=1" in your terminal to disable # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General information about the project. project = 'automaton' copyright = '2013, OpenStack Foundation' # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). add_module_names = True # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'native' # -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. # html_theme_path = ["."] # html_theme = '_theme' # html_static_path = ['static'] html_theme = 'openstackdocs' # Output file base name for HTML help builder. htmlhelp_basename = '%sdoc' % project # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass # [howto/manual]). latex_documents = [ ('index', '%s.tex' % project, '%s Documentation' % project, 'OpenStack Foundation', 'manual'), ] ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1693476054.0302355 automaton-3.2.0/doc/source/contributor/0000775000175000017500000000000000000000000020165 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/doc/source/contributor/index.rst0000664000175000017500000000011700000000000022025 0ustar00zuulzuul00000000000000============ Contributing ============ .. include:: ../../../CONTRIBUTING.rst ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/doc/source/index.rst0000664000175000017500000000052300000000000017454 0ustar00zuulzuul00000000000000===================================== Welcome to automaton's documentation! ===================================== Friendly state machines for python. .. toctree:: :maxdepth: 2 user/index reference/index install/index contributor/index .. rubric:: Indices and tables * :ref:`genindex` * :ref:`modindex` * :ref:`search` ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1693476054.0302355 automaton-3.2.0/doc/source/install/0000775000175000017500000000000000000000000017261 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/doc/source/install/index.rst0000664000175000017500000000030400000000000021117 0ustar00zuulzuul00000000000000============ Installation ============ At the command line:: $ pip install automaton Or, if you have virtualenvwrapper installed:: $ mkvirtualenv automaton $ pip install automaton ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1693476054.0302355 automaton-3.2.0/doc/source/reference/0000775000175000017500000000000000000000000017551 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/doc/source/reference/index.rst0000664000175000017500000000137400000000000021417 0ustar00zuulzuul00000000000000=== API === -------- Machines -------- .. autoclass:: automaton.machines.State :members: .. autoclass:: automaton.machines.FiniteMachine :members: :special-members: __iter__, __contains__ .. autoclass:: automaton.machines.HierarchicalFiniteMachine :noindex: :members: ------- Runners ------- .. autoclass:: automaton.runners.Runner :members: .. autoclass:: automaton.runners.FiniteRunner :members: .. autoclass:: automaton.runners.HierarchicalRunner :members: ---------- Converters ---------- .. automodule:: automaton.converters.pydot :members: ---------- Exceptions ---------- .. automodule:: automaton.exceptions :members: Hierarchy --------- .. inheritance-diagram:: automaton.exceptions :parts: 1 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1693476054.0302355 automaton-3.2.0/doc/source/user/0000775000175000017500000000000000000000000016571 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/doc/source/user/examples.rst0000664000175000017500000004731600000000000021154 0ustar00zuulzuul00000000000000======== Examples ======== ------------------------- Creating a simple machine ------------------------- .. testcode:: from automaton import machines m = machines.FiniteMachine() m.add_state('up') m.add_state('down') m.add_transition('down', 'up', 'jump') m.add_transition('up', 'down', 'fall') m.default_start_state = 'down' print(m.pformat()) **Expected output:** .. testoutput:: +---------+-------+------+----------+---------+ | Start | Event | End | On Enter | On Exit | +---------+-------+------+----------+---------+ | down[^] | jump | up | . | . | | up | fall | down | . | . | +---------+-------+------+----------+---------+ ------------------------------ Transitioning a simple machine ------------------------------ .. testcode:: m.initialize() m.process_event('jump') print(m.pformat()) print(m.current_state) print(m.terminated) m.process_event('fall') print(m.pformat()) print(m.current_state) print(m.terminated) **Expected output:** .. testoutput:: +---------+-------+------+----------+---------+ | Start | Event | End | On Enter | On Exit | +---------+-------+------+----------+---------+ | down[^] | jump | up | . | . | | @up | fall | down | . | . | +---------+-------+------+----------+---------+ up False +----------+-------+------+----------+---------+ | Start | Event | End | On Enter | On Exit | +----------+-------+------+----------+---------+ | @down[^] | jump | up | . | . | | up | fall | down | . | . | +----------+-------+------+----------+---------+ down False ------------------------------------- Running a complex dog-barking machine ------------------------------------- .. testcode:: from automaton import machines from automaton import runners # These reaction functions will get triggered when the registered state # and event occur, it is expected to provide a new event that reacts to the # new stable state (so that the state-machine can transition to a new # stable state, and repeat, until the machine ends up in a terminal # state, whereby it will stop...) def react_to_squirrel(old_state, new_state, event_that_triggered): return "gets petted" def react_to_wagging(old_state, new_state, event_that_triggered): return "gets petted" m = machines.FiniteMachine() m.add_state("sits") m.add_state("lies down", terminal=True) m.add_state("barks") m.add_state("wags tail") m.default_start_state = 'sits' m.add_transition("sits", "barks", "squirrel!") m.add_transition("barks", "wags tail", "gets petted") m.add_transition("wags tail", "lies down", "gets petted") m.add_reaction("barks", "squirrel!", react_to_squirrel) m.add_reaction('wags tail', "gets petted", react_to_wagging) print(m.pformat()) r = runners.FiniteRunner(m) for (old_state, new_state) in r.run_iter("squirrel!"): print("Leaving '%s'" % old_state) print("Entered '%s'" % new_state) **Expected output:** .. testoutput:: +--------------+-------------+-----------+----------+---------+ | Start | Event | End | On Enter | On Exit | +--------------+-------------+-----------+----------+---------+ | barks | gets petted | wags tail | . | . | | lies down[$] | . | . | . | . | | sits[^] | squirrel! | barks | . | . | | wags tail | gets petted | lies down | . | . | +--------------+-------------+-----------+----------+---------+ Leaving 'sits' Entered 'barks' Leaving 'barks' Entered 'wags tail' Leaving 'wags tail' Entered 'lies down' ------------------------------------ Creating a complex CD-player machine ------------------------------------ .. testcode:: from automaton import machines def print_on_enter(new_state, triggered_event): print("Entered '%s' due to '%s'" % (new_state, triggered_event)) def print_on_exit(old_state, triggered_event): print("Exiting '%s' due to '%s'" % (old_state, triggered_event)) m = machines.FiniteMachine() m.add_state('stopped', on_enter=print_on_enter, on_exit=print_on_exit) m.add_state('opened', on_enter=print_on_enter, on_exit=print_on_exit) m.add_state('closed', on_enter=print_on_enter, on_exit=print_on_exit) m.add_state('playing', on_enter=print_on_enter, on_exit=print_on_exit) m.add_state('paused', on_enter=print_on_enter, on_exit=print_on_exit) m.add_transition('stopped', 'playing', 'play') m.add_transition('stopped', 'opened', 'open_close') m.add_transition('stopped', 'stopped', 'stop') m.add_transition('opened', 'closed', 'open_close') m.add_transition('closed', 'opened', 'open_close') m.add_transition('closed', 'stopped', 'cd_detected') m.add_transition('playing', 'stopped', 'stop') m.add_transition('playing', 'paused', 'pause') m.add_transition('playing', 'opened', 'open_close') m.add_transition('paused', 'playing', 'play') m.add_transition('paused', 'stopped', 'stop') m.add_transition('paused', 'opened', 'open_close') m.default_start_state = 'closed' m.initialize() print(m.pformat()) for event in ['cd_detected', 'play', 'pause', 'play', 'stop', 'open_close', 'open_close']: m.process_event(event) print(m.pformat()) print("=============") print("Current state => %s" % m.current_state) print("=============") **Expected output:** .. testoutput:: +------------+-------------+---------+----------------+---------------+ | Start | Event | End | On Enter | On Exit | +------------+-------------+---------+----------------+---------------+ | @closed[^] | cd_detected | stopped | print_on_enter | print_on_exit | | @closed[^] | open_close | opened | print_on_enter | print_on_exit | | opened | open_close | closed | print_on_enter | print_on_exit | | paused | open_close | opened | print_on_enter | print_on_exit | | paused | play | playing | print_on_enter | print_on_exit | | paused | stop | stopped | print_on_enter | print_on_exit | | playing | open_close | opened | print_on_enter | print_on_exit | | playing | pause | paused | print_on_enter | print_on_exit | | playing | stop | stopped | print_on_enter | print_on_exit | | stopped | open_close | opened | print_on_enter | print_on_exit | | stopped | play | playing | print_on_enter | print_on_exit | | stopped | stop | stopped | print_on_enter | print_on_exit | +------------+-------------+---------+----------------+---------------+ Exiting 'closed' due to 'cd_detected' Entered 'stopped' due to 'cd_detected' +-----------+-------------+---------+----------------+---------------+ | Start | Event | End | On Enter | On Exit | +-----------+-------------+---------+----------------+---------------+ | closed[^] | cd_detected | stopped | print_on_enter | print_on_exit | | closed[^] | open_close | opened | print_on_enter | print_on_exit | | opened | open_close | closed | print_on_enter | print_on_exit | | paused | open_close | opened | print_on_enter | print_on_exit | | paused | play | playing | print_on_enter | print_on_exit | | paused | stop | stopped | print_on_enter | print_on_exit | | playing | open_close | opened | print_on_enter | print_on_exit | | playing | pause | paused | print_on_enter | print_on_exit | | playing | stop | stopped | print_on_enter | print_on_exit | | @stopped | open_close | opened | print_on_enter | print_on_exit | | @stopped | play | playing | print_on_enter | print_on_exit | | @stopped | stop | stopped | print_on_enter | print_on_exit | +-----------+-------------+---------+----------------+---------------+ ============= Current state => stopped ============= Exiting 'stopped' due to 'play' Entered 'playing' due to 'play' +-----------+-------------+---------+----------------+---------------+ | Start | Event | End | On Enter | On Exit | +-----------+-------------+---------+----------------+---------------+ | closed[^] | cd_detected | stopped | print_on_enter | print_on_exit | | closed[^] | open_close | opened | print_on_enter | print_on_exit | | opened | open_close | closed | print_on_enter | print_on_exit | | paused | open_close | opened | print_on_enter | print_on_exit | | paused | play | playing | print_on_enter | print_on_exit | | paused | stop | stopped | print_on_enter | print_on_exit | | @playing | open_close | opened | print_on_enter | print_on_exit | | @playing | pause | paused | print_on_enter | print_on_exit | | @playing | stop | stopped | print_on_enter | print_on_exit | | stopped | open_close | opened | print_on_enter | print_on_exit | | stopped | play | playing | print_on_enter | print_on_exit | | stopped | stop | stopped | print_on_enter | print_on_exit | +-----------+-------------+---------+----------------+---------------+ ============= Current state => playing ============= Exiting 'playing' due to 'pause' Entered 'paused' due to 'pause' +-----------+-------------+---------+----------------+---------------+ | Start | Event | End | On Enter | On Exit | +-----------+-------------+---------+----------------+---------------+ | closed[^] | cd_detected | stopped | print_on_enter | print_on_exit | | closed[^] | open_close | opened | print_on_enter | print_on_exit | | opened | open_close | closed | print_on_enter | print_on_exit | | @paused | open_close | opened | print_on_enter | print_on_exit | | @paused | play | playing | print_on_enter | print_on_exit | | @paused | stop | stopped | print_on_enter | print_on_exit | | playing | open_close | opened | print_on_enter | print_on_exit | | playing | pause | paused | print_on_enter | print_on_exit | | playing | stop | stopped | print_on_enter | print_on_exit | | stopped | open_close | opened | print_on_enter | print_on_exit | | stopped | play | playing | print_on_enter | print_on_exit | | stopped | stop | stopped | print_on_enter | print_on_exit | +-----------+-------------+---------+----------------+---------------+ ============= Current state => paused ============= Exiting 'paused' due to 'play' Entered 'playing' due to 'play' +-----------+-------------+---------+----------------+---------------+ | Start | Event | End | On Enter | On Exit | +-----------+-------------+---------+----------------+---------------+ | closed[^] | cd_detected | stopped | print_on_enter | print_on_exit | | closed[^] | open_close | opened | print_on_enter | print_on_exit | | opened | open_close | closed | print_on_enter | print_on_exit | | paused | open_close | opened | print_on_enter | print_on_exit | | paused | play | playing | print_on_enter | print_on_exit | | paused | stop | stopped | print_on_enter | print_on_exit | | @playing | open_close | opened | print_on_enter | print_on_exit | | @playing | pause | paused | print_on_enter | print_on_exit | | @playing | stop | stopped | print_on_enter | print_on_exit | | stopped | open_close | opened | print_on_enter | print_on_exit | | stopped | play | playing | print_on_enter | print_on_exit | | stopped | stop | stopped | print_on_enter | print_on_exit | +-----------+-------------+---------+----------------+---------------+ ============= Current state => playing ============= Exiting 'playing' due to 'stop' Entered 'stopped' due to 'stop' +-----------+-------------+---------+----------------+---------------+ | Start | Event | End | On Enter | On Exit | +-----------+-------------+---------+----------------+---------------+ | closed[^] | cd_detected | stopped | print_on_enter | print_on_exit | | closed[^] | open_close | opened | print_on_enter | print_on_exit | | opened | open_close | closed | print_on_enter | print_on_exit | | paused | open_close | opened | print_on_enter | print_on_exit | | paused | play | playing | print_on_enter | print_on_exit | | paused | stop | stopped | print_on_enter | print_on_exit | | playing | open_close | opened | print_on_enter | print_on_exit | | playing | pause | paused | print_on_enter | print_on_exit | | playing | stop | stopped | print_on_enter | print_on_exit | | @stopped | open_close | opened | print_on_enter | print_on_exit | | @stopped | play | playing | print_on_enter | print_on_exit | | @stopped | stop | stopped | print_on_enter | print_on_exit | +-----------+-------------+---------+----------------+---------------+ ============= Current state => stopped ============= Exiting 'stopped' due to 'open_close' Entered 'opened' due to 'open_close' +-----------+-------------+---------+----------------+---------------+ | Start | Event | End | On Enter | On Exit | +-----------+-------------+---------+----------------+---------------+ | closed[^] | cd_detected | stopped | print_on_enter | print_on_exit | | closed[^] | open_close | opened | print_on_enter | print_on_exit | | @opened | open_close | closed | print_on_enter | print_on_exit | | paused | open_close | opened | print_on_enter | print_on_exit | | paused | play | playing | print_on_enter | print_on_exit | | paused | stop | stopped | print_on_enter | print_on_exit | | playing | open_close | opened | print_on_enter | print_on_exit | | playing | pause | paused | print_on_enter | print_on_exit | | playing | stop | stopped | print_on_enter | print_on_exit | | stopped | open_close | opened | print_on_enter | print_on_exit | | stopped | play | playing | print_on_enter | print_on_exit | | stopped | stop | stopped | print_on_enter | print_on_exit | +-----------+-------------+---------+----------------+---------------+ ============= Current state => opened ============= Exiting 'opened' due to 'open_close' Entered 'closed' due to 'open_close' +------------+-------------+---------+----------------+---------------+ | Start | Event | End | On Enter | On Exit | +------------+-------------+---------+----------------+---------------+ | @closed[^] | cd_detected | stopped | print_on_enter | print_on_exit | | @closed[^] | open_close | opened | print_on_enter | print_on_exit | | opened | open_close | closed | print_on_enter | print_on_exit | | paused | open_close | opened | print_on_enter | print_on_exit | | paused | play | playing | print_on_enter | print_on_exit | | paused | stop | stopped | print_on_enter | print_on_exit | | playing | open_close | opened | print_on_enter | print_on_exit | | playing | pause | paused | print_on_enter | print_on_exit | | playing | stop | stopped | print_on_enter | print_on_exit | | stopped | open_close | opened | print_on_enter | print_on_exit | | stopped | play | playing | print_on_enter | print_on_exit | | stopped | stop | stopped | print_on_enter | print_on_exit | +------------+-------------+---------+----------------+---------------+ ============= Current state => closed ============= ---------------------------------------------------------- Creating a complex CD-player machine (using a state-space) ---------------------------------------------------------- This example is equivalent to the prior one but creates a machine in a more declarative manner. Instead of calling ``add_state`` and ``add_transition`` a explicit and declarative format can be used. For example to create the same machine: .. testcode:: from automaton import machines def print_on_enter(new_state, triggered_event): print("Entered '%s' due to '%s'" % (new_state, triggered_event)) def print_on_exit(old_state, triggered_event): print("Exiting '%s' due to '%s'" % (old_state, triggered_event)) # This will contain all the states and transitions that our machine will # allow, the format is relatively simple and designed to be easy to use. state_space = [ { 'name': 'stopped', 'next_states': { # On event 'play' transition to the 'playing' state. 'play': 'playing', 'open_close': 'opened', 'stop': 'stopped', }, 'on_enter': print_on_enter, 'on_exit': print_on_exit, }, { 'name': 'opened', 'next_states': { 'open_close': 'closed', }, 'on_enter': print_on_enter, 'on_exit': print_on_exit, }, { 'name': 'closed', 'next_states': { 'open_close': 'opened', 'cd_detected': 'stopped', }, 'on_enter': print_on_enter, 'on_exit': print_on_exit, }, { 'name': 'playing', 'next_states': { 'stop': 'stopped', 'pause': 'paused', 'open_close': 'opened', }, 'on_enter': print_on_enter, 'on_exit': print_on_exit, }, { 'name': 'paused', 'next_states': { 'play': 'playing', 'stop': 'stopped', 'open_close': 'opened', }, 'on_enter': print_on_enter, 'on_exit': print_on_exit, }, ] m = machines.FiniteMachine.build(state_space) m.default_start_state = 'closed' print(m.pformat()) **Expected output:** .. testoutput:: +-----------+-------------+---------+----------------+---------------+ | Start | Event | End | On Enter | On Exit | +-----------+-------------+---------+----------------+---------------+ | closed[^] | cd_detected | stopped | print_on_enter | print_on_exit | | closed[^] | open_close | opened | print_on_enter | print_on_exit | | opened | open_close | closed | print_on_enter | print_on_exit | | paused | open_close | opened | print_on_enter | print_on_exit | | paused | play | playing | print_on_enter | print_on_exit | | paused | stop | stopped | print_on_enter | print_on_exit | | playing | open_close | opened | print_on_enter | print_on_exit | | playing | pause | paused | print_on_enter | print_on_exit | | playing | stop | stopped | print_on_enter | print_on_exit | | stopped | open_close | opened | print_on_enter | print_on_exit | | stopped | play | playing | print_on_enter | print_on_exit | | stopped | stop | stopped | print_on_enter | print_on_exit | +-----------+-------------+---------+----------------+---------------+ .. note:: As can be seen the two tables from this example and the prior one are exactly the same. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/doc/source/user/features.rst0000664000175000017500000000057700000000000021152 0ustar00zuulzuul00000000000000======== Features ======== Machines -------- * A :py:class:`.automaton.machines.FiniteMachine` state machine. * A :py:class:`.automaton.machines.HierarchicalFiniteMachine` hierarchical state machine. Runners ------- * A :py:class:`.automaton.runners.FiniteRunner` state machine runner. * A :py:class:`.automaton.runners.HierarchicalRunner` hierarchical state machine runner. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/doc/source/user/history.rst0000664000175000017500000000004100000000000021017 0ustar00zuulzuul00000000000000.. include:: ../../../ChangeLog ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/doc/source/user/index.rst0000664000175000017500000000020100000000000020423 0ustar00zuulzuul00000000000000==================== automaton User Guide ==================== .. toctree:: :maxdepth: 2 features examples history ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1693476054.0222347 automaton-3.2.0/releasenotes/0000775000175000017500000000000000000000000016237 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1693476054.0302355 automaton-3.2.0/releasenotes/notes/0000775000175000017500000000000000000000000017367 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/releasenotes/notes/.placeholder0000664000175000017500000000000000000000000021640 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/releasenotes/notes/drop-python-2-7-73d3113c69d724d6.yaml0000664000175000017500000000021000000000000025112 0ustar00zuulzuul00000000000000--- upgrade: - | Python 2.7 support has been dropped. The minimum version of Python now supported by automaton is Python 3.6. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1693476054.0342357 automaton-3.2.0/releasenotes/source/0000775000175000017500000000000000000000000017537 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/releasenotes/source/2023.1.rst0000664000175000017500000000020200000000000021010 0ustar00zuulzuul00000000000000=========================== 2023.1 Series Release Notes =========================== .. release-notes:: :branch: stable/2023.1 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1693476054.0342357 automaton-3.2.0/releasenotes/source/_static/0000775000175000017500000000000000000000000021165 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/releasenotes/source/_static/.placeholder0000664000175000017500000000000000000000000023436 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1693476054.0342357 automaton-3.2.0/releasenotes/source/_templates/0000775000175000017500000000000000000000000021674 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/releasenotes/source/_templates/.placeholder0000664000175000017500000000000000000000000024145 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/releasenotes/source/conf.py0000664000175000017500000002175600000000000021051 0ustar00zuulzuul00000000000000# -*- coding: utf-8 -*- # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. # automaton Release Notes documentation build configuration file, created by # sphinx-quickstart on Tue Nov 3 17:40:50 2015. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'openstackdocstheme', 'reno.sphinxext', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = 'automaton Release Notes' copyright = '2016, automaton Developers' # Release notes do not need a version number in the title, they # cover multiple releases. release = '' # The short X.Y version. version = '' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all # documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'native' # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'openstackdocs' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'automatonReleaseNotesdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ('index', 'automatonReleaseNotes.tex', 'automaton Release Notes Documentation', 'automaton Developers', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'automatonreleasenotes', 'automaton Release Notes Documentation', ['automaton Developers'], 1) ] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'automatonReleaseNotes', 'automaton Release Notes Documentation', 'automaton Developers', 'automatonReleaseNotes', 'An OpenStack library for parsing configuration options from the command' ' line and configuration files.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # texinfo_no_detailmenu = False # -- Options for Internationalization output ------------------------------ locale_dirs = ['locale/'] # openstackdocstheme options openstackdocs_repo_name = 'openstack/automaton' openstackdocs_auto_name = False openstackdocs_bug_project = 'automaton' openstackdocs_bug_tag = '' ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/releasenotes/source/index.rst0000664000175000017500000000036600000000000021405 0ustar00zuulzuul00000000000000=========================== automaton Release Notes =========================== .. toctree:: :maxdepth: 1 unreleased 2023.1 zed yoga xena wallaby victoria ussuri train stein rocky queens pike ocata ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/releasenotes/source/ocata.rst0000664000175000017500000000023000000000000021353 0ustar00zuulzuul00000000000000=================================== Ocata Series Release Notes =================================== .. release-notes:: :branch: origin/stable/ocata ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/releasenotes/source/pike.rst0000664000175000017500000000021700000000000021221 0ustar00zuulzuul00000000000000=================================== Pike Series Release Notes =================================== .. release-notes:: :branch: stable/pike ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/releasenotes/source/queens.rst0000664000175000017500000000022300000000000021566 0ustar00zuulzuul00000000000000=================================== Queens Series Release Notes =================================== .. release-notes:: :branch: stable/queens ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/releasenotes/source/rocky.rst0000664000175000017500000000022100000000000021413 0ustar00zuulzuul00000000000000=================================== Rocky Series Release Notes =================================== .. release-notes:: :branch: stable/rocky ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/releasenotes/source/stein.rst0000664000175000017500000000022100000000000021406 0ustar00zuulzuul00000000000000=================================== Stein Series Release Notes =================================== .. release-notes:: :branch: stable/stein ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/releasenotes/source/train.rst0000664000175000017500000000017600000000000021412 0ustar00zuulzuul00000000000000========================== Train Series Release Notes ========================== .. release-notes:: :branch: stable/train ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/releasenotes/source/unreleased.rst0000664000175000017500000000014400000000000022417 0ustar00zuulzuul00000000000000========================== Unreleased Release Notes ========================== .. release-notes:: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/releasenotes/source/ussuri.rst0000664000175000017500000000020200000000000021615 0ustar00zuulzuul00000000000000=========================== Ussuri Series Release Notes =========================== .. release-notes:: :branch: stable/ussuri ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/releasenotes/source/victoria.rst0000664000175000017500000000021200000000000022104 0ustar00zuulzuul00000000000000============================= Victoria Series Release Notes ============================= .. release-notes:: :branch: stable/victoria ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/releasenotes/source/wallaby.rst0000664000175000017500000000020600000000000021722 0ustar00zuulzuul00000000000000============================ Wallaby Series Release Notes ============================ .. release-notes:: :branch: stable/wallaby ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/releasenotes/source/xena.rst0000664000175000017500000000017200000000000021224 0ustar00zuulzuul00000000000000========================= Xena Series Release Notes ========================= .. release-notes:: :branch: stable/xena ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/releasenotes/source/yoga.rst0000664000175000017500000000017200000000000021230 0ustar00zuulzuul00000000000000========================= Yoga Series Release Notes ========================= .. release-notes:: :branch: stable/yoga ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/releasenotes/source/zed.rst0000664000175000017500000000016600000000000021056 0ustar00zuulzuul00000000000000======================== Zed Series Release Notes ======================== .. release-notes:: :branch: stable/zed ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/requirements.txt0000664000175000017500000000061100000000000017030 0ustar00zuulzuul00000000000000# The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. # See: https://bugs.launchpad.net/pbr/+bug/1384919 for why this is here... pbr!=2.1.0,>=2.0.0 # Apache-2.0 # For pretty formatting machines/state tables... PrettyTable>=0.7.2 # BSD ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1693476054.0342357 automaton-3.2.0/setup.cfg0000664000175000017500000000136200000000000015371 0ustar00zuulzuul00000000000000[metadata] name = automaton summary = Friendly state machines for python. author = OpenStack author_email = openstack-discuss@lists.openstack.org home_page = https://docs.openstack.org/automaton/latest/ description_file = README.rst python_requires = >=3.8 classifier = Development Status :: 3 - Alpha Intended Audience :: Developers License :: OSI Approved :: Apache Software License Operating System :: POSIX Programming Language :: Python :: 3 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3 :: Only Programming Language :: Python :: Implementation :: CPython Topic :: Software Development :: Libraries [files] packages = automaton [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/setup.py0000664000175000017500000000127100000000000015261 0ustar00zuulzuul00000000000000# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import setuptools setuptools.setup( setup_requires=['pbr>=2.0.0'], pbr=True) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/test-requirements.txt0000664000175000017500000000053500000000000020012 0ustar00zuulzuul00000000000000# The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. coverage!=4.4,>=4.0 # Apache-2.0 oslotest>=3.2.0 # Apache-2.0 stestr>=2.0.0 # Apache-2.0 testtools>=2.2.0 # MIT reno>=3.1.0 # Apache-2.0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1693476025.0 automaton-3.2.0/tox.ini0000664000175000017500000000340100000000000015057 0ustar00zuulzuul00000000000000[tox] minversion = 3.1.1 envlist = py3,docs,pep8 ignore_basepython_conflict = true [testenv] basepython = python3 setenv = OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 OS_TEST_TIMEOUT=60 PYTHONDONTWRITEBYTECODE=1 deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/test-requirements.txt -r{toxinidir}/requirements.txt commands = stestr run --slowest {posargs} [testenv:pep8] deps = pre-commit>=2.6.0 # MIT commands = pre-commit run -a [testenv:venv] commands = {posargs} [testenv:cover] setenv = {[testenv]setenv} PYTHON=coverage run --source automaton --parallel-mode commands = coverage erase stestr run {posargs} coverage combine coverage html -d cover coverage xml -o cover/coverage.xml coverage report [testenv:docs] deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/doc/requirements.txt commands = doc8 --ignore-path "doc/source/history.rst" doc/source sphinx-build -a -E -W -b html doc/source doc/build/html [testenv:releasenotes] deps = {[testenv:docs]deps} commands = sphinx-build -a -E -W -b html releasenotes/source releasenotes/build/html [testenv:bindep] # Do not install any requirements. We want this to be fast and work even if # system dependencies are missing, since it's used to tell you what system # dependencies are missing! This also means that bindep must be installed # separately, outside of the requirements files, and develop mode disabled # explicitly to avoid unnecessarily installing the checked-out repo too (this # further relies on "tox.skipsdist = True" above). deps = bindep commands = bindep test usedevelop = False [flake8] show-source = True exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build