tiptap-python-utils 0.2.0__tar.gz → 0.3.0__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.
Files changed (57) hide show
  1. {tiptap_python_utils-0.2.0/src/tiptap_python_utils.egg-info → tiptap_python_utils-0.3.0}/PKG-INFO +22 -14
  2. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/README.md +21 -13
  3. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/pyproject.toml +1 -1
  4. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/src/tiptap_python_utils/__init__.py +3 -15
  5. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/src/tiptap_python_utils/content.py +31 -0
  6. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/src/tiptap_python_utils/model/base.py +3 -0
  7. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/src/tiptap_python_utils/model/nodes.py +0 -4
  8. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/src/tiptap_python_utils/select/selection.py +4 -1
  9. tiptap_python_utils-0.3.0/src/tiptap_python_utils/shared/__init__.py +11 -0
  10. tiptap_python_utils-0.3.0/src/tiptap_python_utils/shared/families.py +64 -0
  11. tiptap_python_utils-0.3.0/src/tiptap_python_utils/shared/fingerprint.py +20 -0
  12. tiptap_python_utils-0.3.0/src/tiptap_python_utils/shared/identity.py +9 -0
  13. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0/src/tiptap_python_utils.egg-info}/PKG-INFO +22 -14
  14. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/src/tiptap_python_utils.egg-info/SOURCES.txt +0 -1
  15. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/tests/test_compat_imports.py +2 -7
  16. tiptap_python_utils-0.3.0/tests/test_mutations.py +405 -0
  17. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/tests/test_public_api.py +2 -6
  18. tiptap_python_utils-0.2.0/src/tiptap_python_utils/shared/__init__.py +0 -17
  19. tiptap_python_utils-0.2.0/src/tiptap_python_utils/shared/families.py +0 -47
  20. tiptap_python_utils-0.2.0/src/tiptap_python_utils/shared/fingerprint.py +0 -21
  21. tiptap_python_utils-0.2.0/src/tiptap_python_utils/shared/identity.py +0 -48
  22. tiptap_python_utils-0.2.0/src/tiptap_python_utils/shared/sync.py +0 -63
  23. tiptap_python_utils-0.2.0/tests/test_mutations.py +0 -331
  24. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/LICENSE +0 -0
  25. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/MANIFEST.in +0 -0
  26. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/setup.cfg +0 -0
  27. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/src/tiptap_python_utils/codec/__init__.py +0 -0
  28. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/src/tiptap_python_utils/codec/raw.py +0 -0
  29. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/src/tiptap_python_utils/codec/reader.py +0 -0
  30. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/src/tiptap_python_utils/codec/writer.py +0 -0
  31. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/src/tiptap_python_utils/contract/__init__.py +0 -0
  32. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/src/tiptap_python_utils/contract/key.py +0 -0
  33. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/src/tiptap_python_utils/contract/kind.py +0 -0
  34. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/src/tiptap_python_utils/contract/policy.py +0 -0
  35. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/src/tiptap_python_utils/exceptions.py +0 -0
  36. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/src/tiptap_python_utils/model/__init__.py +0 -0
  37. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/src/tiptap_python_utils/model/payload.py +0 -0
  38. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/src/tiptap_python_utils/model/registry.py +0 -0
  39. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/src/tiptap_python_utils/py.typed +0 -0
  40. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/src/tiptap_python_utils/select/__init__.py +0 -0
  41. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/src/tiptap_python_utils/tasks/__init__.py +0 -0
  42. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/src/tiptap_python_utils/tasks/query.py +0 -0
  43. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/src/tiptap_python_utils/text/__init__.py +0 -0
  44. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/src/tiptap_python_utils/text/extract.py +0 -0
  45. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/src/tiptap_python_utils/tree/__init__.py +0 -0
  46. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/src/tiptap_python_utils/tree/path.py +0 -0
  47. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/src/tiptap_python_utils/types.py +0 -0
  48. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/src/tiptap_python_utils/walk/__init__.py +0 -0
  49. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/src/tiptap_python_utils/walk/traversal.py +0 -0
  50. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/src/tiptap_python_utils.egg-info/dependency_links.txt +0 -0
  51. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/src/tiptap_python_utils.egg-info/requires.txt +0 -0
  52. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/src/tiptap_python_utils.egg-info/top_level.txt +0 -0
  53. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/tests/test_codec_raw.py +0 -0
  54. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/tests/test_content.py +0 -0
  55. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/tests/test_extract.py +0 -0
  56. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/tests/test_filter.py +0 -0
  57. {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.3.0}/tests/test_traverser.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tiptap_python_utils
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Python utilities for parsing, traversing, editing, and serializing TipTap JSON content.
5
5
  Author: tiptap_python_utils contributors
