Skip to main content

No project description provided

Project description

Genstates

A flexible state machine library for Python with support for state actions and dynamic transitions.

Table of Contents

  1. Installation
  2. Quick Start
  3. Core Concepts
  4. Configuration
  5. Features
  6. Advanced Usage
  7. Contributing
  8. License

Installation

pip install genstates

Quick Start

You can define your state machine either directly with a Python dictionary or using a YAML file:

Using Python Dictionary

from genstates import Machine

class Calculator:
    def mul_wrapper(self, state, x, y):
        """Wrapper around multiplication that ignores state argument."""
        return x * y

# Define state machine configuration
schema = {
    "machine": {"initial_state": "start"},
    "states": {
        "start": {
            "name": "Start State",
            "transitions": {
                "to_double": {
                    "destination": "double",
                    "rule": "(boolean.tautology)",
                    "validation": {
                        "rule": '(condition.gt (basic.field "value") 0)',
                        "message": "Number must be positive"
                    }
                }
            }
        },
        "double": {
            "name": "Double State",
            "action": "mul_wrapper",  # Calculator.mul_wrapper
            "transitions": {
                "to_triple": {
                    "destination": "triple",
                    "rule": "(boolean.tautology)",
                    "validation": {
                        "rule": '(condition.gt (basic.field "value") 0)',
                        "message": "Number must be positive"
                    }
                }
            }
        },
        "triple": {
            "name": "Triple State",
            "action": "mul_wrapper",
            "transitions": {
                "to_triple": {
                    "destination": "triple",
                    "rule": "(boolean.tautology)",
                    "validation": {
                        "rule": '(condition.gt (basic.field "value") 0)',
                        "message": "Number must be positive"
                    }
                }
            }
        }
    }
}

# Create state machine with Calculator instance for actions
machine = Machine(schema, Calculator())

# Process sequence of numbers
numbers = [2, 3, 4]
results = list(machine.map_action(machine.initial, numbers))
# [4, 9, 12]  # Each number is processed through the states

Using YAML File

Alternatively, you can define the state machine in a YAML file (states.yaml):

machine:
  initial_state: start
states:
  start:
    name: Start State
    transitions:
      to_double:
        destination: double
        rule: "(boolean.tautology)"
        validation:
          rule: '(condition.gt (basic.field "value") 0)'
          message: "Number must be positive"
  double:
    name: Double State
    action: mul_wrapper
    transitions:
      to_triple:
        destination: triple
        rule: "(boolean.tautology)"
        validation:
          rule: '(condition.gt (basic.field "value") 0)'
          message: "Number must be positive"
  triple:
    name: Triple State
    action: mul_wrapper
    transitions:
      to_triple:
        destination: triple
        rule: "(boolean.tautology)"
        validation:
          rule: '(condition.gt (basic.field "value") 0)'
          message: "Number must be positive"

Then load and use it in Python:

import yaml  # requires pyyaml package
from genstates import Machine

class Calculator:
    def mul_wrapper(self, state, x, y):
        """Wrapper around multiplication that ignores state argument."""
        return x * y

# Load schema from YAML file
with open('states.yaml') as file:
    schema = yaml.safe_load(file)

# Create state machine with Calculator instance for actions
machine = Machine(schema, Calculator())

# Process sequence of numbers
numbers = [2, 3, 4]
results = list(machine.map_action(machine.initial, numbers))
# [4, 9, 12]  # Each number is processed through the states

Core Concepts

State Machine

The state machine manages a collection of states and their transitions. It:

  • Maintains the current state
  • Handles state transitions based on rules
  • Executes state actions on items
  • Provides methods for processing sequences

States

States represent different stages or conditions in your workflow. Each state can:

  • Have an optional action to process items
  • Define transitions to other states
  • Include metadata like name and description

Transitions

Transitions define how states can change. Each transition:

  • Has a destination state
  • Uses a rule to determine when to trigger
  • Can include metadata like name and description

Actions

Actions are functions that process items in a state. They can be:

  • Instance methods from a class
  • Functions from a Python module
  • Any callable that accepts appropriate arguments

Configuration

Schema Structure

The state machine is configured using a dictionary with this structure:

