Pystorex
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.TypedDictand store it inimmutables.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.Mapand 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
TypedDictandimmutables.Map, avoiding the performance overhead of Pydantic models during frequent state updates. - Use
batch_updateand built-in methods ofimmutables.Mapto 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
-
Ensure
pyproject.toml&setup.cfgare configured. -
Install build tools:
pip install --upgrade build twine
-
Build distributions:
python -m build
-
Upload:
twine upload dist/*
Contributing
- Fork the repo
- Create a feature branch
- Write tests (pytest) and update docs
- Submit a Pull Request
License
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)
| File | Size | Uploaded | |
|---|---|---|---|
| pystorex-0.2.1.tar.gz | 61.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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
|