A Python state management library inspired by NgRx
Project description
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
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pystorex-0.2.1.tar.gz.
File metadata
- Download URL: pystorex-0.2.1.tar.gz
- Upload date:
- Size: 61.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.11.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
089d3e09c38f4097db9eb462755c3867805fae60c55dde64859a92333d5bf938
|
|
| MD5 |
1fa6a25518923daa9b7afe87f3b22259
|
|
| BLAKE2b-256 |
8ce7d3af4e776e857d2ecad40b82478c069f1f758337714809b27d5b2d11f8a7
|
File details
Details for the file pystorex-0.2.1-py3-none-any.whl.
File metadata
- Download URL: pystorex-0.2.1-py3-none-any.whl
- Upload date:
- Size: 76.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.11.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3ed36741201fa7d3375844b0ca1441298be901c84e978d091d2df8c5c491d43a
|
|
| MD5 |
ed0272b87393a075b92f02f3c04b1c63
|
|
| BLAKE2b-256 |
23969a49027ffff0eed089f8ed364a27c08462eb8031fdd65f7c0af5d9a30e31
|