python-redux


Namepython-redux JSON
Version 0.15.9 PyPI version JSON
download
home_pagehttps://github.com/sassanh/python-redux/
SummaryRedux implementation for Python
upload_time2024-06-25 18:01:59
maintainerNone
docs_urlNone
authorSassan Haradji
requires_python<4.0,>=3.11
licenseApache-2.0
keywords store redux reactive autorun
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # 🎛️ Python Redux

## 🌟 Overview

Python Redux is a Redux implementation for Python, bringing Redux's state management
architecture to Python applications.

### 🔎 Sample Usage

Minimal todo application store implemented using python-redux:

```python
import uuid
from dataclasses import replace
from typing import Sequence

from immutable import Immutable

from redux import (
    BaseAction,
    BaseEvent,
    CompleteReducerResult,
    FinishAction,
    ReducerResult,
)
from redux.main import Store


# state:
class ToDoItem(Immutable):
    id: str
    content: str
    is_done: bool = False


class ToDoState(Immutable):
    items: Sequence[ToDoItem]


# actions:
class AddTodoItemAction(BaseAction):
    content: str


class MarkTodoItemDone(BaseAction):
    id: str


class RemoveTodoItemAction(BaseAction):
    id: str


# events:
class CallApi(BaseEvent):
    parameters: object


# reducer:
def reducer(
    state: ToDoState | None,
    action: BaseAction,
) -> ReducerResult[ToDoState, BaseAction, BaseEvent]:
    if state is None:
        return ToDoState(
            items=[
                ToDoItem(
                    id=uuid.uuid4().hex,
                    content='Initial Item',
                ),
            ],
        )
    if isinstance(action, AddTodoItemAction):
        return replace(
            state,
            items=[
                *state.items,
                ToDoItem(
                    id=uuid.uuid4().hex,
                    content=action.content,
                ),
            ],
        )
    if isinstance(action, RemoveTodoItemAction):
        return replace(
            state,
            actions=[item for item in state.items if item.id != action.id],
        )
    if isinstance(action, MarkTodoItemDone):
        return CompleteReducerResult(
            state=replace(
                state,
                items=[
                    replace(item, is_done=True) if item.id == action.id else item
                    for item in state.items
                ],
            ),
            events=[CallApi(parameters={})],
        )
    return state


store = Store(reducer)


# subscription:
dummy_render = print
store.subscribe(dummy_render)


# autorun:
@store.autorun(
    lambda state: state.items[0].content if len(state.items) > 0 else None,
)
def reaction(content: str | None) -> None:
    print(content)


@store.view(lambda state: state.items[0])
def first_item(first_item: ToDoItem) -> ToDoItem:
    return first_item


@store.view(lambda state: [item for item in state.items if item.is_done])
def done_items(done_items: list[ToDoItem]) -> list[ToDoItem]:
    return done_items


# event listener, note that this will run async in a separate thread, so it can include
# io operations like network calls, etc:
dummy_api_call = print
store.subscribe_event(
    CallApi,
    lambda event: dummy_api_call(event.parameters, done_items()),
)

# dispatch:
store.dispatch(AddTodoItemAction(content='New Item'))

store.dispatch(MarkTodoItemDone(id=first_item().id))

store.dispatch(FinishAction())
```

## ⚙️ Features

- Redux API for Python developers.

- Reduce boilerplate by dropping `type` property, payload classes and action creators:

  - Each action is a subclass of `BaseAction`.
  - Its type is checked by utilizing `isinstance` (no need for `type` property).
  - Its payload are its direct properties (no need for a separate `payload` object).
  - Its creator is its auto-generated constructor.

- Use type annotations for all its API.