6
6
  License: MIT License
@@ -219,36 +219,44 @@ task.shared_id # sharedId attr, if any
219
219
 
220
220
  ## Shared-Node Synchronization
221
221
 
222
- `shared_families` collects canonical bodies grouped by `sharedId`;
223
- `sync_shared` rewrites every matching node in a document using those canonical
224
- bodies while preserving per-instance identity (`id`, `sharedId`).
222
+ `Content.shared_families()` collects canonical bodies grouped by `sharedId` into
223
+ a `SharedFamilies` value object. `Content.sync_shared(families)` rewrites every
224
+ matching node in the document from those canonical bodies, preserving
225
+ per-instance identity (`id`, `sharedId`). Both return immutable values — the
226
+ original `Content` is never mutated.
225
227
 
226
228
  ```python
227
- from tiptap_python_utils import shared_families, sync_shared
229
+ from tiptap_python_utils import Content
228
230
 
229
231
  # Canonical doc: the source of truth for every shared body.
230
- canonical = {"type": "doc", "content": [
232
+ canonical = Content.require({"type": "doc", "content": [
231
233
  {
232
234
  "type": "paragraph",
233
235
  "attrs": {"id": "p1", "sharedId": "intro"},
234
236
  "content": [{"type": "text", "text": "Authoritative intro"}],
235
237
  }
236
- ]}
238
+ ]})
237
239
 
238
240
  # Doc that mirrors the same sharedId but with a stale body.
239
- target = {"type": "doc", "content": [
241
+ target = Content.require({"type": "doc", "content": [
240
242
  {
241
243
  "type": "paragraph",
242
244
  "attrs": {"id": "p1-copy", "sharedId": "intro"},
243
245
  "content": [{"type": "text", "text": "Stale copy"}],
244
246
  }
245
- ]}
247
+ ]})
246
248
 
247
- families = shared_families(canonical)
248
- synced_json, changed = sync_shared(target, families)
249
- assert changed is True
249
+ synced = target.sync_shared(canonical.shared_families())
250
+ assert synced.has_shared("intro")
250
251
  ```
251
252
 
253
+ Related helpers on `Content`:
254
+
255
+ - `content.where_shared_id(sid)` — `Selection` over every node with that sharedId.
256
+ - `content.has_shared(sid)` — quick presence check.
257
+ - `node.with_shared_id(sid)` — stamp a sharedId onto a node (returns a new node).
258
+ - `new_shared_id()` — mint a fresh `shared-…` identifier.
259
+
252
260
  ## Architecture (one paragraph)
253
261
 
254
262
  The package is layered: `contract` (key/kind/policy primitives) → `model`