schema = {
    "machine": {
        "initial_state": "state_key",  # Key of the initial state
    },
    "states": {
        "state_key": {  # Unique key for this state
            "name": "Human Readable Name",  # Display name for the state
            "action": "action_name",  # Optional: Name of action function
            "transitions": {  # Optional: Dictionary of transitions
                "transition_key": {  # Unique key for this transition
                    "name": "Human Readable Name",  # Display name
                    "destination": "destination_state_key",  # Target state
                    "rule": "(boolean.tautology)",  # Transition rule
                    "validation": {  # Optional: Validation for the transition
                        "rule": "(condition.gt 0)",  # Validation rule
                        "message": "Error message if validation fails"  # Custom error message
                    }
                },
            },
        },
    },
}

State Configuration

States are configured with these fields:

  • name: Human-readable name for the state
  • action: Optional name of function to execute
  • transitions: Dictionary of possible transitions

Transition Rules

Transitions use genruler expressions to determine when they trigger. Common patterns:

  • (boolean.tautology): Always transition
  • (condition.equal (basic.field "value") 10): Transition when value equals 10
  • (condition.gt (basic.field "count") 5): Transition when count greater than 5

Features

State Actions

State actions are functions that process items in a state.

Action Resolution

  1. Actions are specified in state configuration:

    "double_state": {
        "name": "Double State",
        "action": "double",  # Name of the function to call
        "transitions": { ... }
    }
    
  2. Functions are looked up in the provided module:

    class NumberProcessor:
        def double(self, state, x, context=None):
            """Wrapper around multiplication that ignores state argument."""
            return x * 2
    
    machine = Machine(schema, NumberProcessor())
    

Action Types

Actions can be defined in several ways. When do_action is called with a context parameter, it is passed as the second argument to the action:

  1. Instance methods:

    class Processor:
        # Without context
        def double(self, state, x):
            # state is the current State object
            # x is the item to process
            return x * 2
    
        # With context
        def process(self, state, context, x):
            # state is the current State object
            # context is passed from do_action
            # x is the item to process
            return x * context['multiplier']
    

    Then set up the state machine as follows:

    machine = Machine(schema, Processor())
    
  2. Module functions (via wrapper class):

    # state_operations.py
    
    # Without context
    def add(state, x, y):
        # state is ignored
        # x and y are items to process
        return x + y
    
    # With context
    def add_with_bonus(state, context, x, y):
        # state is ignored
        # context is passed from do_action
        # x and y are items to process
        return x + y + context['bonus']
    

    Then set up the state machine as follows:

    import state_operations
    
    machine = Machine(schema, state_operations)
    

Calling Actions

Using the OperatorWrapper defined above as an example, actions can be called using do_action. The state machine will resolve the action based on the current state and pass arguments appropriately:

import state_operations

# Define a simple schema with two states
schema = {
    "machine": {"initial_state": "start"},
    "states": {
        "start": {
            "action": "add",
            ...
        },
        "bonus": {
            "action": "add_with_bonus",
            ...
        }
    }
}

# Initialize machine
machine = Machine(schema, state_operations)

# Get state and call action without context
start_state = machine.states["start"]
result = start_state.do_action(3, 4)  # calls add(state, 3, 4)

# Get state and call action with context
start_state = machine.states["bonus"]
context = {'bonus': 10}
result = bonus_state.do_action(3, 4, context=context)  # calls add_with_bonus(state, context, 3, 4)

Sequence Processing

Map Action

Process a sequence of items through the state machine, returning a list of results:

from genstates import Machine

class Calculator:
    def mul_wrapper(self, state, x, y):
        """Wrapper around multiplication that ignores state argument."""
        return x * y

schema = {
    "machine": {"initial_state": "start"},
    "states": {
        "start": {
            "name": "Start State",
            "action": "mul_wrapper",  # Calculator.mul_wrapper
            "transitions": {
                "to_multiply": {
                    "destination": "multiply",
                    "rule": "(boolean.tautology)",
                }
            }
        },
        "multiply": {
            "name": "Multiply State",
            "action": "mul_wrapper",
            "transitions": {
                "to_multiply": {
                    "destination": "multiply",
                    "rule": "(boolean.tautology)",
                }
            }
        }
    }
}

machine = Machine(schema, Calculator())

# Process numbers through the state machine
numbers = [(2,3), (4,5), (6,7)]
result = machine.map_action(machine.initial, numbers)
# Result: [6, 20, 42]

Reduce Action

Process a sequence of items through the state machine, accumulating results:

from genstates import Machine

