Skip to main content

Pystorex

pystorex icon

PyPI version Python versions Documentation License Ask DeepWiki

A lightweight Python state management library inspired by NgRx/Redux patterns and ReactiveX for Python (reactivex). Manage application state with reducers, handle side effects with effects, compose middleware, and select state slices efficiently.


Features

  • Typed State: Define your root state using typing_extensions.TypedDict and store it in immutables.Map, fully generic.
  • Reducers: Pure functions to update state in response to actions.
  • Effects: Handle side effects by listening to action streams and optionally dispatching actions.
  • Middleware: Insert custom logic (logging, thunks, error handling) into the dispatch pipeline.
  • Selectors: Memoized and configurable (deep compare, TTL) state accessors.
  • Immutable Updates: Use shallow copy at the feature level or integrate with immutables.Map and utility functions (to_immutable, to_dict, to_pydantic).
  • Hot Module Management: Register/unregister feature reducers and effects at runtime.

Installation

pip install pystorex

Requires Python 3.9+ support.


Quick Start

This example demonstrates how to define state using TypedDict and handle state updates with immutables.Map for better performance and clarity.

import time
from typing import Optional
from typing_extensions import TypedDict
from reactivex import operators as ops
from immutables import Map

from pystorex.actions import create_action
from pystorex import create_store, create_reducer, on, create_effect
from pystorex.store_selectors import create_selector
from pystorex.middleware import LoggerMiddleware
from pystorex.map_utils import batch_update

# 1. Define state model (TypedDict)
class CounterState(TypedDict):
    count: int
    loading: bool
    error: Optional[str]
    last_updated: Optional[float]

counter_initial_state = CounterState(
    count=0, loading=False, error=None, last_updated=None
)

# 2. Define Actions
increment = create_action("increment")
decrement = create_action("decrement")
reset = create_action("reset", lambda value: value)
increment_by = create_action("incrementBy", lambda amount: amount)
load_count_request = create_action("loadCountRequest")
load_count_success = create_action("loadCountSuccess", lambda value: value)
load_count_failure = create_action("loadCountFailure", lambda error: error)

# 3. Define Reducer

def counter_handler(state: Map, action) -> Map:
    now = time.time()
    if action.type == increment.type:
        return state.set("count", state["count"] + 1).set("last_updated", now)
    elif action.type == decrement.type:
        return batch_update(state, {"count": state["count"] - 1, "last_updated": now})
    elif action.type == reset.type:
        return batch_update(state, {"count": action.payload, "last_updated": now})
    elif action.type == increment_by.type:
        return batch_update(state, {"count": state["count"] + action.payload, "last_updated": now})
    elif action.type == load_count_request.type:
        return batch_update(state, {"loading": True, "error": None})
    elif action.type == load_count_success.type:
        return batch_update(state, {"loading": False, "count": action.payload, "last_updated": now})
    elif action.type == load_count_failure.type:
        return batch_update(state, {"loading": False, "error": action.payload})
    return state

counter_reducer = create_reducer(
    counter_initial_state,
    on(increment, counter_handler),
    on(decrement, counter_handler),
    on(reset, counter_handler),
    on(increment_by, counter_handler),
    on(load_count_request, counter_handler),
    on(load_count_success, counter_handler),
    on(load_count_failure, counter_handler),
)

# 4. Define Effects
class CounterEffects:
    @create_effect
    def load_count(self, action_stream):
        return action_stream.pipe(
            ops.filter(lambda action: action.type == load_count_request.type),
            ops.do_action(lambda _: print("Effect: Loading counter...")),
            ops.delay(1.0),
            ops.map(lambda _: load_count_success(42))
        )

# 5. Create Store and Register Modules
store = create_store()
store.apply_middleware(LoggerMiddleware)
store.register_root({"counter": counter_reducer})
store.register_effects(CounterEffects)

# 6. Use Selector to Subscribe to State
get_counter_state = lambda state: state["counter"]
get_count = create_selector(
    get_counter_state,
    result_fn=lambda counter: counter.get("count", 0)
)
store.select(get_count).subscribe(
    lambda c: print(f"Count: {c[1]}")
)

# 7. Execute Example Operations
if __name__ == "__main__":
    store.dispatch(increment())
    store.dispatch(increment_by(5))
    store.dispatch(decrement())
    store.dispatch(reset(10))
    store.dispatch(load_count_request())
    # Give Effects some time
    time.sleep(2)

Notes

  • State management has been updated to use TypedDict and immutables.Map, avoiding the performance overhead of Pydantic models during frequent state updates.
  • Use batch_update and built-in methods of immutables.Map to ensure immutability of state updates.
  • Pydantic models can be dynamically converted as needed using utility functions. See the example source code for details.

Examples

This project includes example scripts demonstrating monolithic and modular usage patterns:

  • examples/counter_example/counter_example_monolithic.py: Monolithic Counter example using TypedDict + Map.
  • examples/counter_example/main.py: Modular Counter example.
  • examples/detection_example/...: Detection examples.

Run them with:

python examples/counter_example/counter_example_monolithic.py
python examples/counter_example/main.py

Publishing to PyPI

  1. Ensure pyproject.toml & setup.cfg are configured.

  2. Install build tools:

    pip install --upgrade build twine
    
  3. Build distributions:

    python -m build
    
  4. Upload:

    twine upload dist/*
    

Contributing

  • Fork the repo
  • Create a feature branch
  • Write tests (pytest) and update docs
  • Submit a Pull Request

License

MIT

Metadata

Release files for pystorex 0.2.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pystorex 0.2.1
File Size Uploaded
pystorex-0.2.1.tar.gz 61.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pystorex 0.2.1
File Interpreter ABI Platform
pystorex-0.2.1-py3-none-any.whl Python 3 none any Details

Total release size: 138.6 kB

Release files / pystorex-0.2.1.tar.gz

Download URL pystorex-0.2.1.tar.gz
Size 61.7 kB
Tags Source
SHA-256 checksum
How to use checksums
089d3e09c38f4097db9eb462755c3867805fae60c55dde64859a92333d5bf938
BLAKE2b-256 checksum
How to use checksums
8ce7d3af4e776e857d2ecad40b82478c069f1f758337714809b27d5b2d11f8a7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.11.9

Release files / pystorex-0.2.1-py3-none-any.whl

Download URL pystorex-0.2.1-py3-none-any.whl
Size 76.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3ed36741201fa7d3375844b0ca1441298be901c84e978d091d2df8c5c491d43a
BLAKE2b-256 checksum
How to use checksums
23969a49027ffff0eed089f8ed364a27c08462eb8031fdd65f7c0af5d9a30e31
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.11.9

Release history Release notifications | RSS feed

This release

0.2.1 This release

2 release files

0.2.0

2 release files

0.1.8

2 release files

0.1.7

2 release files

0.1.6

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page