From a52eb72a56479f29596f3002820775b4dbcf33b1 Mon Sep 17 00:00:00 2001 From: "akshita.dixit" Date: Mon, 2 Dec 2024 15:02:20 +0530 Subject: [PATCH] Bump version to 0.0.2 and add workflow and executor classes with example usage --- examples/config_example.yml | 16 ++++++---- examples/workflow_example.py | 15 ++++++++++ flow-fsm/core/workflow.py | 18 ----------- flow-fsm/runtime/executor.py | 0 flow-fsm/visualisation/__init__.py | 0 {flow-fsm => flowfsm}/__init__.py | 0 {flow-fsm => flowfsm}/config/__init__.py | 0 {flow-fsm => flowfsm}/config/loader.py | 18 ++++++----- {flow-fsm => flowfsm}/config/parser.py | 0 {flow-fsm => flowfsm}/core/__init__.py | 0 {flow-fsm => flowfsm}/core/base.py | 6 ++-- {flow-fsm => flowfsm}/core/errors.py | 0 {flow-fsm => flowfsm}/core/event.py | 25 ++++++++++++---- {flow-fsm => flowfsm}/core/state.py | 22 +++++++++++--- {flow-fsm => flowfsm}/core/transition.py | 17 +++++++---- flowfsm/core/workflow.py | 16 ++++++++++ {flow-fsm => flowfsm}/runtime/__init__.py | 0 flowfsm/runtime/executor.py | 16 ++++++++++ {flow-fsm => flowfsm}/runtime/hooks.py | 0 {flow-fsm => flowfsm}/runtime/listeners.py | 0 {flow-fsm => flowfsm}/runtime/logger.py | 0 flowfsm/utils/__init__.py | 5 ++++ .../visualisation}/__init__.py | 0 pyproject.toml | 2 +- test.py | 14 +++++++++ uv.lock | 30 ------------------- 26 files changed, 139 insertions(+), 81 deletions(-) create mode 100644 examples/workflow_example.py delete mode 100644 flow-fsm/core/workflow.py delete mode 100644 flow-fsm/runtime/executor.py delete mode 100644 flow-fsm/visualisation/__init__.py rename {flow-fsm => flowfsm}/__init__.py (100%) rename {flow-fsm => flowfsm}/config/__init__.py (100%) rename {flow-fsm => flowfsm}/config/loader.py (72%) rename {flow-fsm => flowfsm}/config/parser.py (100%) rename {flow-fsm => flowfsm}/core/__init__.py (100%) rename {flow-fsm => flowfsm}/core/base.py (92%) rename {flow-fsm => flowfsm}/core/errors.py (100%) rename {flow-fsm => flowfsm}/core/event.py (60%) rename {flow-fsm => flowfsm}/core/state.py (75%) rename {flow-fsm => flowfsm}/core/transition.py (78%) create mode 100644 flowfsm/core/workflow.py rename {flow-fsm => flowfsm}/runtime/__init__.py (100%) create mode 100644 flowfsm/runtime/executor.py rename {flow-fsm => flowfsm}/runtime/hooks.py (100%) rename {flow-fsm => flowfsm}/runtime/listeners.py (100%) rename {flow-fsm => flowfsm}/runtime/logger.py (100%) create mode 100644 flowfsm/utils/__init__.py rename {flow-fsm/utils => flowfsm/visualisation}/__init__.py (100%) create mode 100644 test.py delete mode 100644 uv.lock diff --git a/examples/config_example.yml b/examples/config_example.yml index e46572c..e067fe6 100644 --- a/examples/config_example.yml +++ b/examples/config_example.yml @@ -1,26 +1,30 @@ name: "SimpleFSM" states: Idle: - on_enter: "Entering Idle" - on_exit: "Exiting Idle" + on_enter: "Entering Idle hehe" + on_exit: "Exiting Idle hehe" Active: - on_enter: "Entering Active" - on_exit: "Exiting Active" + on_enter: "Entering Active ehehe" + on_exit: "Exiting Active ehehe" transitions: - source: "Idle" target: "Active" condition: "lambda: True" - action: "lambda: print('Activating')" + action: "print('====Activating====')" - source: "Active" target: "Idle" condition: "lambda: True" - action: "lambda: print('Deactivating')" + action: "print('====Deactivating====')" events: Activate: transitions: - source: "Idle" target: "Active" + terminal: + - "Active" Deactivate: transitions: - source: "Active" target: "Idle" + terminal: + - "Idle" \ No newline at end of file diff --git a/examples/workflow_example.py b/examples/workflow_example.py new file mode 100644 index 0000000..338f6f8 --- /dev/null +++ b/examples/workflow_example.py @@ -0,0 +1,15 @@ +from ..flowfsm.config.parser import parse_fsm_config +from ..flowfsm.config.loader import load_fsm_from_config +from ..flowfsm.runtime.executor import Executor + +# Load FSM configuration +config = parse_fsm_config("./config_example.yml") + +# Create FSM +workflow = load_fsm_from_config(config) + +print(f"Workflow '{workflow.name}' initialized with states: {workflow.states}") + +# Execute FSM +executor = Executor(workflow) +executor.run() diff --git a/flow-fsm/core/workflow.py b/flow-fsm/core/workflow.py deleted file mode 100644 index b2b2c50..0000000 --- a/flow-fsm/core/workflow.py +++ /dev/null @@ -1,18 +0,0 @@ -from .base import FSMBase -from .state import StateRegistry -from .transition import TransitionRegistry -from .event import EventRegistry - - -class Workflow(FSMBase): - """Workflow class extending FSMBase.""" - def __init__(self, name, config): - super().__init__(name) - self.states = [StateRegistry.register(state) for state in config["states"]] - self.transitions = [ - TransitionRegistry.register( - src, tgt, condition=cfg.get("condition"), action=cfg.get("action") - ) - for src, tgt, cfg in config["transitions"] - ] - self.events = {event: EventRegistry.register(event) for event in config["events"]} diff --git a/flow-fsm/runtime/executor.py b/flow-fsm/runtime/executor.py deleted file mode 100644 index e69de29..0000000 diff --git a/flow-fsm/visualisation/__init__.py b/flow-fsm/visualisation/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/flow-fsm/__init__.py b/flowfsm/__init__.py similarity index 100% rename from flow-fsm/__init__.py rename to flowfsm/__init__.py diff --git a/flow-fsm/config/__init__.py b/flowfsm/config/__init__.py similarity index 100% rename from flow-fsm/config/__init__.py rename to flowfsm/config/__init__.py diff --git a/flow-fsm/config/loader.py b/flowfsm/config/loader.py similarity index 72% rename from flow-fsm/config/loader.py rename to flowfsm/config/loader.py index 2c7d522..336dc8a 100644 --- a/flow-fsm/config/loader.py +++ b/flowfsm/config/loader.py @@ -12,7 +12,7 @@ def create_states(states_config): for state_name, state_config in states_config.items(): on_enter = state_config.get("on_enter") on_exit = state_config.get("on_exit") - state = State(state_name, on_enter=on_enter, on_exit=on_exit) + state = StateRegistry.register(state_name, on_enter=on_enter, on_exit=on_exit) states[state_name] = state return states @@ -23,7 +23,9 @@ def create_transitions(transitions_config): source, target = transition["source"], transition["target"] condition = transition.get("condition") action = transition.get("action") - transition = Transition(source, target, condition=condition, action=action) + source = StateRegistry.get(source) if source in StateRegistry._states else StateRegistry.register(source) + target = StateRegistry.get(target) if target in StateRegistry._states else StateRegistry.register(target) + transition = TransitionRegistry.register(source, target, condition=condition, action=action) transitions.append(transition) return transitions @@ -31,7 +33,7 @@ def create_events(events_config, transitions): """Dynamically create events and bind them to transitions.""" events = {} for event_name, event_config in events_config.items(): - event = Event(event_name) + event = EventRegistry.register(event_name) for transition in event_config["transitions"]: # Find the transition object based on source and target for t in transitions: @@ -43,14 +45,14 @@ def create_events(events_config, transitions): def load_fsm_from_config(config): """Create FSM components from the configuration file.""" + # Clear existing registries to avoid conflicts + StateRegistry.clear() + TransitionRegistry.clear() + EventRegistry.clear() states = create_states(config['states']) transitions = create_transitions(config['transitions']) events = create_events(config['events'], transitions) - workflow = Workflow(config["name"], { - "states": list(states.keys()), - "transitions": [(t.source, t.target, {"condition": t.is_valid, "action": t.execute}) for t in transitions], - "events": list(events.keys()) - }) + workflow = Workflow(config["name"], list(states.items()), transitions, events) return workflow diff --git a/flow-fsm/config/parser.py b/flowfsm/config/parser.py similarity index 100% rename from flow-fsm/config/parser.py rename to flowfsm/config/parser.py diff --git a/flow-fsm/core/__init__.py b/flowfsm/core/__init__.py similarity index 100% rename from flow-fsm/core/__init__.py rename to flowfsm/core/__init__.py diff --git a/flow-fsm/core/base.py b/flowfsm/core/base.py similarity index 92% rename from flow-fsm/core/base.py rename to flowfsm/core/base.py index ee00095..1405698 100644 --- a/flow-fsm/core/base.py +++ b/flowfsm/core/base.py @@ -13,11 +13,11 @@ def __new__(cls, name, bases, dct): @staticmethod def generate_trigger_event_method(): def trigger_event(self, event): - transition = event.trigger(self.current_state) + transition = event.trigger(self, self.current_state) if transition: - self.current_state.exit() + self.current_state().exit() transition.execute() - self.current_state = StateRegistry.get(transition.target)() + self.current_state = StateRegistry.get(transition.target.name)() self.current_state.enter() else: raise InvalidTransitionError(f"No valid transition for event '{event}' from state '{self.current_state}'.") diff --git a/flow-fsm/core/errors.py b/flowfsm/core/errors.py similarity index 100% rename from flow-fsm/core/errors.py rename to flowfsm/core/errors.py diff --git a/flow-fsm/core/event.py b/flowfsm/core/event.py similarity index 60% rename from flow-fsm/core/event.py rename to flowfsm/core/event.py index a3f3943..9643ed9 100644 --- a/flow-fsm/core/event.py +++ b/flowfsm/core/event.py @@ -3,14 +3,16 @@ class EventRegistry: _events = {} @classmethod - def register(cls, name): + def register(cls, name, terminal_states=None): + """Register events and optionally define terminal states.""" if name in cls._events: raise ValueError(f"Event '{name}' is already registered.") - def create_event_class(name): - """Dynamically creates an event class.""" + def create_event_class(name, terminal_states): + """Dynamically creates an event class with terminal states.""" def __init__(self): self.transitions = [] + self.terminal_states = terminal_states or [] # Default to an empty list if no terminal states are provided def add_transition(self, transition): self.transitions.append(transition) @@ -29,26 +31,37 @@ def trigger(self, current_state): } return type(name, (object,), methods) - event_class = create_event_class(name) + # Create and register the event class with terminal states + event_class = create_event_class(name, terminal_states) cls._events[name] = event_class return event_class @classmethod def get(cls, name): + """Retrieve an event class by name.""" if name not in cls._events: raise ValueError(f"Event '{name}' is not registered.") return cls._events[name] + + @classmethod + def clear(cls): + """Clear all registered events.""" + cls._events = {} class Event: """User API for creating and managing events.""" - def __init__(self, name): + def __init__(self, name, terminal_states=None): self.name = name - self._event_class = EventRegistry.register(name) + self.terminal_states = terminal_states + self._event_class = EventRegistry.register(name, terminal_states) self._event_instance = self._event_class() def __getattr__(self, attr): + """Delegate attribute access to the event instance.""" return getattr(self._event_instance, attr) def __repr__(self): + """Represent the event instance.""" return repr(self._event_instance) + \ No newline at end of file diff --git a/flow-fsm/core/state.py b/flowfsm/core/state.py similarity index 75% rename from flow-fsm/core/state.py rename to flowfsm/core/state.py index 9e9d45d..f3aff7d 100644 --- a/flow-fsm/core/state.py +++ b/flowfsm/core/state.py @@ -7,16 +7,25 @@ class StateRegistry: def register(cls, name, on_enter=None, on_exit=None): def create_state_class(name, on_enter=None, on_exit=None): """Dynamically creates a state class with custom behavior.""" - def default_enter(self): + def enter(self): + if on_enter: + print(on_enter) + return print(f"Entering {name}") - def default_exit(self): + def exit(self): + if on_exit: + print(on_exit) + return print(f"Exiting {name}") methods = { - "enter": on_enter or default_enter, - "exit": on_exit or default_exit, + "enter": enter, + "exit": exit, "__repr__": lambda self: f"", + "name": name, + "on_enter": on_enter, + "on_exit": on_exit, } return type(name, (object,), methods) @@ -32,6 +41,11 @@ def get(cls, name): if name not in cls._states: raise ValueError(f"State '{name}' is not registered.") return cls._states[name] + + @classmethod + def clear(cls): + """Clear all registered states.""" + cls._states = {} # User-facing State API diff --git a/flow-fsm/core/transition.py b/flowfsm/core/transition.py similarity index 78% rename from flow-fsm/core/transition.py rename to flowfsm/core/transition.py index cace4bc..fb340b1 100644 --- a/flow-fsm/core/transition.py +++ b/flowfsm/core/transition.py @@ -6,12 +6,12 @@ class TransitionRegistry: def register(cls, source, target, condition=None, action=None): def create_transition_class(source, target, condition=None, action=None): """Dynamically creates a transition class.""" - def is_valid(self): - return condition() if condition else True + def is_valid(*args): + return eval(condition) if condition else True - def execute(self): + def execute(): if action: - action() + eval(action) methods = { "is_valid": is_valid, @@ -19,6 +19,8 @@ def execute(self): "__repr__": lambda self: f" {target}>", "source": source, "target": target, + "condition": condition, + "action": action, } return type(f"Transition_{source}_to_{target}", (object,), methods) @@ -30,6 +32,11 @@ def execute(self): def get_all(cls): """Retrieve all registered transitions.""" return cls._transitions + + @classmethod + def clear(cls): + """Clear all registered transitions.""" + cls._transitions = [] class Transition: @@ -42,4 +49,4 @@ def __getattr__(self, attr): return getattr(self._transition_instance, attr) def __repr__(self): - return repr(self._transition_instance) + return repr(self._transition_instance) \ No newline at end of file diff --git a/flowfsm/core/workflow.py b/flowfsm/core/workflow.py new file mode 100644 index 0000000..6a6cf36 --- /dev/null +++ b/flowfsm/core/workflow.py @@ -0,0 +1,16 @@ +from .base import FSMBase +from .state import StateRegistry +from .transition import Transition +from .event import EventRegistry + + +class Workflow(FSMBase): + """Workflow class extending FSMBase.""" + def __init__(self, name, states, transitions, events): + super().__init__(name) + + self.states = states + self.current_state = self.states[0][1] + self.current_state().enter() + self.transitions = transitions + self.events = events diff --git a/flow-fsm/runtime/__init__.py b/flowfsm/runtime/__init__.py similarity index 100% rename from flow-fsm/runtime/__init__.py rename to flowfsm/runtime/__init__.py diff --git a/flowfsm/runtime/executor.py b/flowfsm/runtime/executor.py new file mode 100644 index 0000000..7f83423 --- /dev/null +++ b/flowfsm/runtime/executor.py @@ -0,0 +1,16 @@ +class Executor: + """The executor runs the FSM based on events and transitions.""" + def __init__(self, workflow): + self.workflow = workflow + + def run(self): + """Run the FSM, processing events and triggering transitions.""" + print(f"Starting FSM: {self.workflow.name}") + while True: + # Wait for events to trigger transitions + event_name = "Activate" #input("Enter event name: ").strip() + if event_name in self.workflow.events: + event = self.workflow.events[event_name] + self.workflow.trigger_event(event) + else: + print(f"Event '{event_name}' not found.") diff --git a/flow-fsm/runtime/hooks.py b/flowfsm/runtime/hooks.py similarity index 100% rename from flow-fsm/runtime/hooks.py rename to flowfsm/runtime/hooks.py diff --git a/flow-fsm/runtime/listeners.py b/flowfsm/runtime/listeners.py similarity index 100% rename from flow-fsm/runtime/listeners.py rename to flowfsm/runtime/listeners.py diff --git a/flow-fsm/runtime/logger.py b/flowfsm/runtime/logger.py similarity index 100% rename from flow-fsm/runtime/logger.py rename to flowfsm/runtime/logger.py diff --git a/flowfsm/utils/__init__.py b/flowfsm/utils/__init__.py new file mode 100644 index 0000000..6ae707b --- /dev/null +++ b/flowfsm/utils/__init__.py @@ -0,0 +1,5 @@ +def print_params_decorator(func): + def wrapper(*args, **kwargs): + print(f"Parameters passed to {func.__name__}: args={args}, kwargs={kwargs}") + return func(*args, **kwargs) + return wrapper \ No newline at end of file diff --git a/flow-fsm/utils/__init__.py b/flowfsm/visualisation/__init__.py similarity index 100% rename from flow-fsm/utils/__init__.py rename to flowfsm/visualisation/__init__.py diff --git a/pyproject.toml b/pyproject.toml index d849bfc..624b446 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "flow-fsm" -version = "0.0.1" +version = "0.0.2" description = "A versatile Python framework for finite state machines and workflows." readme = "README.md" requires-python = ">=3.13" diff --git a/test.py b/test.py new file mode 100644 index 0000000..78608de --- /dev/null +++ b/test.py @@ -0,0 +1,14 @@ +from flowfsm.config.parser import parse_fsm_config +from flowfsm.config.loader import load_fsm_from_config +from flowfsm.runtime.executor import Executor + + +# Load FSM configuration +config = parse_fsm_config("./examples/config_example.yml") + +# Create FSM +workflow = load_fsm_from_config(config) + +# Execute FSM +executor = Executor(workflow) +executor.run() diff --git a/uv.lock b/uv.lock deleted file mode 100644 index 34e6bca..0000000 --- a/uv.lock +++ /dev/null @@ -1,30 +0,0 @@ -version = 1 -requires-python = ">=3.13" - -[[package]] -name = "flow-fsm" -version = "0.0.1" -source = { virtual = "." } -dependencies = [ - { name = "pyyaml" }, -] - -[package.metadata] -requires-dist = [{ name = "pyyaml", specifier = ">=6.0.2" }] - -[[package]] -name = "pyyaml" -version = "6.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, - { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, - { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, - { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, - { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, - { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, - { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, - { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, - { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, -]