Skip to main content

A Python state management library inspired by NgRx

Project description

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 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_feature to add/remove features at runtime.
  • Immutable State: Integrate immutables.Map for structural sharing.
  • DevTools: Capture action/state history for time-travel debugging.

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:
    python -m twine upload dist/*
    

Contributing

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

License

MIT

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

pystorex-0.2.0.tar.gz (62.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pystorex-0.2.0-py3-none-any.whl (77.4 kB view details)

Uploaded Python 3

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

Hashes for pystorex-0.2.0.tar.gz
Algorithm Hash digest
SHA256 da70e711d5800367818733f19b7fa8810c3d1e24f60293f634aa67e1e9538f56
MD5 27090c6721621a0d3ea5229345f8bc9d
BLAKE2b-256 172c7e8d23a923eade5b72a8cda88a30cac1daa40477729de3b5ceddfb02bd04

See more details on using hashes here.

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

Hashes for pystorex-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7331f9719044de3bce493037313326b54a154e2b800b43c82185af1067e9c86e
MD5 f3351f498717ef7b5377258e03ef47d5
BLAKE2b-256 b8176c3755454e6db7acc8bdb7efec6b3ac6fb8b71372c95a82f6a7fd002fc32

See more details on using hashes here.

Supported by

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