- Immutable state management for predictable state updates using [python-immutable](https://github.com/sassanh/python-immutable).

- Offers a streamlined, native [API](#handling-side-effects-with-events) for handling
  side-effects asynchronously, eliminating the necessity for more intricate utilities
  such as redux-thunk or redux-saga.

- Incorporates the [autorun decorator](#autorun-decorator) and
  the [view decorator](#view-decorator), inspired by the mobx framework, to better
  integrate with elements of the software following procedural patterns.

- Supports middlewares.

## 📦 Installation

The package handle in PyPI is `python-redux`

### Pip

```bash
pip install python-redux
```

### Poetry

```bash
poetry add python-redux
```

## 🛠 Usage

### Handling Side Effects with Events

Python-redux introduces a powerful concept for managing side effects: **Events**.
This approach allows reducers to remain pure while still signaling the need for
side effects.

#### Why Events?

- **Separation of Concerns**: By returning events, reducers stay pure and focused
  solely on state changes, delegating side effects to other parts of the software.
- **Flexibility**: Events allow asynchronous operations like API calls to be handled
  separately, enhancing scalability and maintainability.

#### How to Use Events

- **Reducers**: Reducers primarily return a new state. They can optionally return
  actions and events, maintaining their purity as these do not enact side effects
  themselves.
- **Dispatch Function**: Besides actions, dispatch function can now accept events,
  enabling a more integrated flow of state and side effects.
- **Event Listeners**: Implement listeners for specific events. These listeners
  handle the side effects (e.g., API calls) asynchronously.

#### Best Practices

- **Define Clear Events**: Create well-defined events that represent specific side
  effects.
- **Use Asynchronously**: Design event listeners to operate asynchronously, keeping
  your application responsive. Note that python-redux, by default, runs all event
  handler functions in new threads.

This concept fills the gap in handling side effects within Redux's ecosystem, offering
a more nuanced and integrated approach to state and side effect management.

See todo sample below or check the [todo demo](/tests/test_todo.py) or
[features demo](/tests/test_features.py) to see it in action.

### Autorun Decorator

Inspired by MobX's [autorun](https://mobx.js.org/reactions.html#autorun) and
[reaction](https://mobx.js.org/reactions.html#reaction), python-redux introduces
the autorun decorator. This decorator requires a selector function as an argument.
The selector is a function that accepts the store instance and returns a derived
object from the store's state. The primary function of autorun is to establish a
subscription to the store. Whenever the store is changed, autorun executes the
selector with the updated store.
Importantly, the decorated function is triggered only if there is a change in the
selector's return value. This mechanism ensures that the decorated function runs
in response to relevant state changes, enhancing efficiency and responsiveness in
the application.

See todo sample below or check the [todo demo](/tests/test_todo.py) or
[features demo](/tests/test_features.py) to see it in action.

### View Decorator

Inspired by MobX's [computed](https://mobx.js.org/computeds.html), python-redux introduces
the view decorator. It takes a selector and each time the decorated function is called,
it only runs the function body if the returned value of the selector is changed,
otherwise it simply returns the previous value. So unlike `computed` of MobX, it
doesn't extract the requirements of the function itself, you need to provide them
in the return value of the selector function.

### Combining reducers - `combine_reducers`

You can compose high level reducers by combining smaller reducers using `combine_reducers`
utility function. This works mostly the same as the JS redux library version except
that it provides a mechanism to dynamically add/remove reducers to/from it.
This is done by generating an id and returning it along the generated reducer.
This id is used to refer to this reducer in the future. Let's assume you composed
a reducer like this:

```python
reducer, reducer_id = combine_reducers(
    state_type=StateType,
    first=straight_reducer,
    second=second_reducer,
)
```

You can then add a new reducer to it using the `reducer_id` like this:

```python
store.dispatch(
    CombineReducerRegisterAction(
        _id=reducer_id,
        key='third',
        third=third_reducer,
    ),
)
```

You can also remove a reducer from it like this:

```python
store.dispatch(
    CombineReducerRegisterAction(
        _id=reducer_id,
        key='second',
    ),
)
```

Without this id, all the combined reducers in the store tree would register `third`
reducer and unregister `second` reducer, but thanks to this `reducer_id`, these
actions will only target the desired combined reducer.

## 🎉 Demo

For a detailed example, see [features demo](/tests/test_features.py).

## 🤝 Contributing

Contributions following Python best practices are welcome.

## 📜 License

This project is released under the Apache-2.0 License. See the [LICENSE](./LICENSE)
file for more details.


            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/sassanh/python-redux/",
    "name": "python-redux",
    "maintainer": null,
    "docs_url": null,
    "requires_python": "<4.0,>=3.11",
    "maintainer_email": null,
    "keywords": "store, redux, reactive, autorun",
    "author": "Sassan Haradji",
    "author_email": "sassanh@gmail.com",
    "download_url": "https://files.pythonhosted.org/packages/3f/97/2873795a486ac66a4f4b1a2b57ece4060c5301d25ff1938802399a7b705f/python_redux-0.15.9.tar.gz",
    "platform": null,
    "description": "# \ud83c\udf9b\ufe0f Python Redux\n\n## \ud83c\udf1f Overview\n\nPython Redux is a Redux implementation for Python, bringing Redux's state management\narchitecture to Python applications.\n\n### \ud83d\udd0e Sample Usage\n\nMinimal todo application store implemented using python-redux:\n\n```python\nimport uuid\nfrom dataclasses import replace\nfrom typing import Sequence\n\nfrom immutable import Immutable\n\nfrom redux import (\n    BaseAction,\n    BaseEvent,\n    CompleteReducerResult,\n    FinishAction,\n    ReducerResult,\n)\nfrom redux.main import Store\n\n\n# state:\nclass ToDoItem(Immutable):\n    id: str\n    content: str\n    is_done: bool = False\n\n\nclass ToDoState(Immutable):\n    items: Sequence[ToDoItem]\n\n\n# actions:\nclass AddTodoItemAction(BaseAction):\n    content: str\n\n\nclass MarkTodoItemDone(BaseAction):\n    id: str\n\n\nclass RemoveTodoItemAction(BaseAction):\n    id: str\n\n\n# events:\nclass CallApi(BaseEvent):\n    parameters: object\n\n\n# reducer:\ndef reducer(\n    state: ToDoState | None,\n    action: BaseAction,\n) -> ReducerResult[ToDoState, BaseAction, BaseEvent]:\n    if state is None:\n        return ToDoState(\n            items=[\n                ToDoItem(\n                    id=uuid.uuid4().hex,\n                    content='Initial Item',\n                ),\n            ],\n        )\n    if isinstance(action, AddTodoItemAction):\n        return replace(\n            state,\n            items=[\n                *state.items,\n                ToDoItem(\n                    id=uuid.uuid4().hex,\n                    content=action.content,\n                ),\n            ],\n        )\n    if isinstance(action, RemoveTodoItemAction):\n        return replace(\n            state,\n            actions=[item for item in state.items if item.id != action.id],\n        )\n    if isinstance(action, MarkTodoItemDone):\n        return CompleteReducerResult(\n            state=replace(\n                state,\n                items=[\n                    replace(item, is_done=True) if item.id == action.id else item\n                    for item in state.items\n                ],\n            ),\n            events=[CallApi(parameters={})],\n        )\n    return state\n\n\nstore = Store(reducer)\n\n\n# subscription:\ndummy_render = print\nstore.subscribe(dummy_render)\n\n\n# autorun:\n@store.autorun(\n    lambda state: state.items[0].content if len(state.items) > 0 else None,\n)\ndef reaction(content: str | None) -> None:\n    print(content)\n\n\n@store.view(lambda state: state.items[0])\ndef first_item(first_item: ToDoItem) -> ToDoItem:\n    return first_item\n\n\n@store.view(lambda state: [item for item in state.items if item.is_done])\ndef done_items(done_items: list[ToDoItem]) -> list[ToDoItem]:\n    return done_items\n\n\n# event listener, note that this will run async in a separate thread, so it can include\n# io operations like network calls, etc:\ndummy_api_call = print\nstore.subscribe_event(\n    CallApi,\n    lambda event: dummy_api_call(event.parameters, done_items()),\n)\n\n# dispatch:\nstore.dispatch(AddTodoItemAction(content='New Item'))\n\nstore.dispatch(MarkTodoItemDone(id=first_item().id))\n\nstore.dispatch(FinishAction())\n```\n\n## \u2699\ufe0f Features\n\n- Redux API for Python developers.\n\n- Reduce boilerplate by dropping `type` property, payload classes and action creators:\n\n  - Each action is a subclass of `BaseAction`.\n  - Its type is checked by utilizing `isinstance` (no need for `type` property).\n  - Its payload are its direct properties (no need for a separate `payload` object).\n  - Its creator is its auto-generated constructor.\n\n- Use type annotations for all its API.\n\n- Immutable state management for predictable state updates using [python-immutable](https://github.com/sassanh/python-immutable).\n\n- Offers a streamlined, native [API](#handling-side-effects-with-events) for handling\n  side-effects asynchronously, eliminating the necessity for more intricate utilities\n  such as redux-thunk or redux-saga.\n\n- Incorporates the [autorun decorator](#autorun-decorator) and\n  the [view decorator](#view-decorator), inspired by the mobx framework, to better\n  integrate with elements of the software following procedural patterns.\n\n- Supports middlewares.\n\n## \ud83d\udce6 Installation\n\nThe package handle in PyPI is `python-redux`\n\n### Pip\n\n```bash\npip install python-redux\n```\n\n### Poetry\n\n```bash\npoetry add python-redux\n```\n\n## \ud83d\udee0 Usage\n\n### Handling Side Effects with Events\n\nPython-redux introduces a powerful concept for managing side effects: **Events**.\nThis approach allows reducers to remain pure while still signaling the need for\nside effects.\n\n#### Why Events?\n\n- **Separation of Concerns**: By returning events, reducers stay pure and focused\n  solely on state changes, delegating side effects to other parts of the software.\n- **Flexibility**: Events allow asynchronous operations like API calls to be handled\n  separately, enhancing scalability and maintainability.\n\n#### How to Use Events\n\n- **Reducers**: Reducers primarily return a new state. They can optionally return\n  actions and events, maintaining their purity as these do not enact side effects\n  themselves.\n- **Dispatch Function**: Besides actions, dispatch function can now accept events,\n  enabling a more integrated flow of state and side effects.\n- **Event Listeners**: Implement listeners for specific events. These listeners\n  handle the side effects (e.g., API calls) asynchronously.\n\n#### Best Practices\n\n- **Define Clear Events**: Create well-defined events that represent specific side\n  effects.\n- **Use Asynchronously**: Design event listeners to operate asynchronously, keeping\n  your application responsive. Note that python-redux, by default, runs all event\n  handler functions in new threads.\n\nThis concept fills the gap in handling side effects within Redux's ecosystem, offering\na more nuanced and integrated approach to state and side effect management.\n\nSee todo sample below or check the [todo demo](/tests/test_todo.py) or\n[features demo](/tests/test_features.py) to see it in action.\n\n### Autorun Decorator\n\nInspired by MobX's [autorun](https://mobx.js.org/reactions.html#autorun) and\n[reaction](https://mobx.js.org/reactions.html#reaction), python-redux introduces\nthe autorun decorator. This decorator requires a selector function as an argument.\nThe selector is a function that accepts the store instance and returns a derived\nobject from the store's state. The primary function of autorun is to establish a\nsubscription to the store. Whenever the store is changed, autorun executes the\nselector with the updated store.\nImportantly, the decorated function is triggered only if there is a change in the\nselector's return value. This mechanism ensures that the decorated function runs\nin response to relevant state changes, enhancing efficiency and responsiveness in\nthe application.\n\nSee todo sample below or check the [todo demo](/tests/test_todo.py) or\n[features demo](/tests/test_features.py) to see it in action.\n\n### View Decorator\n\nInspired by MobX's [computed](https://mobx.js.org/computeds.html), python-redux introduces\nthe view decorator. It takes a selector and each time the decorated function is called,\nit only runs the function body if the returned value of the selector is changed,\notherwise it simply returns the previous value. So unlike `computed` of MobX, it\ndoesn't extract the requirements of the function itself, you need to provide them\nin the return value of the selector function.\n\n### Combining reducers - `combine_reducers`\n\nYou can compose high level reducers by combining smaller reducers using `combine_reducers`\nutility function. This works mostly the same as the JS redux library version except\nthat it provides a mechanism to dynamically add/remove reducers to/from it.\nThis is done by generating an id and returning it along the generated reducer.\nThis id is used to refer to this reducer in the future. Let's assume you composed\na reducer like this:\n\n```python\nreducer, reducer_id = combine_reducers(\n    state_type=StateType,\n    first=straight_reducer,\n    second=second_reducer,\n)\n```\n\nYou can then add a new reducer to it using the `reducer_id` like this:\n\n```python\nstore.dispatch(\n    CombineReducerRegisterAction(\n        _id=reducer_id,\n        key='third',\n        third=third_reducer,\n    ),\n)\n```\n\nYou can also remove a reducer from it like this:\n\n```python\nstore.dispatch(\n    CombineReducerRegisterAction(\n        _id=reducer_id,\n        key='second',\n    ),\n)\n```\n\nWithout this id, all the combined reducers in the store tree would register `third`\nreducer and unregister `second` reducer, but thanks to this `reducer_id`, these\nactions will only target the desired combined reducer.\n\n## \ud83c\udf89 Demo\n\nFor a detailed example, see [features demo](/tests/test_features.py).\n\n## \ud83e\udd1d Contributing\n\nContributions following Python best practices are welcome.\n\n## \ud83d\udcdc License\n\nThis project is released under the Apache-2.0 License. See the [LICENSE](./LICENSE)\nfile for more details.\n\n",
    "bugtrack_url": null,
    "license": "Apache-2.0",
    "summary": "Redux implementation for Python",
    "version": "0.15.9",
    "project_urls": {
        "Homepage": "https://github.com/sassanh/python-redux/",
        "Repository": "https://github.com/sassanh/python-redux/"
    },
    "split_keywords": [
        "store",
        " redux",
        " reactive",
        " autorun"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "223dcc9f25c7818848bcd44d65a7253b5ccd5251fa9dec866c2c665589f60bc8",
                "md5": "417bd856f9daf1868120438a5089b4b8",
                "sha256": "6807ef0d3e28821d96b9ee24976731213d94798ab50e6173d2e7c9c61ff4aff3"
            },
            "downloads": -1,
            "filename": "python_redux-0.15.9-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "417bd856f9daf1868120438a5089b4b8",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": "<4.0,>=3.11",
            "size": 25049,
            "upload_time": "2024-06-25T18:01:57",
            "upload_time_iso_8601": "2024-06-25T18:01:57.328687Z",
            "url": "https://files.pythonhosted.org/packages/22/3d/cc9f25c7818848bcd44d65a7253b5ccd5251fa9dec866c2c665589f60bc8/python_redux-0.15.9-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "3f972873795a486ac66a4f4b1a2b57ece4060c5301d25ff1938802399a7b705f",
                "md5": "cbc99b0b5bfe272ff86e438ac7961051",
                "sha256": "5d4374907e26d0b6916dc6fb32f2a54cba87eebd8ed6e4c5fb4b35951cdd0871"
            },
            "downloads": -1,
            "filename": "python_redux-0.15.9.tar.gz",
            "has_sig": false,
            "md5_digest": "cbc99b0b5bfe272ff86e438ac7961051",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": "<4.0,>=3.11",
            "size": 22650,
            "upload_time": "2024-06-25T18:01:59",
            "upload_time_iso_8601": "2024-06-25T18:01:59.369377Z",
            "url": "https://files.pythonhosted.org/packages/3f/97/2873795a486ac66a4f4b1a2b57ece4060c5301d25ff1938802399a7b705f/python_redux-0.15.9.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-06-25 18:01:59",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "sassanh",
    "github_project": "python-redux",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "python-redux"
}
        
Elapsed time: 0.53898s