# -*- coding: utf-8 -*-
from setuptools import setup

packages = \
['unicycle']

package_data = \
{'': ['*']}

install_requires = \
['anyio>=3.5.0,<4.0.0']

setup_kwargs = {
    'name': 'unicycle',
    'version': '0.0.1',
    'description': 'Predictable, unidirectional state store for Python.',
    'long_description': '# Unicycle\n\nA predictable, unidirectional state store for Python. Inspired by [Redux][].\n\nUnicycle allows you to:\n\n- Keep the state of a component or application in one or more immutable objects\n- Trigger state changes with action messages\n- Subscribe to state transitions\n\n[redux]: https://redux.js.org/\n\n## Install\n\n```shell\npip install unicycle\n```\n\n## Simple usage\n\nLet\'s build a Pokédex with Unicycle!\n\n### Step 1 - Define your state\n\nStart by defining the application\'s state. Our Pokédex is going to keep track of which Pokémon we\'ve seen, and which ones we\'ve caught.\n\nThe state can be anything! It can be as simple as a single boolean, but usually you\'ll need more complexity than that. The only rule is that **state is immutable**. You are not allowed to modify state directly nor mutate. For this reason, we recommend you use a `NamedTuple` or a frozen `dataclass` as your state type.\n\nYou _can_ define your state using mutable objects as long as you never actually mutate them. Using immutable type hints like `Mapping` and `Sequence` can help, if you want to go this route.\n\n```python\nfrom typing import NamedTuple, Set\n\nclass PokedexState(NamedTuple):\n    """Pokedex state.\n\n    Properties:\n        seen: Names of all seen Pokemon.\n        caught: Names of all caught Pokemon.\n    """\n    seen: frozenset[str] = frozenset()\n    caught: frozenset[str] = frozenset()\n```\n\n### Step 2 - Define your actions\n\nNext, we define our **actions**. An action is an event that may\ncause the state to change. The state of our Pokédex will change whenever\nwe see or catch a new Pokémon.\n\nAn action object can be anything! Like state, though, actions are **read-only**. If you need to attach payloads to action, we recommend you use a `NamedTuple` or a frozen `dataclass` as your action type.\n\nIf you use type-hints, you must also create a `Union` of actions the store may receive.\n\n```python\nfrom typing import NamedTuple, Union\n\nclass PokemonSeen(NamedTuple):\n    """A Pokemon named `name` has been spotted."""\n    name: str\n\nclass PokemonCaught(NamedTuple):\n    """A Pokemon named `name` has been caught."""\n    name: str\n\nPokedexAction = Union[PokemonSeen, PokemonCaught]\n```\n\n### Step 3 - Define how your state changes\n\nWith our state shape and actions defined, we need to define:\n\n- Our initial state.\n- How the state changes in response to those actions.\n\nFor this, we\'ll create a subclass of `Store`. We can set the initial state of the store using the `state` attribute, and we can write reducers to [fold][] the action into the previous state to calculate a new state.\n\nUse the `@reducer` decorator to mark a given method as handling a certain action type. We have two state changes to worry about:\n\n- If we see a Pokémon, we need to ensure it is in the seen list\n- If we catch a Pokémon, we need to ensure it is in both the seen and caught\n\n```python\nfrom unicycle import Store, reducer\n\nclass PokedexStore(Store[PokedexState, PokedexAction]):\n    state: PokedexState = PokedexState()\n\n    @reducer(PokemonSeen)\n    def pokemon_seen(self, action: PokemonSeen) -> PokedexState:\n        prev_state = self.state\n        return PokedexState(\n            seen=prev_state.seen.union([action.name]),\n            caught=prev_state.caught,\n        )\n\n    @reducer(PokemonCaught)\n    def pokemon_caught(self,  action: PokemonCaught) -> PokedexState:\n        prev_state = self.state\n        return PokedexState(\n            seen=prev_state.seen.union([action.name]),\n            caught=prev_state.caught.union([action.name]),\n        )\n```\n\n[reduce]: https://en.wikipedia.org/wiki/Fold_(higher-order_function)\n\n### Step 4 - Add it to your app\n\nLet\'s wire this state up to a simple HTTP API that:\n\n- Can add a Pokémon to the seen list\n- Can add a Pokémon to the caught list\n- Pushes out WebSocket notifications whenever our Pokédex state changes!\n\nTo trigger state changes, use `Store.dispatch` to send actions into the store. From there, you can retrieve the state from `Store.state`. Additionally, you can use `Store.subscribe` to receive to state changes notifications asynchronously.\n\n```python\nfrom quart import Quart, request, websocket\n\napp = Quart("Pokedex")\nstore = PokedexStore()\n\n@app.route("/seen", methods=["PUT"])\nasync def put_seen() -> None:\n    name = request.data.decode()\n    state = store.dispatch(PokemonSeen(name=name)))\n    return state.seen\n\n@app.route("/caught", methods=["PUT"])\nasync def put_caught() -> None:\n    name = request.data.decode()\n    state = store.dispatch(PokemonCaught(name=name)))\n    return state.caught\n\n@app.route("/", methods=["GET"])\nasync def get_pokedex() -> Dict[str, OrderedSet[str]]:\n    state = store.state\n    return {\n        "seen": state.seen,\n        "caught": state.caught,\n    }\n\n@app.websocket(\'/notifications\')\nasync def notifications() -> None:\n    with store.subscribe() as notifications:\n        async for state, action in notifications:\n            await websocket.send(\n                {\n                    "seen": state.seen,\n                    "caught": state.caught,\n                }\n            )\n\napp.run()\n```\n\n## Complicated usage\n\n### Combined stores\n\nFor more complicated states, you can combine several stores into one. This is a powerful feature that allows you to separate your state into different domains while still receiving all the same actions.\n\nA combined store is just another `Store`, so you can nest combined stores in other combined stores to create whatever state tree you need. For our Pokédex, we could split our single store into two: a `SeenStore` for tracking seen Pokémon and a `CaughtStore` for tracking caught Pokémon.\n\nUsing our same actions...\n\n```python\nfrom typing import NamedTuple, Union\nfrom unicycle import Store, combine_stores, reducer\n\nclass PokemonSeen(NamedTuple):\n    """A Pokemon named `name` has been spotted."""\n    name: str\n\n\nclass PokemonCaught(NamedTuple):\n    """A Pokemon named `name` has been caught."""\n    name: str\n\nPokedexAction = Union[PokemonSeen, PokemonCaught]\n```\n\nWe can create a `SeenState` and `SeenStore`...\n\n```python\nclass SeenState(NamedTuple):\n    """Seen Pokemon state.\n\n    Properties:\n        names: Names of all seen Pokemon names.\n    """\n    names: frozenset[str] = frozenset()\n\n\nclass SeenStore(Store[SeenState, PokedexAction]):\n    state: SeenState = SeenState()\n\n    @reducer(PokemonSeen, PokemonCaught)\n    def pokemon_seen(self, action: Union[PokemonSeen, PokemonCaught]) -> SeenState:\n        names = self.state.names\n        return SeenState(names=names.union([action.name]))\n```\n\nAs well as a `CaughtState` and `CaughtStore`...\n\n```python\nclass CaughtState(NamedTuple):\n    """Caught Pokemon state.\n\n    Properties:\n        names: Names of all caught Pokemon.\n    """\n    names: frozenset[str] = frozenset()\n\n\nclass CaughtStore(Store[CaughtState, PokedexAction]):\n    state: CaughtState = CaughtState()\n\n    @reducer(PokemonCaught)\n    def pokemon_caught(self, action: PokemonCaught) -> CaughtState:\n        names = self.state.names\n        return CaughtState(names=names.union([action.name]))\n```\n\nTo create our combined store, we must create a combined state object, built up of the sub-states, as well as a combined store, using `@combined_store`.\n\n```python\nclass PokedexState(NamedTuple):\n    seen: SeenState\n    caught: CaughtState\n\n@combined_store(PokedexState, seen=SeenStore, caught=CaughtStore)\nclass PokedexStore(Store(PokedexState, PokedexAction)):\n    pass\n```\n\nFrom here, we can `dispatch` actions into the combined `PokedexStore`, and the actions will be sent into the `SeenStore` and `CaughtStore` substores, giving us a new combined state!\n\n```python\n>>> pokedex_store = PokedexStore()\n>>> pokedex_store.state\nPokedexState(seen=SeenState(names=frozenset()), caught=CaughtState(names=frozenset()))\n>>>\n>>> pokedex_store.dispatch(PokemonSeen("Squirtle"))\nPokedexState(seen=SeenState(names=frozenset({\'Squirtle\'})), caught=CaughtState(names=frozenset()))\n>>>\n>>> pokedex_store.dispatch(PokemonSeen("Charmander"))\nPokedexState(seen=SeenState(names=frozenset({\'Charmander\', \'Squirtle\'})), caught=CaughtState(names=frozenset({\'Charmander\'})))\n```\n\n### Computed states and caching\n\nWhen using a state store like Unicycle, it\'s usually a good idea to store the most fundamental state possible, and compute derived data on the fly.\n\nThere\'s more than one way to compute derived state from your store. One easy option is to use functions:\n\n```python\ndef get_all_known_pokemon(state: PokedexState) -> FrozenSet[str]:\n    return state.seen.names | state.caught.names\n```\n\nIf you\'re using a `NamedTuple` or `dataclass` for your states, another option is to add methods to your state objects to create these "selectors".\n\n```python\nclass SeenState(NamedTuple):\n    names: frozenset[str] = frozenset()\n\n    @property\n    def bulbasaur_is_seen(self) -> bool:\n        return "Bulbasaur" in self.names\n```\n\nSince the state objects are immutable, if you have expensive computed state, you can use tools like `functools.cache` to cache these computed states. **Caching like this can cause unintended performance issues and memory leaks** (depending on the arguments a method may receive), so be careful, and only add caching after you\'ve confirmed it\'ll actually improve performance.\n\n```python\nclass SeenState(NamedTuple):\n    names: frozenset[str] = frozenset()\n\n    @property\n    @functools.cache\n    def shasum(self) -> str:\n        # expensive calculation\n        ...\n```\n\nNeed to compute state from several substates? No problem! Add methods to a common ancestor of substate.\n\n```python\nclass PokedexState(NamedTuple):\n    seen: SeenState\n    caught: CaughtState\n\n    def pokemon_is_known(self, name: str) -> bool:\n        return name in self.seen.names or name in self.caught.names\n```\n',
    'author': 'Mike Cousins',
    'author_email': 'mike@cousins.io',
    'maintainer': None,
    'maintainer_email': None,
    'url': 'https://github.com/mcous/unicycle',
    'packages': packages,
    'package_data': package_data,
    'install_requires': install_requires,
    'python_requires': '>=3.7,<4.0',
}


setup(**setup_kwargs)