@@ -268,13 +276,13 @@ Common imports are available from the package root:
268
276
  from tiptap_python_utils import (
269
277
  Content,
270
278
  Paragraph,
279
+ SharedFamilies,
271
280
  TaskItem,
272
281
  Text,
273
282
  has_open_tasks,
274
283
  kind,
284
+ new_shared_id,
275
285
  open_tasks,
276
- shared_families,
277
- sync_shared,
278
286
  text_slices,
279
287
  visible_text,
280
288
  word_count,
@@ -168,36 +168,44 @@ task.shared_id # sharedId attr, if any
168
168
 
169
169
  ## Shared-Node Synchronization
170
170
 
171
- `shared_families` collects canonical bodies grouped by `sharedId`;
172
- `sync_shared` rewrites every matching node in a document using those canonical
173
- bodies while preserving per-instance identity (`id`, `sharedId`).
171
+ `Content.shared_families()` collects canonical bodies grouped by `sharedId` into
172
+ a `SharedFamilies` value object. `Content.sync_shared(families)` rewrites every
173
+ matching node in the document from those canonical bodies, preserving
174
+ per-instance identity (`id`, `sharedId`). Both return immutable values — the
175
+ original `Content` is never mutated.
174
176
 
175
177
  ```python
176
- from tiptap_python_utils import shared_families, sync_shared
178
+ from tiptap_python_utils import Content
177
179
 
178
180
  # Canonical doc: the source of truth for every shared body.
179
- canonical = {"type": "doc", "content": [
181
+ canonical = Content.require({"type": "doc", "content": [
180
182
  {
181
183
  "type": "paragraph",
182
184
  "attrs": {"id": "p1", "sharedId": "intro"},
183
185
  "content": [{"type": "text", "text": "Authoritative intro"}],
184
186
  }
185
- ]}
187
+ ]})
186
188
 
187
189
  # Doc that mirrors the same sharedId but with a stale body.
188
- target = {"type": "doc", "content": [
190
+ target = Content.require({"type": "doc", "content": [
189
191
  {
190
192
  "type": "paragraph",
191
193
  "attrs": {"id": "p1-copy", "sharedId": "intro"},
192
194
  "content": [{"type": "text", "text": "Stale copy"}],
193
195
  }
194
- ]}
196
+ ]})
195
197
 
196
- families = shared_families(canonical)
197
- synced_json, changed = sync_shared(target, families)
198
- assert changed is True
198
+ synced = target.sync_shared(canonical.shared_families())
199
+ assert synced.has_shared("intro")
199
200
  ```
200
201
 
202
+ Related helpers on `Content`:
203
+
204
+ - `content.where_shared_id(sid)` — `Selection` over every node with that sharedId.
205
+ - `content.has_shared(sid)` — quick presence check.
206
+ - `node.with_shared_id(sid)` — stamp a sharedId onto a node (returns a new node).
207
+ - `new_shared_id()` — mint a fresh `shared-…` identifier.
208
+
201
209
  ## Architecture (one paragraph)
202
210
 
203
211
  The package is layered: `contract` (key/kind/policy primitives) → `model`
@@ -217,13 +225,13 @@ Common imports are available from the package root:
217
225
  from tiptap_python_utils import (
218
226
  Content,
219
227
  Paragraph,
228
+ SharedFamilies,
220
229
  TaskItem,
221
230
  Text,
222
231
  has_open_tasks,
223
232
  kind,
233
+ new_shared_id,
224
234
  open_tasks,
225
- shared_families,
226
- sync_shared,
227
235
  text_slices,
228
236
  visible_text,
229
237
  word_count,
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tiptap_python_utils"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Python utilities for parsing, traversing, editing, and serializing TipTap JSON content."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -22,15 +22,7 @@ from .model import (
22
22
  )
23
23
  from .contract.policy import content_id, is_parseable, node_id, tiptap_id
24
24
  from .select import Selection
25
- from .shared import (
26
- fingerprint_shared,
27
- has_shared,
28
- new_shared_id,
29
- shared_id,
30
- shared_families,
31
- stamp_shared,
32
- sync_shared,
33
- )
25
+ from .shared import SharedFamilies, fingerprint, new_shared_id
34
26
  from .tasks import has_open_tasks, open_tasks, syncable_tasks
35
27
  from .text import NodeText, text_slices, visible_text, word_count
36
28
  from .walk import Ref, Walker
@@ -52,6 +44,7 @@ __all__ = [
52
44
  "Paragraph",
53
45
  "Ref",
54
46
  "Selection",
47
+ "SharedFamilies",
55
48
  "TableCell",
56
49
  "TaskItem",
57
50
  "TaskList",
@@ -60,9 +53,8 @@ __all__ = [
60
53
  "Unknown",
61
54
  "Walker",
62
55
  "content_id",
63
- "fingerprint_shared",
56
+ "fingerprint",
64
57
  "has_open_tasks",
65
- "has_shared",
66
58
  "is_parseable",
67
59
  "key",
68
60
  "kind",
@@ -70,10 +62,6 @@ __all__ = [
70
62
  "node_id",
71
63
  "open_tasks",
72
64
  "registry",
73
- "shared_families",
74
- "shared_id",
75
- "stamp_shared",
76
- "sync_shared",
77
65
  "syncable_tasks",
78
66
  "text_slices",
79
67
  "tiptap_id",
@@ -28,6 +28,7 @@ from .model import (
28
28
  Text,
29
29
  )
30
30
  from .select import Selection
31
+ from .shared import SharedFamilies
31
32
  from .walk import Ref, Walker
32
33
 
33
34
 
@@ -117,6 +118,36 @@ class Content:
117
118
  tuple(ref for ref in self.refs() if ref.node.kind == node_kind),
118
119
  )
119
120
 
121
+ def where_shared_id(self, shared_id: str) -> Selection:
122
+ return Selection(
123
+ self,
124
+ tuple(
125
+ ref
126
+ for ref in self.refs(parseable=True)
127
+ if ref.node.shared_id == shared_id
128
+ ),
129
+ )
130
+
131
+ def has_shared(self, shared_id: str) -> bool:
132
+ return any(
133
+ ref.node.shared_id == shared_id for ref in self.refs(parseable=True)
134
+ )
135
+
136
+ def shared_families(self) -> SharedFamilies:
137
+ return SharedFamilies.from_content(self)
138
+
139
+ def sync_shared(self, families: SharedFamilies) -> "Content":
140
+ if self._root is None:
141
+ return self
142
+ refs = tuple(
143
+ ref
144
+ for ref in self.refs(parseable=True)
145
+ if ref.node.shared_id and ref.node.shared_id in families
146
+ )
147
+ if not refs:
148
+ return self
149
+ return Selection(self, refs).transform(families.merge)
150
+
120
151
  def append_root(self, node_or_raw: Any) -> "Content":
121
152
  return self.of(kind.DOC).append(node_or_raw)
122
153
 
@@ -79,6 +79,9 @@ class Node:
79
79
  attrs[name] = deepcopy(value)
80
80
  return replace(self, attrs=attrs, present=self.present | {key.ATTRS})
81
81
 
82
+ def with_shared_id(self, value: str) -> "Node":
83
+ return self.with_attr(key.SHARED_ID, value)
84
+
82
85
  def with_content(self, content: ContentTuple) -> "Node":
83
86
  return replace(self, content=content, present=self.present | {key.CONTENT})
84
87
 
@@ -66,10 +66,6 @@ class TaskItem(Node):
66
66
  def is_completed(self) -> bool:
67
67
  return bool(task_completion(self.attrs, self.extra))
68
68
 
69
- @property
70
- def shared_id(self) -> str | None:
71
- return policy.shared_id(self.attrs)
72
-
73
69
  def raw_attrs(self) -> Dict[str, Any]:
74
70
  attrs = super().raw_attrs()
75
71
  if key.CHECKED in attrs or (
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from copy import deepcopy
6
6
  from dataclasses import replace
7
- from typing import TYPE_CHECKING, Any, Iterator, Tuple
7
+ from typing import TYPE_CHECKING, Any, Callable, Iterator, Tuple
8
8
 
9
9
  from .. import codec
10
10
  from ..contract import key
@@ -69,6 +69,9 @@ class Selection:
69
69
  raise TiptapValidationError("Child node content must not be a document root")
70
70
  return self._apply(lambda node: node.append(child))
71
71
 
72
+ def transform(self, fn: Callable[[Node], Node]) -> "Content":
73
+ return self._apply(fn)
74
+
72
75
  def dump(self) -> str:
73
76
  return self._content.dump()
74
77
 
@@ -0,0 +1,11 @@
1
+ """Shared-node service exports."""
2
+
3
+ from .families import SharedFamilies
4
+ from .fingerprint import fingerprint
5
+ from .identity import new_shared_id
6
+
7
+ __all__ = [
8
+ "SharedFamilies",
9
+ "fingerprint",
10
+ "new_shared_id",
11
+ ]
@@ -0,0 +1,64 @@
1
+ """SharedFamilies: canonical bodies grouped by sharedId."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from types import MappingProxyType
7
+ from typing import TYPE_CHECKING, Iterator, Mapping
8
+
9
+ from .. import codec
10
+ from ..contract import key
11
+ from ..exceptions import TiptapValidationError
12
+ from ..model import Node
13
+ from .fingerprint import fingerprint
14
+
15
+ if TYPE_CHECKING:
16
+ from ..content import Content
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class SharedFamilies:
21
+ """Canonical node bodies indexed by sharedId."""
22
+
23
+ _bodies: Mapping[str, Node] = field(default_factory=lambda: MappingProxyType({}))
24
+
25
+ @classmethod
26
+ def from_content(cls, content: "Content") -> "SharedFamilies":
27
+ bodies: dict[str, Node] = {}
28
+ prints: dict[str, str] = {}
29
+ for ref in content.refs(parseable=True):
30
+ sid = ref.node.shared_id
31
+ if not sid:
32
+ continue
33
+ fp = fingerprint(ref.node)
34
+ if sid in prints and prints[sid] != fp:
35
+ raise TiptapValidationError(
36
+ f"Conflicting node bodies detected for sharedId '{sid}'"
37
+ )
38
+ bodies.setdefault(sid, ref.node)
39
+ prints.setdefault(sid, fp)
40
+ return cls(MappingProxyType(bodies))
41
+
42
+ def __contains__(self, shared_id: str) -> bool:
43
+ return shared_id in self._bodies
44
+
45
+ def __getitem__(self, shared_id: str) -> Node:
46
+ return self._bodies[shared_id]
47
+
48
+ def __iter__(self) -> Iterator[str]:
49
+ return iter(self._bodies)
50
+
51
+ def __len__(self) -> int:
52
+ return len(self._bodies)
53
+
54
+ def merge(self, target: Node) -> Node:
55
+ """Return target rewritten from the canonical body, preserving local id and sharedId."""
56
+ canonical = self[target.shared_id]
57
+ raw = canonical.raw()
58
+ attrs = dict(raw.get(key.ATTRS, {}))
59
+ if target.id:
60
+ attrs[key.ID] = target.id
61
+ if target.shared_id:
62
+ attrs[key.SHARED_ID] = target.shared_id
63
+ raw[key.ATTRS] = attrs
64
+ return codec.read_node(raw)
@@ -0,0 +1,20 @@
1
+ """Identity-stripped fingerprint of a node, used to detect divergent bodies."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+
7
+ from ..contract import key
8
+ from ..model import Node
9
+
10
+
11
+ def fingerprint(node: Node) -> str:
12
+ raw = node.raw()
13
+ attrs = dict(raw.get(key.ATTRS, {}))
14
+ attrs.pop(key.ID, None)
15
+ attrs.pop(key.SHARED_ID, None)
16
+ if attrs:
17
+ raw[key.ATTRS] = attrs
18
+ else:
19
+ raw.pop(key.ATTRS, None)
20
+ return json.dumps(raw, sort_keys=True)
@@ -0,0 +1,9 @@
1
+ """Shared-node identity primitives."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from uuid import uuid4
6
+
7
+
8
+ def new_shared_id() -> str:
9
+ return f"shared-{uuid4().hex}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tiptap_python_utils
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Python utilities for parsing, traversing, editing, and serializing TipTap JSON content.
5
5
  Author: tiptap_python_utils contributors
6
6
  License: MIT License
@@ -219,36 +219,44 @@ task.shared_id # sharedId attr, if any
219
219
 
220
220
  ## Shared-Node Synchronization
221
221
 
222
- `shared_families` collects canonical bodies grouped by `sharedId`;
223
- `sync_shared` rewrites every matching node in a document using those canonical
224
- bodies while preserving per-instance identity (`id`, `sharedId`).
222
+ `Content.shared_families()` collects canonical bodies grouped by `sharedId` into
223
+ a `SharedFamilies` value object. `Content.sync_shared(families)` rewrites every
224
+ matching node in the document from those canonical bodies, preserving
225
+ per-instance identity (`id`, `sharedId`). Both return immutable values — the
226
+ original `Content` is never mutated.
225
227
 
226
228
  ```python
227
- from tiptap_python_utils import shared_families, sync_shared
229
+ from tiptap_python_utils import Content
228
230
 
229
231
  # Canonical doc: the source of truth for every shared body.
230
- canonical = {"type": "doc", "content": [
232
+ canonical = Content.require({"type": "doc", "content": [
231
233
  {
232
234
  "type": "paragraph",
233
235
  "attrs": {"id": "p1", "sharedId": "intro"},
234
236
  "content": [{"type": "text", "text": "Authoritative intro"}],
235
237
  }
236
- ]}
238
+ ]})
237
239
 
238
240
  # Doc that mirrors the same sharedId but with a stale body.
239
- target = {"type": "doc", "content": [
241
+ target = Content.require({"type": "doc", "content": [
240
242
  {
241
243
  "type": "paragraph",
242
244
  "attrs": {"id": "p1-copy", "sharedId": "intro"},
243
245
  "content": [{"type": "text", "text": "Stale copy"}],
244
246
  }
245
- ]}
247
+ ]})
246
248
 
247
- families = shared_families(canonical)
248
- synced_json, changed = sync_shared(target, families)
249
- assert changed is True
249
+ synced = target.sync_shared(canonical.shared_families())
250
+ assert synced.has_shared("intro")
250
251
  ```
251
252
 
253
+ Related helpers on `Content`:
254
+
255
+ - `content.where_shared_id(sid)` — `Selection` over every node with that sharedId.
256
+ - `content.has_shared(sid)` — quick presence check.
257
+ - `node.with_shared_id(sid)` — stamp a sharedId onto a node (returns a new node).
258
+ - `new_shared_id()` — mint a fresh `shared-…` identifier.
259
+
252
260
  ## Architecture (one paragraph)
253
261
 
254
262
  The package is layered: `contract` (key/kind/policy primitives) → `model`
@@ -268,13 +276,13 @@ Common imports are available from the package root:
268
276
  from tiptap_python_utils import (
269
277
  Content,
270
278
  Paragraph,
279
+ SharedFamilies,
271
280
  TaskItem,
272
281
  Text,
273
282
  has_open_tasks,
274
283
  kind,
284
+ new_shared_id,
275
285
  open_tasks,
276
- shared_families,
277
- sync_shared,
278
286
  text_slices,
279
287
  visible_text,
280
288
  word_count,
@@ -31,7 +31,6 @@ src/tiptap_python_utils/shared/__init__.py
31
31
  src/tiptap_python_utils/shared/families.py
32
32
  src/tiptap_python_utils/shared/fingerprint.py
33
33
  src/tiptap_python_utils/shared/identity.py
34
- src/tiptap_python_utils/shared/sync.py
35
34
  src/tiptap_python_utils/tasks/__init__.py
36
35
  src/tiptap_python_utils/tasks/query.py
37
36
  src/tiptap_python_utils/text/__init__.py
@@ -60,14 +60,9 @@ SUBPACKAGE_PUBLIC_NAMES: dict[str, frozenset[str]] = {
60
60
  "tiptap_python_utils.select": frozenset({"Selection"}),
61
61
  "tiptap_python_utils.shared": frozenset(
62
62
  {
63
- "fingerprint_shared",
64
- "has_shared",
63
+ "SharedFamilies",
64
+ "fingerprint",
65
65
  "new_shared_id",
66
- "normalize_shared_id",
67
- "shared_families",
68
- "shared_id",
69
- "stamp_shared",
70
- "sync_shared",
71
66
  }
72
67
  ),
73
68
  "tiptap_python_utils.tasks": frozenset(