statepatch 0.2.0__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.
@@ -25,3 +25,6 @@ apps/docs/.docs
25
25
  /.deepsec/
26
26
  .vercel
27
27
  .env*
28
+
29
+ MAINTAIN_EPOCH.md
30
+ DOCS_EPOCH.md
@@ -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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "statepatch"
3
- version = "0.2.0"
3
+ version = "0.2.2"
4
4
  description = "Immer-style ops over a plain JSON tree: observables that emit every mutation as an op"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -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):
@@ -34,15 +36,21 @@ def _snapshot(node: Any) -> Any:
34
36
 
35
37
 
36
38
  def plain(value: Any) -> Any:
37
- """Resolve a proxy or observable to a plain snapshot; pass anything else through."""
39
+ """Resolve a proxy or observable to its live stored node (mutating it emits no ops); pass anything else through."""
38
40
  from ._core import Observable, Proxy
39
41
 
40
- if isinstance(value, Proxy):
41
- _owner, _rel, node = value._target()
42
- return _snapshot(node)
43
42
  if isinstance(value, Observable):
44
- return _snapshot(value._state)
45
- return value
43
+ value = Proxy(value, ())
44
+ if not isinstance(value, Proxy):
45
+ return value
46
+ owner, rel, node = value._target()
47
+ mounted = any(path[: len(rel)] == rel for _child, path, _unsub in owner._mounts.values())
48
+ return _snapshot(node) if mounted else node
49
+
50
+
51
+ def snapshot(value: Any) -> Any:
52
+ """Copy a value and its mounted observables into an independent plain tree."""
53
+ return _snapshot(value)
46
54
 
47
55
 
48
56
  def _adopt(value: Any, base: Path) -> tuple[Any, list[tuple[Path, "Observable"]]]:
@@ -4,7 +4,7 @@ mounted observables forward their op streams at the mount path."""
4
4
  from datetime import datetime, timezone
5
5
 
6
6
  import pytest
7
- from statepatch import Op, append, observable
7
+ from statepatch import Observable, Op, append, observable, plain
8
8
 
9
9
 
10
10
  def collect(obs):
@@ -180,6 +180,26 @@ def test_snapshot_ops_carry_plain_values_for_nested_mounts():
180
180
  assert ops[-1] == Op("replace", ("wrap", "inner", "n"), 7)
181
181
 
182
182
 
183
+ def test_plain_returns_the_live_node():
184
+ state = observable({"runs": [{"id": "r1"}]})
185
+ ops = collect(state)
186
+ assert plain(state) is state._state
187
+ assert plain(state["runs"]) is state._state["runs"]
188
+ assert plain(state["runs"][0]["id"]) == "r1"
189
+ plain(state["runs"]).append({"id": "r2"})
190
+ assert state.value == {"runs": [{"id": "r1"}, {"id": "r2"}]}
191
+ assert ops == []
192
+
193
+
194
+ def test_plain_unwraps_mounts_into_a_snapshot():
195
+ child = observable({"n": 0})
196
+ parent = observable({"wrap": {"inner": child}, "other": [1]})
197
+ assert plain(parent["wrap"]) == {"inner": {"n": 0}}
198
+ assert not isinstance(plain(parent["wrap"])["inner"], Observable)
199
+ assert plain(parent["wrap"]["inner"]) is child._state
200
+ assert plain(parent["other"]) is parent._state["other"]
201
+
202
+
183
203
  def test_container_helpers():
184
204
  state = observable({"d": {"a": 1}, "l": [1, 2, 3]})
185
205
  ops = collect(state)
@@ -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.0/PKG-INFO DELETED
@@ -1,26 +0,0 @@
1
- Metadata-Version: 2.5
2
- Name: statepatch
3
- Version: 0.2.0
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
- ```
@@ -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
- ```