{"id":17180799,"url":"https://github.com/jsok/trackingstatemachine","last_synced_at":"2025-03-25T00:41:16.183Z","repository":{"id":7453931,"uuid":"8797711","full_name":"jsok/TrackingStateMachine","owner":"jsok","description":"Python based State Machine which can track multiple items in each state and manage their transitions.","archived":false,"fork":false,"pushed_at":"2013-03-19T01:38:05.000Z","size":164,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-01-30T03:15:20.236Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jsok.png","metadata":{"files":{"readme":"README.rst","changelog":"CHANGES.txt","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2013-03-15T11:32:01.000Z","updated_at":"2014-04-04T04:16:15.000Z","dependencies_parsed_at":"2022-08-30T06:51:11.426Z","dependency_job_id":null,"html_url":"https://github.com/jsok/TrackingStateMachine","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jsok%2FTrackingStateMachine","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jsok%2FTrackingStateMachine/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jsok%2FTrackingStateMachine/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jsok%2FTrackingStateMachine/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jsok","download_url":"https://codeload.github.com/jsok/TrackingStateMachine/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245377958,"owners_count":20605374,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-15T00:31:47.266Z","updated_at":"2025-03-25T00:41:16.154Z","avatar_url":"https://github.com/jsok.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"===========\nTracking State Machine\n===========\n\nA State Machine which can simultaneously track many items in all states and manage transitions.\n\nRequirements\n-------------\n\n* Python 2.7\n\nQuickstart\n----------\n\nThe basic concepts in TSM are:\n * TrackingStateMachine: The manager object which contains all state objects and initiates transitions.\n * TrackingState: A state in the TSM, whose minimal responsibility is to store any objects which are in this state.\n * TrackingItem: A base class for items which will be stored in a state (or states).\n\nDefining States and Items\n-------------------------\n\nFirst we must describe the items we will be storing in each state::\n\n    class Friend(TrackingItem):\n        def __init__(self, name, reason):\n            super(self.__class__, self).__init__()\n            self.name = name\n            self.reason\n\n\nHere we define a simple item which does nothing but store a name, and a reason why we are friends with them.\n\nNext we define a state::\n\n    class FriendshipState(TrackingState):\n        def __init__(self, name):\n            super(self.__class__, self).__init__(name, Friend)\n            self.items = []\n\n        def _track(self, item):\n            yield TransitionValidationResult(True, None)\n            self.items.append(item)\n\nThis is a a trivial state which stores a list of ``Friend`` s which currently exist in it.\n\nThe ``_track()`` method tells the TSM what to do with items transitioning into this state.\nAn important note to make is the ``yield`` statement in ``_track()``::\n\n        yield TransitionValidationResult(True, None)\n\nThis exposes some of the design of TSM, specifically that the function of tracking items and performing transitions\nare implementing using generators.\n\nTransition Validations\n----------------------\n\nAs just mentioned, transitions and item tracking are implemented using generators.\nTrackingState implementations need to conform to this protocol by:\n\n1. Performing ``yield TransitionValidationResult(True, None)`` to tell TSM that they are ready to commit to the\n   transition.\n\n2. If they decide that a transition will break one of their invariants, they can perform:\n   ``yield TransitionValidationResult(False, \"DO NOT WANT!\")``\n   to tell TSM that the transition should abort.\n\nA more explicit example, say we never want to track items whose name is \"Jonathan\", this can be achieved by::\n\n    def _track(self, item):\n        if item.name is \"Jonathan\":\n            yield TransitionValidationResult(False, \"I don't track Jonathans\")\n\n        # I'm happy to accept all other names at this point however\n        yield TransitionValidationResult(True, None)\n        self.items.append(item)\n\nTrackingItem Validations\n------------------------\n\nChecking the name on each track event is a little bit tedious, therefore TSM provides TrackingItem validations too::\n\n    class Friend(TrackingItem):\n        def __init__(self, name, reason):\n            super(self.__class__, self).__init__()\n            self.name = name\n            self.reason = reason\n\n            self.validations.extend([\n                (lambda item: item.name is not \"Jonathan\"),\n            ])\n\n``TrackingItem.validations`` is a list of lambdas which are applied to the item, if any of them are False, the item is\ndeemed as invalid.\n\nThis validation mechanism is used by TSM automatically, any items which are tracked (explicitly, or implicitly via\na transition) are subject to these validations. Failures will result in transitions being aborted.\n\nTransition vs TrackingItem validations\n--------------------------------------\n\nAt this point you may wonder which form of validation to use when?\n\nItem validations are useful for:\n\n* Sanitising your items (correct types, presence of values, bounds checks etc.)\n\nTransition validations are useful for:\n\n* Checking for state internal invariants\n\nSay we modify our example and create a \"No Jonathans rule\", e.g. one Jonathan is fine, two is not::\n\n    class Friend(TrackingItem):\n        def __init__(self, name, reason):\n            super(self.__class__, self).__init__()\n            self.name = name\n            self.reason = reason\n\n            self.validations.extend([\n                (lambda item: isinstance(item.name, str)),\n            ])\n\n    class FriendshipState(TrackingState):\n        def __init__(self, name):\n            super(self.__class__, self).__init__(name, Friend)\n            self.items = []\n\n        def __know_person(self, name):\n            # Return index of person if we know them, otherwise None\n            for i, person in enumerate(self.items):\n                if person.name == name:\n                    return i\n            return None\n\n        def _track(self, item):\n            if self.__know_person(\"Jonathan\"):\n                yield TransitionValidationResult(False, \"I already know one Jonathan\")\n\n            # I'm happy to accept all other names at this point however\n            yield TransitionValidationResult(True, None)\n            self.items.append(item)\n\nHere we see the guidelines in practise, an item ensures the name is actually a string, but in and of itself,\nit has no capacity to check if there exists another item also called Jonathan.\n\nThe invariant (only one Jonathan) is enforced in the transition validation.\n\nThe State Machine\n-----------------\n\nNow that we've defined our state and item, we can describe our state machine.\n\nLet's say we are quite fickle and fall in and out of friendships often::\n\n    tsm = TrackingStateMachine()\n    tsm.add_state(FriendshipState(\"Friend\"))\n    tsm.add_state(FriendshipState(\"Enemy\"))\n\nTo describe how people move between being our Friend and Enemy, we add transitions::\n\n    tsm.add_transition(\"falling_out\", \"Friend\", \"Enemy\")\n    tsm.add_transition(\"resolve_differences\", \"Enemy\", \"Friend\")\n\nHowever we haven't yet defined in our ``FriendshipState`` how to have a falling out or how to resolve differences.\n\nIn general, we say::\n\n    tsm.add_transition(TRANSITION_NAME, FROM_STATE, TO_STATE)\n\nDefining Transitions\n--------------------\n\nTo define our transitions, we must create methods in the state with the same name as that registered with the TSM::\n\n    class FriendshipState(TrackingState):\n        def __know_person(self, name):\n            # Return index of person if we know them, otherwise None\n            for i, person in enumerate(self.items):\n                if person.name == name:\n                    return i\n            return None\n\n        def __remove_name(self, name):\n            known = self.__know_person(name)\n            if not known:\n                yield TransitionValidationResult(False, \"Person {0} is not known to us\".format(name))\n\n            # We've made sure person exists and is in this state\n            yield TransitionValidationResult(True, None)\n            self.items.pop(known)\n\n        def falling_out(self, item):\n            return self.__remove_name(item.name)\n\n        def resolve_differences(self, item):\n            return self.__remove_name(item.name)\n\nAs with all transitions, they must yield a successful transition validation.\n\nNotice, these two transitions are fundamentally identical -- removing the person from the state's internal list of\nitems. The transition names are simply semantic.\n\nPerforming Transitions\n----------------------\n\nWith the above TSM configuration we can now make friends and enemies!::\n\n    # Declare some people as friends\n    friends = [Friend(\"Jonathan\", \"I love myself\"), Friend(\"Chris\", \"Cool dude\"), Friend(\"James\", \"Nice guy\")]\n\n    # We 'track' each friend in the relevant state\n    for friend in friends:\n        tsm.state(\"Friends\").track(friend)\n\n    # Jonathan annoyed us, he's now an enemy\n    tsm.transition(\"falling_out\", Friend(\"Jonathan\", None), Friend(\"Jonathan\", \"I hate myself\"))\n\nSo the way we perform transitions is of the form::\n\n    transition(TRANSITION_NAME, FROM_STATE_ITEM, TO_STATE_ITEM)\n\nWhen we un-friended Jonathan above, we had to re-create a ``Friend`` object to specify him to each state,\nthe first time we didn't bother giving a reason because we knew that ``FriendshipState`` isn't interested in the\nreason for removing a person.\n\nDictionary Based Items\n----------------------\n\nPerforming some transitions immediately exposes some annoyances:\n\n* ``Friend`` items are exposed outside of the TSM.\n* We must create ``Friend`` items and know which parameters are useful in which context. e.g. When can I set the\n  Friendship reason to ``None``?\n\nTo address these two issues, TSM allows dictionary items to be used when performing transitions::\n\n    # Previously, to un-friend Jonathan\n    tsm.transition(\"falling_out\", Friend(\"Jonathan\", None), Friend(\"Jonathan\", \"I hate myself\"))\n\n    # Now with dictionary items\n    tsm.transition(\"falling_out\", {\"name\": \"Jonathan\"}, {\"name\": \"Jonathan\", \"reason\": \"I hate myself\"})\n\nHowever to enable this, we need to change how we init our ``TrackingItem``, in this case ``Friend``::\n\n    class Friend(TrackingItem):\n        def __init__(self, properties):\n            super(self.__class__, self).__init__()\n            self.name = properties.get(\"name\")\n            self.reason = properties.get(\"reason\")\n\n            self.validations.extend([\n                (lambda item: item.name is not None),\n            ])\n\nNotice we don't validate the ``reason``, this is because ``reason`` s presence is optional. We actually only care if a\nreason is supplied at one point -- when tracking a new ``Friend``, i.e.::\n\n    class FriendshipState(TrackingState):\n        def _track(self, item):\n            if not item.reason:\n                yield TransitionValidationResult(False, \"You must supply a reason\")\n\n            if \"Jonathan\" in self.items:\n                yield TransitionValidationResult(False, \"I already have one Jonathan\")\n\n            # I'm happy to accept all other names at this point however\n            yield TransitionValidationResult(True, None)\n            self.items.append(item)\n\nTransition Parameters\n---------------------\n\nDictionary based items didn't solve one problem:\n\n* We still need to mention **Jonathan** twice in our transition.\n\nIf we accidentally mis-typed the name the second time, we could risk never getting our Friend back!\n\nTSM provides a mechanism for the *-from-* state to communicate paramaters to the *-to-* state via\n``TransitionParamater`` objects, which can be inserted into dictionary items as follows::\n\n    tsm.transition(\"falling_out\",\n                   {\"name\": \"Jonathan\"},\n                   {\"name\": TransitionParameter(\"name\"), \"reason\": \"I hate myself\"})\n\nWhat we want to achieve here is to have the *-from-* state fill in the name for us. This requires one small tweak in\nhow our state transitions::\n\n    class FriendshipState(TrackingState):\n        def __remove_name(self, name):\n            known = self.__know_person(name)\n            if not known:\n                yield TransitionValidationResult(False, \"Person {0} is not known to us\".format(name))\n\n            # We've made sure person exists and is in this state\n            success = TransitionValidationResult(True, None)\n            success.add_parameter(\"name\", name)\n            yield success\n\n            self.items.pop(known)\n\n        def falling_out(self, item):\n            return self.__remove_name(item.name)\n\n        def resolve_differences(self, item):\n            return self.__remove_name(item.name)\n\nA state communicates which, after transition validation succeeds, a list of parameters which may be useful to the\nnext state.\n\nIt is also possible to provide a default value in the case where the *-from-* state fails to provide us with a\nparamater::\n\n    {\"name\": TransitionParameter(\"name\"), \"foo\": TransitionParamater(\"foo\", value=\"Default Foo\")}\n\n\nState Actions\n-------------\n\nSometimes we want to perform some action on the items tracked in a state. Since these can be quite varied,\nstate actions are fairly free to do what they like.\n\nLike transitions, we must register them with the TSM and create methods with corresponding names on the state::\n\n    tsm = TrackingStateMachine()\n\n    tsm.add_state(FriendshipState(\"Friend\"))\n    tsm.add_state(FriendshipState(\"Enemy\"))\n\n    tsm.add_action(\"upper_case\", \"Friend\")\n\nAnd the action definition::\n\n    class FriendshipState(TrackingState):\n        def upper_case(self, args):\n            initial = args.get(\"initial\", None)\n\n            if not initial:\n                raise TransitionActionError(\"Must provide the intial of the person to upper case\")\n\n            for person in self.items:\n                if person.name.upper().startswith(initial.upper()):\n                    person.name = person.name.upper()\n\nTo issue the action we simply::\n\n    tsm.action(\"upper_case\", {\"initial\": 'J'})\n\nNote, that this action, although technically defined on the ``Enemy`` state, isn't registered with the TSM because\nwhen we registered the action, we specified that the action was on the ``Friend`` state.\n\nQuerying States\n---------------\n\n``TrackingState`` provides a ``get`` method, we can make use of it and are free to scope its functionality as we\nplease::\n\n    class Friend(TrackingState):\n        def _get(self, initial=None):\n            \"\"\"Return all known persons with the given initial, otherwise return all persons\"\"\"\n\n            matches = []\n            for person in self.items:\n                if not initial:\n                    matches.append(person)\n                elif person.name.upper().startswith(initial.upper()):\n                    matches.append(person)\n\n            # A bit contrived, but shows all our possible return forms\n            if not matches:\n                return None\n            elif len(matches) == 1:\n                return matches[0]\n            else:\n                return mactches\n\nWhen implementing ``_get`` (note the underscore), you are free to return either:\n\n* ``None`` -- if your criteria matched nothing\n* An instance of ``TrackingItem``\n* A list of ``TrackingItem``\n\nItems retrieved using ``get`` are always exported as dicts::\n\n    \u003e tsm.state(\"Friend\").get(initial='J')\n    {\"name\": \"Jonathan\", \"reason\": \"I love myself\"}\n\n    \u003e tsm.state(\"Friend\").get(initial='Z')\n    None\n\n    \u003e tsm.state(\"Friend\").get()\n    [{\"name\": \"Jonathan\", \"reason\": \"I love myself\"}, {...}, {...}, etc ]","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjsok%2Ftrackingstatemachine","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjsok%2Ftrackingstatemachine","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjsok%2Ftrackingstatemachine/lists"}