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 Pydantic or any Python object, 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 dispatch pipeline.
- Selectors: Memoized and configurable (deep compare, TTL) state accessors.
- Immutable Updates: Shallow copy at feature level or integrate with
immutables.Map. - Hot Module Management: Register/unregister feature reducers and effects at runtime.
Installation
pip install pystorex
Requires Python 3.7+
Quick Start
import time
from typing import Optional
from pydantic import BaseModel
from reactivex import operators as ops
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
# 1. 定義狀態模型
class CounterState(BaseModel):
count: int = 0
loading: bool = False
error: Optional[str] = None
last_updated: Optional[float] = None
# 2. 定義 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. 定義 Reducer
def counter_handler(state: CounterState, action) -> CounterState:
new_state = state.copy(deep=True)
now = time.time()
if action.type == increment.type:
new_state.count += 1
new_state.last_updated = now
elif action.type == decrement.type:
new_state.count -= 1
new_state.last_updated = now
elif action.type == reset.type:
new_state.count = action.payload
new_state.last_updated = now
elif action.type == increment_by.type:
new_state.count += action.payload
new_state.last_updated = now
elif action.type == load_count_request.type:
new_state.loading = True
new_state.error = None
elif action.type == load_count_success.type:
new_state.loading = False
new_state.count = action.payload
new_state.last_updated = now
elif action.type == load_count_failure.type:
new_state.loading = False
new_state.error = action.payload
return new_state
counter_reducer = create_reducer(
CounterState(),
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. 定義 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. 建立 Store、註冊模組
store = create_store()
store.apply_middleware(LoggerMiddleware)
store.register_root({"counter": counter_reducer})
store.register_effects(CounterEffects)
# 6. 訂閱狀態與測試
get_counter_state = lambda state: state["counter"]
get_count = create_selector(
get_counter_state,
result_fn=lambda counter: counter.count or 0
)
store.select(get_count).subscribe(
lambda c: print(f"Count: {c[1]}")
)
# 7. 執行操作示例
if __name__ == "__main__":
store.dispatch(increment())
store.dispatch(increment_by(5))
store.dispatch(decrement())
store.dispatch(reset(10))
store.dispatch(load_count_request())
# 給 Effects 一些時間
time.sleep(2)
Examples
This project includes the following example scripts to demonstrate both the modular and monolithic usage patterns:
Counter Example
examples/counter_example/main.py: Entry point for the modular Counter example.examples/counter_example/counter_example_monolithic.py: Monolithic Counter example.
Detection Example
examples/detection_example/main.py: Entry point for the modular Detection example.examples/detection_example/detection_example_monolithic.py: Monolithic Detection example.
You can run them from the project root:
python examples/counter_example/main.py
python examples/counter_example/counter_example_monolithic.py
python examples/detection_example/main.py
python examples/detection_example/detection_example_monolithic.py
Core Concepts
Store
Manages application state, dispatches actions, and notifies subscribers.
store = create_store(MyRootState())
store.register_root({
"feature_key": feature_reducer,
# ... more reducers
})
store.register_effects(FeatureEffects)
Actions
Use create_action(type, prepare_fn) to define action creators.
from pystorex.actions import create_action
my_action = create_action("myAction", lambda data: {"payload": data})
Reducers
Pure functions taking (state, action) and returning new state.
from pystorex import create_reducer, on
reducer = create_reducer(
InitialState(),
on(my_action, my_handler)
)
Effects
Side-effect handlers listening to action streams via ReactiveX.
from pystorex import create_effect
from reactivex import operators as ops
class FeatureEffects:
@create_effect
def log_actions(action$):
return action$.pipe(
ops.filter(lambda a: a.type == my_action.type),
ops.map(lambda _: another_action())
)
Middleware
Insert custom dispatch logic. Example: Logger
class LoggerMiddleware:
def on_next(self, action): print("▶️", action.type)
def on_complete(self, result, action): print("✅", action)
def on_error(self, err, action): print("❌", err)
store.apply_middleware(LoggerMiddleware)
Selectors
Memoized accessors with optional deep=True or ttl.
from pystorex.selectors import create_selector
get_items = create_selector(
lambda s: s.feature.items,
result_fn=lambda items: [i.value for i in items],
deep=True, ttl=5.0
)
Advanced Topics
- Hot Module DnD:
store.register_feature/store.unregister_featureto add/remove features at runtime. - Immutable State: Integrate
immutables.Mapfor structural sharing. - DevTools: Capture action/state history for time-travel debugging.
Publishing to PyPI
- Ensure
pyproject.toml&setup.cfgare configured. - Install build tools:
pip install --upgrade build twine
- Build distributions:
python -m build
- Upload:
python -m 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.0.tar.gz.
File metadata
- Download URL: pystorex-0.2.0.tar.gz
- Upload date:
- Size: 62.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.11.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
da70e711d5800367818733f19b7fa8810c3d1e24f60293f634aa67e1e9538f56
|
|
| MD5 |
27090c6721621a0d3ea5229345f8bc9d
|
|
| BLAKE2b-256 |
172c7e8d23a923eade5b72a8cda88a30cac1daa40477729de3b5ceddfb02bd04
|
File details
Details for the file pystorex-0.2.0-py3-none-any.whl.
File metadata
- Download URL: pystorex-0.2.0-py3-none-any.whl
- Upload date:
- Size: 77.4 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 |
7331f9719044de3bce493037313326b54a154e2b800b43c82185af1067e9c86e
|
|
| MD5 |
f3351f498717ef7b5377258e03ef47d5
|
|
| BLAKE2b-256 |
b8176c3755454e6db7acc8bdb7efec6b3ac6fb8b71372c95a82f6a7fd002fc32
|