{ "info": { "author": "Glyph", "author_email": "glyph@twistedmatrix.com", "bugtrack_url": null, "classifiers": [ "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8" ], "description": "Automat\n=======\n\n\n.. image:: https://readthedocs.org/projects/automat/badge/?version=latest\n :target: http://automat.readthedocs.io/en/latest/\n :alt: Documentation Status\n\n\n.. image:: https://travis-ci.org/glyph/automat.svg?branch=master\n :target: https://travis-ci.org/glyph/automat\n :alt: Build Status\n\n\n.. image:: https://coveralls.io/repos/glyph/automat/badge.png\n :target: https://coveralls.io/r/glyph/automat\n :alt: Coverage Status\n\n\nSelf-service finite-state machines for the programmer on the go.\n----------------------------------------------------------------\n\nAutomat is a library for concise, idiomatic Python expression of finite-state\nautomata (particularly deterministic finite-state transducers).\n\nRead more here, or on `Read the Docs `_\\ , or watch the following videos for an overview and presentation\n\nOverview and presentation by **Glyph Lefkowitz** at the first talk of the first Pyninsula meetup, on February 21st, 2017:\n\n.. image:: https://img.youtube.com/vi/0wOZBpD1VVk/0.jpg\n :target: https://www.youtube.com/watch?v=0wOZBpD1VVk\n :alt: Glyph Lefkowitz - Automat - Pyninsula #0\n\n\nPresentation by **Clinton Roy** at PyCon Australia, on August 6th 2017:\n\n.. image:: https://img.youtube.com/vi/TedUKXhu9kE/0.jpg\n :target: https://www.youtube.com/watch?v=TedUKXhu9kE\n :alt: Clinton Roy - State Machines - Pycon Australia 2017\n\n\nWhy use state machines?\n^^^^^^^^^^^^^^^^^^^^^^^\n\nSometimes you have to create an object whose behavior varies with its state,\nbut still wishes to present a consistent interface to its callers.\n\nFor example, let's say you're writing the software for a coffee machine. It\nhas a lid that can be opened or closed, a chamber for water, a chamber for\ncoffee beans, and a button for \"brew\".\n\nThere are a number of possible states for the coffee machine. It might or\nmight not have water. It might or might not have beans. The lid might be open\nor closed. The \"brew\" button should only actually attempt to brew coffee in\none of these configurations, and the \"open lid\" button should only work if the\ncoffee is not, in fact, brewing.\n\nWith diligence and attention to detail, you can implement this correctly using\na collection of attributes on an object; ``has_water``\\ , ``has_beans``\\ ,\n``is_lid_open`` and so on. However, you have to keep all these attributes\nconsistent. As the coffee maker becomes more complex - perhaps you add an\nadditional chamber for flavorings so you can make hazelnut coffee, for\nexample - you have to keep adding more and more checks and more and more\nreasoning about which combinations of states are allowed.\n\nRather than adding tedious 'if' checks to every single method to make sure that\neach of these flags are exactly what you expect, you can use a state machine to\nensure that if your code runs at all, it will be run with all the required\nvalues initialized, because they have to be called in the order you declare\nthem.\n\nYou can read about state machines and their advantages for Python programmers\nin considerably more detail\n`in this excellent series of articles from ClusterHQ `_.\n\nWhat makes Automat different?\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nThere are\n`dozens of libraries on PyPI implementing state machines `_.\nSo it behooves me to say why yet another one would be a good idea.\n\nAutomat is designed around this principle: while organizing your code around\nstate machines is a good idea, your callers don't, and shouldn't have to, care\nthat you've done so. In Python, the \"input\" to a stateful system is a method\ncall; the \"output\" may be a method call, if you need to invoke a side effect,\nor a return value, if you are just performing a computation in memory. Most\nother state-machine libraries require you to explicitly create an input object,\nprovide that object to a generic \"input\" method, and then receive results,\nsometimes in terms of that library's interfaces and sometimes in terms of\nclasses you define yourself.\n\nFor example, a snippet of the coffee-machine example above might be implemented\nas follows in naive Python:\n\n.. code-block:: python\n\n class CoffeeMachine(object):\n def brew_button(self):\n if self.has_water and self.has_beans and not self.is_lid_open:\n self.heat_the_heating_element()\n # ...\n\nWith Automat, you'd create a class with a ``MethodicalMachine`` attribute:\n\n.. code-block:: python\n\n from automat import MethodicalMachine\n\n class CoffeeBrewer(object):\n _machine = MethodicalMachine()\n\nand then you would break the above logic into two pieces - the ``brew_button``\n*input*\\ , declared like so:\n\n.. code-block:: python\n\n @_machine.input()\n def brew_button(self):\n \"The user pressed the 'brew' button.\"\n\nIt wouldn't do any good to declare a method *body* on this, however, because\ninput methods don't actually execute their bodies when called; doing actual\nwork is the *output*\\ 's job:\n\n.. code-block:: python\n\n @_machine.output()\n def _heat_the_heating_element(self):\n \"Heat up the heating element, which should cause coffee to happen.\"\n self._heating_element.turn_on()\n\nAs well as a couple of *states* - and for simplicity's sake let's say that the\nonly two states are ``have_beans`` and ``dont_have_beans``\\ :\n\n.. code-block:: python\n\n @_machine.state()\n def have_beans(self):\n \"In this state, you have some beans.\"\n @_machine.state(initial=True)\n def dont_have_beans(self):\n \"In this state, you don't have any beans.\"\n\n``dont_have_beans`` is the ``initial`` state because ``CoffeeBrewer`` starts without beans\nin it.\n\n(And another input to put some beans in:)\n\n.. code-block:: python\n\n @_machine.input()\n def put_in_beans(self):\n \"The user put in some beans.\"\n\nFinally, you hook everything together with the ``upon`` method of the functions\ndecorated with ``_machine.state``\\ :\n\n.. code-block:: python\n\n\n # When we don't have beans, upon putting in beans, we will then have beans\n # (and produce no output)\n dont_have_beans.upon(put_in_beans, enter=have_beans, outputs=[])\n\n # When we have beans, upon pressing the brew button, we will then not have\n # beans any more (as they have been entered into the brewing chamber) and\n # our output will be heating the heating element.\n have_beans.upon(brew_button, enter=dont_have_beans,\n outputs=[_heat_the_heating_element])\n\nTo *users* of this coffee machine class though, it still looks like a POPO\n(Plain Old Python Object):\n\n.. code-block:: python\n\n >>> coffee_machine = CoffeeMachine()\n >>> coffee_machine.put_in_beans()\n >>> coffee_machine.brew_button()\n\nAll of the *inputs* are provided by calling them like methods, all of the\n*outputs* are automatically invoked when they are produced according to the\noutputs specified to ``upon`` and all of the states are simply opaque tokens -\nalthough the fact that they're defined as methods like inputs and outputs\nallows you to put docstrings on them easily to document them.\n\nHow do I get the current state of a state machine?\n--------------------------------------------------\n\nDon't do that.\n\nOne major reason for having a state machine is that you want the callers of the\nstate machine to just provide the appropriate input to the machine at the\nappropriate time, and *not have to check themselves* what state the machine is\nin. So if you are tempted to write some code like this:\n\n.. code-block:: python\n\n if connection_state_machine.state == \"CONNECTED\":\n connection_state_machine.send_message()\n else:\n print(\"not connected\")\n\nInstead, just make your calling code do this:\n\n.. code-block:: python\n\n connection_state_machine.send_message()\n\nand then change your state machine to look like this:\n\n.. code-block:: python\n\n @_machine.state()\n def connected(self):\n \"connected\"\n @_machine.state()\n def not_connected(self):\n \"not connected\"\n @_machine.input()\n def send_message(self):\n \"send a message\"\n @_machine.output()\n def _actually_send_message(self):\n self._transport.send(b\"message\")\n @_machine.output()\n def _report_sending_failure(self):\n print(\"not connected\")\n connected.upon(send_message, enter=connected, [_actually_send_message])\n not_connected.upon(send_message, enter=not_connected, [_report_sending_failure])\n\nso that the responsibility for knowing which state the state machine is in\nremains within the state machine itself.\n\nInput for Inputs and Output for Outputs\n---------------------------------------\n\nQuite often you want to be able to pass parameters to your methods, as well as\ninspecting their results. For example, when you brew the coffee, you might\nexpect a cup of coffee to result, and you would like to see what kind of coffee\nit is. And if you were to put delicious hand-roasted small-batch artisanal\nbeans into the machine, you would expect a *better* cup of coffee than if you\nwere to use mass-produced beans. You would do this in plain old Python by\nadding a parameter, so that's how you do it in Automat as well.\n\n.. code-block:: python\n\n @_machine.input()\n def put_in_beans(self, beans):\n \"The user put in some beans.\"\n\nHowever, one important difference here is that *we can't add any\nimplementation code to the input method*. Inputs are purely a declaration of\nthe interface; the behavior must all come from outputs. Therefore, the change\nin the state of the coffee machine must be represented as an output. We can\nadd an output method like this:\n\n.. code-block:: python\n\n @_machine.output()\n def _save_beans(self, beans):\n \"The beans are now in the machine; save them.\"\n self._beans = beans\n\nand then connect it to the ``put_in_beans`` by changing the transition from\n``dont_have_beans`` to ``have_beans`` like so:\n\n.. code-block:: python\n\n dont_have_beans.upon(put_in_beans, enter=have_beans,\n outputs=[_save_beans])\n\nNow, when you call:\n\n.. code-block:: python\n\n coffee_machine.put_in_beans(\"real good beans\")\n\nthe machine will remember the beans for later.\n\nSo how do we get the beans back out again? One of our outputs needs to have a\nreturn value. It would make sense if our ``brew_button`` method returned the cup\nof coffee that it made, so we should add an output. So, in addition to heating\nthe heating element, let's add a return value that describes the coffee. First\na new output:\n\n.. code-block:: python\n\n @_machine.output()\n def _describe_coffee(self):\n return \"A cup of coffee made with {}.\".format(self._beans)\n\nNote that we don't need to check first whether ``self._beans`` exists or not,\nbecause we can only reach this output method if the state machine says we've\ngone through a set of states that sets this attribute.\n\nNow, we need to hook up ``_describe_coffee`` to the process of brewing, so change\nthe brewing transition to:\n\n.. code-block:: python\n\n have_beans.upon(brew_button, enter=dont_have_beans,\n outputs=[_heat_the_heating_element,\n _describe_coffee])\n\nNow, we can call it:\n\n.. code-block:: python\n\n >>> coffee_machine.brew_button()\n [None, 'A cup of coffee made with real good beans.']\n\nExcept... wait a second, what's that ``None`` doing there?\n\nSince every input can produce multiple outputs, in automat, the default return\nvalue from every input invocation is a ``list``. In this case, we have both\n``_heat_the_heating_element`` and ``_describe_coffee`` outputs, so we're seeing\nboth of their return values. However, this can be customized, with the\n``collector`` argument to ``upon``\\ ; the ``collector`` is a callable which takes an\niterable of all the outputs' return values and \"collects\" a single return value\nto return to the caller of the state machine.\n\nIn this case, we only care about the last output, so we can adjust the call to\n``upon`` like this:\n\n.. code-block:: python\n\n have_beans.upon(brew_button, enter=dont_have_beans,\n outputs=[_heat_the_heating_element,\n _describe_coffee],\n collector=lambda iterable: list(iterable)[-1]\n )\n\nAnd now, we'll get just the return value we want:\n\n.. code-block:: python\n\n >>> coffee_machine.brew_button()\n 'A cup of coffee made with real good beans.'\n\nIf I can't get the state of the state machine, how can I save it to (a database, an API response, a file on disk...)\n--------------------------------------------------------------------------------------------------------------------\n\nThere are APIs for serializing the state machine.\n\nFirst, you have to decide on a persistent representation of each state, via the\n``serialized=`` argument to the ``MethodicalMachine.state()`` decorator.\n\nLet's take this very simple \"light switch\" state machine, which can be on or\noff, and flipped to reverse its state:\n\n.. code-block:: python\n\n class LightSwitch(object):\n _machine = MethodicalMachine()\n @_machine.state(serialized=\"on\")\n def on_state(self):\n \"the switch is on\"\n @_machine.state(serialized=\"off\", initial=True)\n def off_state(self):\n \"the switch is off\"\n @_machine.input()\n def flip(self):\n \"flip the switch\"\n on_state.upon(flip, enter=off_state, outputs=[])\n off_state.upon(flip, enter=on_state, outputs=[])\n\nIn this case, we've chosen a serialized representation for each state via the\n``serialized`` argument. The on state is represented by the string ``\"on\"``\\ , and\nthe off state is represented by the string ``\"off\"``.\n\nNow, let's just add an input that lets us tell if the switch is on or not.\n\n.. code-block:: python\n\n @_machine.input()\n def query_power(self):\n \"return True if powered, False otherwise\"\n @_machine.output()\n def _is_powered(self):\n return True\n @_machine.output()\n def _not_powered(self):\n return False\n on_state.upon(query_power, enter=on_state, outputs=[_is_powered],\n collector=next)\n off_state.upon(query_power, enter=off_state, outputs=[_not_powered],\n collector=next)\n\nTo save the state, we have the ``MethodicalMachine.serializer()`` method. A\nmethod decorated with ``@serializer()`` gets an extra argument injected at the\nbeginning of its argument list: the serialized identifier for the state. In\nthis case, either ``\"on\"`` or ``\"off\"``. Since state machine output methods can\nalso affect other state on the object, a serializer method is expected to\nreturn *all* relevant state for serialization.\n\nFor our simple light switch, such a method might look like this:\n\n.. code-block:: python\n\n @_machine.serializer()\n def save(self, state):\n return {\"is-it-on\": state}\n\nSerializers can be public methods, and they can return whatever you like. If\nnecessary, you can have different serializers - just multiple methods decorated\nwith ``@_machine.serializer()`` - for different formats; return one data-structure\nfor JSON, one for XML, one for a database row, and so on.\n\nWhen it comes time to unserialize, though, you generally want a private method,\nbecause an unserializer has to take a not-fully-initialized instance and\npopulate it with state. It is expected to *return* the serialized machine\nstate token that was passed to the serializer, but it can take whatever\narguments you like. Of course, in order to return that, it probably has to\ntake it somewhere in its arguments, so it will generally take whatever a paired\nserializer has returned as an argument.\n\nSo our unserializer would look like this:\n\n.. code-block:: python\n\n @_machine.unserializer()\n def _restore(self, blob):\n return blob[\"is-it-on\"]\n\nGenerally you will want a classmethod deserialization constructor which you\nwrite yourself to call this, so that you know how to create an instance of your\nown object, like so:\n\n.. code-block:: python\n\n @classmethod\n def from_blob(cls, blob):\n self = cls()\n self._restore(blob)\n return self\n\nSaving and loading our ``LightSwitch`` along with its state-machine state can now\nbe accomplished as follows:\n\n.. code-block:: python\n\n >>> switch1 = LightSwitch()\n >>> switch1.query_power()\n False\n >>> switch1.flip()\n []\n >>> switch1.query_power()\n True\n >>> blob = switch1.save()\n >>> switch2 = LightSwitch.from_blob(blob)\n >>> switch2.query_power()\n True\n\nMore comprehensive (tested, working) examples are present in ``docs/examples``.\n\nGo forth and machine all the state!\n\n\n", "description_content_type": "", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/glyph/Automat", "keywords": "fsm finite state machine automata", "license": "MIT", "maintainer": "", "maintainer_email": "", "name": "Automat", "package_url": "https://pypi.org/project/Automat/", "platform": "", "project_url": "https://pypi.org/project/Automat/", "project_urls": { "Homepage": "https://github.com/glyph/Automat" }, "release_url": "https://pypi.org/project/Automat/0.8.0/", "requires_dist": [ "attrs (>=16.1.0)", "six", "graphviz (>0.5.1) ; extra == 'visualize'", "Twisted (>=16.1.1) ; extra == 'visualize'" ], "requires_python": "", "summary": "Self-service finite-state machines for the programmer on the go.", "version": "0.8.0" }, "last_serial": 6001880, "releases": { "0.0.1": [], "0.1.1": [ { "comment_text": "", "digests": { "md5": "13d1820068890e5fa6106af2284e3d7e", "sha256": "b3e8e67007bab9a840f1c750207d6f7c7d8fa102efc7e7bde4e48d31d13fc103" }, "downloads": -1, "filename": "Automat-0.1.1-py2-none-any.whl", "has_sig": false, "md5_digest": "13d1820068890e5fa6106af2284e3d7e", "packagetype": "bdist_wheel", "python_version": "py2", "requires_python": null, "size": 8197, "upload_time": "2014-09-20T01:27:47", "url": "https://files.pythonhosted.org/packages/7e/83/693b7b5554b73671ed04a8ad2f4e9a69e06d29a6278d77eec292e546b945/Automat-0.1.1-py2-none-any.whl" }, { "comment_text": "", "digests": { "md5": "c3c0cab49baa6a6150db23a37879d261", "sha256": "da060c95e768f38b6c11c39f00c7276c0fe87bb81b3607e3c20ed933606fbaee" }, "downloads": -1, "filename": "Automat-0.1.1.tar.gz", "has_sig": false, "md5_digest": "c3c0cab49baa6a6150db23a37879d261", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 5499, "upload_time": "2014-09-20T01:27:54", "url": "https://files.pythonhosted.org/packages/db/43/7b7f5a8aa070ea56d0c1214ad96594e04a4e01e820fd47e9e66a7177eb0d/Automat-0.1.1.tar.gz" } ], "0.2.0": [ { "comment_text": "", "digests": { "md5": "f03c3dff605f7874f734da9069adae90", "sha256": "0bdb3374faaacf6f490a7aca8706a4cadd3c6b439cb9905732a9f410022cdf6c" }, "downloads": -1, "filename": "Automat-0.2.0-py2-none-any.whl", "has_sig": false, "md5_digest": "f03c3dff605f7874f734da9069adae90", "packagetype": "bdist_wheel", "python_version": "py2", "requires_python": null, "size": 10544, "upload_time": "2015-07-17T09:32:23", "url": "https://files.pythonhosted.org/packages/06/be/7f5c3fb8c8c918a62c765cc46eccc7cd568081de9e30fc18a73cabbe5e60/Automat-0.2.0-py2-none-any.whl" } ], "0.3.0": [ { "comment_text": "", "digests": { "md5": "616dd3db577d29b46fad4edd85bae4e1", "sha256": "feb7aa604d91e5bf735edd081d8d6f26dfb339ea9363f2165e16a31c0fb1f62b" }, "downloads": -1, "filename": "Automat-0.3.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "616dd3db577d29b46fad4edd85bae4e1", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 12232, "upload_time": "2015-12-22T20:19:39", "url": "https://files.pythonhosted.org/packages/7d/9b/e54fd937fcc8b10c61fbb4ccb29fc05424f3c1fa752bebc52c8930dcb662/Automat-0.3.0-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "6276d5b9b4a801598f04f30ddc7ee664", "sha256": "24077c6c8698339ee98dda3983c1544844977fcfa66b2e34eb8ac1ece870af02" }, "downloads": -1, "filename": "Automat-0.3.0.tar.gz", "has_sig": false, "md5_digest": "6276d5b9b4a801598f04f30ddc7ee664", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 8682, "upload_time": "2016-12-29T13:00:58", "url": "https://files.pythonhosted.org/packages/1b/09/48f1a4a170a26d951a62bbb5e6e731b40995a64d77d408cd9fe240800c6f/Automat-0.3.0.tar.gz" } ], "0.4.0": [ { "comment_text": "", "digests": { "md5": "53f64cd5c149429ec80b55be5e92d43d", "sha256": "edaf9108517829602af93b3706d4266e941c1c3bcddefe45458831c8ddbc7b08" }, "downloads": -1, "filename": "Automat-0.4.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "53f64cd5c149429ec80b55be5e92d43d", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 22390, "upload_time": "2017-01-24T02:14:04", "url": "https://files.pythonhosted.org/packages/0d/a0/a5043e27c758a6a13ede290678af89ce83439c72947fa6380aab1ddbd9f6/Automat-0.4.0-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "c04e1009407983d281fb84b05f43d348", "sha256": "24b97bc7e77804875da59383cccc7013bbec00296edf9baa7e2b5a969ed09df7" }, "downloads": -1, "filename": "Automat-0.4.0.tar.gz", "has_sig": false, "md5_digest": "c04e1009407983d281fb84b05f43d348", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 25791, "upload_time": "2017-01-24T02:14:06", "url": "https://files.pythonhosted.org/packages/e1/4c/ef19106d70137de31531f4d98a4f835ab2a0ffa6cb3c1c0fd1b9f5c0710f/Automat-0.4.0.tar.gz" } ], "0.5.0": [ { "comment_text": "", "digests": { "md5": "7b598a5369da8157c0fc6644458951d0", "sha256": "294e91c3c52e3c2d3123230ed605e5a4b09cfc487716c9d218d118e2ef57c5c1" }, "downloads": -1, "filename": "Automat-0.5.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "7b598a5369da8157c0fc6644458951d0", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 33227, "upload_time": "2017-02-08T23:15:53", "url": "https://files.pythonhosted.org/packages/90/a3/6ab06631bb242891859b14902db756452f7d137f5b7b9ea8f8785e803bc4/Automat-0.5.0-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "3a73d95339da60b58a624851f2319fe3", "sha256": "4889ec6763377432ec4db265ad552bbe956768ea3fff39014855308ba79dd7c2" }, "downloads": -1, "filename": "Automat-0.5.0.tar.gz", "has_sig": false, "md5_digest": "3a73d95339da60b58a624851f2319fe3", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 32599, "upload_time": "2017-02-08T23:15:54", "url": "https://files.pythonhosted.org/packages/73/5a/e5dc9a87e5795ba164e012f2b1cd659e31b722355b79e934e0af892d0493/Automat-0.5.0.tar.gz" } ], "0.6.0": [ { "comment_text": "", "digests": { "md5": "c0fad6ad1e018157a2b3c8bf637b8a4d", "sha256": "2140297df155f7990f6f4c73b2ab0583bd8150db9ed2a1b48122abe66e9908c1" }, "downloads": -1, "filename": "Automat-0.6.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "c0fad6ad1e018157a2b3c8bf637b8a4d", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 35207, "upload_time": "2017-05-16T23:17:07", "url": "https://files.pythonhosted.org/packages/17/6a/1baf488c2015ecafda48c03ca984cf0c48c254622668eb1732dbe2eae118/Automat-0.6.0-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "ad7bba58d262d8956d732330cb5ef53d", "sha256": "3c1fd04ecf08ac87b4dd3feae409542e9bf7827257097b2b6ed5692f69d6f6a8" }, "downloads": -1, "filename": "Automat-0.6.0.tar.gz", "has_sig": false, "md5_digest": "ad7bba58d262d8956d732330cb5ef53d", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 31767, "upload_time": "2017-05-16T23:17:09", "url": "https://files.pythonhosted.org/packages/de/05/b8e453085cf8a7f27bb1226596f4ccf5cc9e758377d60284f990bbdc592c/Automat-0.6.0.tar.gz" } ], "0.7.0": [ { "comment_text": "", "digests": { "md5": "826c7c73bf5a247a8ccff783e4a89144", "sha256": "fdccab66b68498af9ecfa1fa43693abe546014dd25cf28543cbe9d1334916a58" }, "downloads": -1, "filename": "Automat-0.7.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "826c7c73bf5a247a8ccff783e4a89144", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 37638, "upload_time": "2018-06-14T07:18:55", "url": "https://files.pythonhosted.org/packages/a3/86/14c16bb98a5a3542ed8fed5d74fb064a902de3bdd98d6584b34553353c45/Automat-0.7.0-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "b72d5b7a83c2b1bd6e9ec3614a69f9ea", "sha256": "cbd78b83fa2d81fe2a4d23d258e1661dd7493c9a50ee2f1a5b2cac61c1793b0e" }, "downloads": -1, "filename": "Automat-0.7.0.tar.gz", "has_sig": false, "md5_digest": "b72d5b7a83c2b1bd6e9ec3614a69f9ea", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 61461, "upload_time": "2018-06-14T07:18:57", "url": "https://files.pythonhosted.org/packages/4a/4f/64db3ffda8828cb0541fe949354615f39d02f596b4c33fb74863756fc565/Automat-0.7.0.tar.gz" } ], "0.8.0": [ { "comment_text": "", "digests": { "md5": "3e78357fdeaa213cb3d54803d811e0da", "sha256": "81c93c55d2742c55e74e6497a48e048a859fa01d7aa0b91a032be432229837e2" }, "downloads": -1, "filename": "Automat-0.8.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "3e78357fdeaa213cb3d54803d811e0da", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 31791, "upload_time": "2019-10-20T04:27:48", "url": "https://files.pythonhosted.org/packages/e5/11/756922e977bb296a79ccf38e8d45cafee446733157d59bcd751d3aee57f5/Automat-0.8.0-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "47e980a48201a1dabe37fa11f0187013", "sha256": "269a09dfb063a3b078983f4976d83f0a0d3e6e7aaf8e27d8df1095e09dc4a484" }, "downloads": -1, "filename": "Automat-0.8.0.tar.gz", "has_sig": false, "md5_digest": "47e980a48201a1dabe37fa11f0187013", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 59687, "upload_time": "2019-10-20T04:27:50", "url": "https://files.pythonhosted.org/packages/4c/9a/3052851fa3a888d1ff32f053fba424ed929b47383fb5327855fdf70018cd/Automat-0.8.0.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "3e78357fdeaa213cb3d54803d811e0da", "sha256": "81c93c55d2742c55e74e6497a48e048a859fa01d7aa0b91a032be432229837e2" }, "downloads": -1, "filename": "Automat-0.8.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "3e78357fdeaa213cb3d54803d811e0da", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 31791, "upload_time": "2019-10-20T04:27:48", "url": "https://files.pythonhosted.org/packages/e5/11/756922e977bb296a79ccf38e8d45cafee446733157d59bcd751d3aee57f5/Automat-0.8.0-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "47e980a48201a1dabe37fa11f0187013", "sha256": "269a09dfb063a3b078983f4976d83f0a0d3e6e7aaf8e27d8df1095e09dc4a484" }, "downloads": -1, "filename": "Automat-0.8.0.tar.gz", "has_sig": false, "md5_digest": "47e980a48201a1dabe37fa11f0187013", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 59687, "upload_time": "2019-10-20T04:27:50", "url": "https://files.pythonhosted.org/packages/4c/9a/3052851fa3a888d1ff32f053fba424ed929b47383fb5327855fdf70018cd/Automat-0.8.0.tar.gz" } ] }