statepatch 0.2.1__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {statepatch-0.2.1 → statepatch-0.2.2}/.gitignore +3 -0
- statepatch-0.2.2/PKG-INFO +38 -0
- statepatch-0.2.2/README.md +29 -0
- {statepatch-0.2.1 → statepatch-0.2.2}/pyproject.toml +1 -1
- {statepatch-0.2.1 → statepatch-0.2.2}/src/statepatch/__init__.py +2 -1
- {statepatch-0.2.1 → statepatch-0.2.2}/src/statepatch/_core.py +1 -1
- {statepatch-0.2.1 → statepatch-0.2.2}/src/statepatch/_wire.py +8 -1
- statepatch-0.2.2/tests/test_snapshot.py +19 -0
- statepatch-0.2.1/PKG-INFO +0 -26
- statepatch-0.2.1/README.md +0 -17
- {statepatch-0.2.1 → statepatch-0.2.2}/tests/test_observable.py +0 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: statepatch
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: Immer-style ops over a plain JSON tree: observables that emit every mutation as an op
|
|
5
|
+
Project-URL: Repository, https://github.com/assistant-ui/harness-sdk
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# statepatch
|
|
11
|
+
|
|
12
|
+
Immer-style ops over a plain JSON tree: `observable(initial)` wraps one value, `.value` reads it, and every mutation emits the matching `replace` / `add` / `remove` op to subscribers. Observables compose: one stored inside another mounts its op stream at that path. Part of [harness-sdk](https://github.com/assistant-ui/harness-sdk), under `statewire` and `pinned`.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
uv add statepatch
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Use
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from statepatch import observable
|
|
24
|
+
|
|
25
|
+
state = observable({"runs": []})
|
|
26
|
+
state.subscribe(print)
|
|
27
|
+
|
|
28
|
+
run = observable({"runId": "r1", "queue": []})
|
|
29
|
+
state["runs"].append(run) # add op at ["runs", 0]
|
|
30
|
+
run["queue"].append({"id": "q1"}) # forwarded as add at ["runs", 0, "queue", 0]
|
|
31
|
+
state["runs"].pop(0) # remove op; run is detached and reusable
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Docs
|
|
35
|
+
|
|
36
|
+
- Reference: https://github.com/assistant-ui/harness-sdk/blob/main/apps/docs/app/docs/reference/packages/page.mdx
|
|
37
|
+
- Guide: https://github.com/assistant-ui/harness-sdk/blob/main/apps/docs/app/docs/guides/statewire/host/page.mdx
|
|
38
|
+
- Spec: https://github.com/assistant-ui/harness-sdk/blob/main/apps/docs/app/docs/spec/statewire-protocol/statepatch/page.mdx
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# statepatch
|
|
2
|
+
|
|
3
|
+
Immer-style ops over a plain JSON tree: `observable(initial)` wraps one value, `.value` reads it, and every mutation emits the matching `replace` / `add` / `remove` op to subscribers. Observables compose: one stored inside another mounts its op stream at that path. Part of [harness-sdk](https://github.com/assistant-ui/harness-sdk), under `statewire` and `pinned`.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
uv add statepatch
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Use
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from statepatch import observable
|
|
15
|
+
|
|
16
|
+
state = observable({"runs": []})
|
|
17
|
+
state.subscribe(print)
|
|
18
|
+
|
|
19
|
+
run = observable({"runId": "r1", "queue": []})
|
|
20
|
+
state["runs"].append(run) # add op at ["runs", 0]
|
|
21
|
+
run["queue"].append({"id": "q1"}) # forwarded as add at ["runs", 0, "queue", 0]
|
|
22
|
+
state["runs"].pop(0) # remove op; run is detached and reusable
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Docs
|
|
26
|
+
|
|
27
|
+
- Reference: https://github.com/assistant-ui/harness-sdk/blob/main/apps/docs/app/docs/reference/packages/page.mdx
|
|
28
|
+
- Guide: https://github.com/assistant-ui/harness-sdk/blob/main/apps/docs/app/docs/guides/statewire/host/page.mdx
|
|
29
|
+
- Spec: https://github.com/assistant-ui/harness-sdk/blob/main/apps/docs/app/docs/spec/statewire-protocol/statepatch/page.mdx
|
|
@@ -7,13 +7,14 @@ stream at that path; removing it detaches it, live and reusable.
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
from ._core import Observable, Proxy, append, observable
|
|
10
|
-
from ._wire import Listener, Op, Path, Segment, plain
|
|
10
|
+
from ._wire import Listener, Op, Path, Segment, plain, snapshot
|
|
11
11
|
|
|
12
12
|
__all__ = [
|
|
13
13
|
"observable",
|
|
14
14
|
"Observable",
|
|
15
15
|
"Proxy",
|
|
16
16
|
"plain",
|
|
17
|
+
"snapshot",
|
|
17
18
|
"append",
|
|
18
19
|
"Op",
|
|
19
20
|
"Path",
|
|
@@ -4,7 +4,7 @@ from ._wire import _MISSING, Listener, Op, Path, Segment, _adopt, _index, _snaps
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class Observable:
|
|
7
|
-
__slots__ = ("_state", "_listeners", "_mounts", "_parent")
|
|
7
|
+
__slots__ = ("_state", "_listeners", "_mounts", "_parent", "__weakref__")
|
|
8
8
|
|
|
9
9
|
def __init__(self, initial: Any = None) -> None:
|
|
10
10
|
if isinstance(initial, Observable):
|
|
@@ -22,8 +22,10 @@ _MISSING: Any = object()
|
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
def _snapshot(node: Any) -> Any:
|
|
25
|
-
from ._core import Observable
|
|
25
|
+
from ._core import Observable, Proxy
|
|
26
26
|
|
|
27
|
+
if isinstance(node, Proxy):
|
|
28
|
+
_, _, node = node._target()
|
|
27
29
|
if isinstance(node, Observable):
|
|
28
30
|
return _snapshot(node._state)
|
|
29
31
|
if isinstance(node, dict):
|
|
@@ -46,6 +48,11 @@ def plain(value: Any) -> Any:
|
|
|
46
48
|
return _snapshot(node) if mounted else node
|
|
47
49
|
|
|
48
50
|
|
|
51
|
+
def snapshot(value: Any) -> Any:
|
|
52
|
+
"""Copy a value and its mounted observables into an independent plain tree."""
|
|
53
|
+
return _snapshot(value)
|
|
54
|
+
|
|
55
|
+
|
|
49
56
|
def _adopt(value: Any, base: Path) -> tuple[Any, list[tuple[Path, "Observable"]]]:
|
|
50
57
|
"""Validated deep copy keeping ``Observable`` nodes in place; returns the
|
|
51
58
|
stored tree and the mount points found inside it (paths rooted at ``base``)."""
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from statepatch import observable, plain, snapshot
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_snapshot_copies_only_the_selected_subtree_and_unwraps_mounts():
|
|
5
|
+
child = observable({"values": [1]})
|
|
6
|
+
state = observable({"selected": {"child": child}, "other": {"large": [2]}})
|
|
7
|
+
copied = snapshot(state["selected"])
|
|
8
|
+
assert copied == {"child": {"values": [1]}}
|
|
9
|
+
copied["child"]["values"].append(3)
|
|
10
|
+
assert plain(child["values"]) == [1]
|
|
11
|
+
child["values"].append(4)
|
|
12
|
+
assert copied["child"]["values"] == [1, 3]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_snapshot_accepts_proxies_inside_plain_containers():
|
|
16
|
+
state = observable({"items": [{"id": "m1"}]})
|
|
17
|
+
copied = snapshot({"wrapped": [state["items"][0]]})
|
|
18
|
+
state["items"][0]["id"] = "m2"
|
|
19
|
+
assert copied == {"wrapped": [{"id": "m1"}]}
|
statepatch-0.2.1/PKG-INFO
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.5
|
|
2
|
-
Name: statepatch
|
|
3
|
-
Version: 0.2.1
|
|
4
|
-
Summary: Immer-style ops over a plain JSON tree: observables that emit every mutation as an op
|
|
5
|
-
Project-URL: Repository, https://github.com/assistant-ui/harness-sdk
|
|
6
|
-
License-Expression: MIT
|
|
7
|
-
Requires-Python: >=3.11
|
|
8
|
-
Description-Content-Type: text/markdown
|
|
9
|
-
|
|
10
|
-
# statepatch
|
|
11
|
-
|
|
12
|
-
Immer-style ops over a plain JSON tree.
|
|
13
|
-
|
|
14
|
-
`observable(initial)` wraps one value: `.value` reads it, every mutation applies to the tree and emits the matching op (`replace` / `add` / `remove`) to subscribers. Observables compose: an observable stored inside another mounts its op stream at that path.
|
|
15
|
-
|
|
16
|
-
```python
|
|
17
|
-
from statepatch import observable
|
|
18
|
-
|
|
19
|
-
state = observable({"runs": []})
|
|
20
|
-
state.subscribe(print)
|
|
21
|
-
|
|
22
|
-
run = observable({"runId": "r1", "queue": []})
|
|
23
|
-
state["runs"].append(run) # add op at ["runs", 0]
|
|
24
|
-
run["queue"].append({"id": "q1"}) # forwarded as add at ["runs", 0, "queue", 0]
|
|
25
|
-
state["runs"].pop(0) # remove op; run is detached and reusable
|
|
26
|
-
```
|
statepatch-0.2.1/README.md
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
# statepatch
|
|
2
|
-
|
|
3
|
-
Immer-style ops over a plain JSON tree.
|
|
4
|
-
|
|
5
|
-
`observable(initial)` wraps one value: `.value` reads it, every mutation applies to the tree and emits the matching op (`replace` / `add` / `remove`) to subscribers. Observables compose: an observable stored inside another mounts its op stream at that path.
|
|
6
|
-
|
|
7
|
-
```python
|
|
8
|
-
from statepatch import observable
|
|
9
|
-
|
|
10
|
-
state = observable({"runs": []})
|
|
11
|
-
state.subscribe(print)
|
|
12
|
-
|
|
13
|
-
run = observable({"runId": "r1", "queue": []})
|
|
14
|
-
state["runs"].append(run) # add op at ["runs", 0]
|
|
15
|
-
run["queue"].append({"id": "q1"}) # forwarded as add at ["runs", 0, "queue", 0]
|
|
16
|
-
state["runs"].pop(0) # remove op; run is detached and reusable
|
|
17
|
-
```
|
|
File without changes
|