class Calculator:
    def mul_wrapper(self, state, x, y):
        """Wrapper around multiplication that ignores state argument."""
        return x * y

schema = {
    "machine": {"initial_state": "start"},
    "states": {
        "start": {
            "name": "Start State",
            "action": "mul_wrapper",  # Calculator.mul_wrapper
            "transitions": {
                "to_multiply": {
                    "destination": "multiply",
                    "rule": "(boolean.tautology)",
                }
            }
        },
        "multiply": {
            "name": "Multiply State",
            "action": "mul_wrapper",
            "transitions": {
                "to_multiply": {
                    "destination": "multiply",
                    "rule": "(boolean.tautology)",
                }
            }
        }
    }
}

machine = Machine(schema, Calculator())

# Process numbers through the state machine
numbers = [2, 3, 4]
result = machine.reduce_action(machine.initial, numbers)
# Result: 24 (first mul: 2*3=6, then mul: 6*4=24)

Foreach Action

Process a sequence of items through the state machine, executing each state's action on the items as they flow through:

from genstates import Machine

# Module to store processed results
class Module:
    def __init__(self):
        self.processed = []

    def collect(self, state, x):
        self.processed.append(x)
        return x

    def double(self, state, x):
        result = x * 2
        self.processed.append(result)
        return result

# Create module instance to store results
module = Module()

schema = {
    "machine": {"initial_state": "start"},
    "states": {
        "start": {
            "action": "collect",  # collect items
            "transitions": {
                "next": {
                    "destination": "double",
                    "rule": "(boolean.tautology)",
                }
            }
        },
        "double": {
            "action": "double",  # double items
        }
    }
}

machine = Machine(schema, module)

# Process items through the state machine
items = [1, 2, 3]
machine.foreach_action(machine.initial, items)

# First item: progress from start -> double, then double(1) -> [2]
# Next items: progress back to double state, double(2) -> [2, 4], double(3) -> [2, 4, 6]
print(module.processed)  # [2, 4, 6]

Unlike map_action which returns results, foreach_action is used when you want to execute state actions for their side effects (e.g., saving to a database, sending notifications) rather than collecting return values.

Visualization

Export state machine as a Graphviz DOT string:

dot_string = machine.graph()

# Generate visualization using graphviz
import graphviz
graph = graphviz.Source(dot_string)
graph.render("state_machine", format="png")

Graphviz output

Advanced Usage

Custom Action Modules

Create custom modules for complex processing:

class DataProcessor:
    def __init__(self, config):
        self.config = config

    def process(self, data):
        # Complex processing logic
        return processed_data

machine = Machine(schema, DataProcessor(config))

Complex State Transitions

Use transition rules for complex logic:

"transitions": {
    "to_error": {
        "destination": "error",
        "rule": """(boolean.and
            (condition.gt (basic.field "retries") 3)
            (condition.equal (basic.field "status") "failed")
        )""",
    }
}

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

This project is licensed under the MIT License.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

genstates-0.2.0.tar.gz (12.4 kB view details)

Uploaded Source

Built Distribution

genstates-0.2.0-py3-none-any.whl (10.3 kB view details)

Uploaded Python 3

File details

Details for the file genstates-0.2.0.tar.gz.

File metadata

  • Download URL: genstates-0.2.0.tar.gz
  • Upload date:
  • Size: 12.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.8.4 CPython/3.12.7 Linux/6.11.0-8-generic

File hashes

Hashes for genstates-0.2.0.tar.gz
Algorithm Hash digest
SHA256 c2a825af1ce4bd679ae9255e647c54bf6c99f5fff0a602e6dfb0f16427b23baa
MD5 a307c7eaa7c2472499b45f7288700489
BLAKE2b-256 9e7cafadf03e8f59746d27c3a86294e950b4b695b9038bcfbf49fd45b1ee43e8

See more details on using hashes here.

File details

Details for the file genstates-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: genstates-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 10.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.8.4 CPython/3.12.7 Linux/6.11.0-8-generic

File hashes

Hashes for genstates-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ab651f98139f077828ac53d6f7ffc052e653348fbefefb98a80cfc80016c688d
MD5 5d3d66e91472be89ffba2c6272046908
BLAKE2b-256 31095de4ba022a7dda7f44e2b5e90ec118e0efba9e9107ae1d0fe73a13e1dbc3

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page