tiptap-python-utils 0.2.0__tar.gz → 0.4.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.
- {tiptap_python_utils-0.2.0/src/tiptap_python_utils.egg-info → tiptap_python_utils-0.4.0}/PKG-INFO +22 -14
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/README.md +21 -13
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/pyproject.toml +1 -1
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/src/tiptap_python_utils/__init__.py +3 -15
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/src/tiptap_python_utils/content.py +31 -0
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/src/tiptap_python_utils/contract/key.py +2 -0
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/src/tiptap_python_utils/model/base.py +3 -0
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/src/tiptap_python_utils/model/nodes.py +0 -4
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/src/tiptap_python_utils/select/selection.py +4 -1
- tiptap_python_utils-0.4.0/src/tiptap_python_utils/shared/__init__.py +11 -0
- tiptap_python_utils-0.4.0/src/tiptap_python_utils/shared/families.py +73 -0
- tiptap_python_utils-0.4.0/src/tiptap_python_utils/shared/fingerprint.py +23 -0
- tiptap_python_utils-0.4.0/src/tiptap_python_utils/shared/identity.py +9 -0
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0/src/tiptap_python_utils.egg-info}/PKG-INFO +22 -14
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/src/tiptap_python_utils.egg-info/SOURCES.txt +0 -1
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/tests/test_compat_imports.py +2 -7
- tiptap_python_utils-0.4.0/tests/test_mutations.py +535 -0
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/tests/test_public_api.py +2 -6
- tiptap_python_utils-0.2.0/src/tiptap_python_utils/shared/__init__.py +0 -17
- tiptap_python_utils-0.2.0/src/tiptap_python_utils/shared/families.py +0 -47
- tiptap_python_utils-0.2.0/src/tiptap_python_utils/shared/fingerprint.py +0 -21
- tiptap_python_utils-0.2.0/src/tiptap_python_utils/shared/identity.py +0 -48
- tiptap_python_utils-0.2.0/src/tiptap_python_utils/shared/sync.py +0 -63
- tiptap_python_utils-0.2.0/tests/test_mutations.py +0 -331
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/LICENSE +0 -0
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/MANIFEST.in +0 -0
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/setup.cfg +0 -0
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/src/tiptap_python_utils/codec/__init__.py +0 -0
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/src/tiptap_python_utils/codec/raw.py +0 -0
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/src/tiptap_python_utils/codec/reader.py +0 -0
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/src/tiptap_python_utils/codec/writer.py +0 -0
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/src/tiptap_python_utils/contract/__init__.py +0 -0
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/src/tiptap_python_utils/contract/kind.py +0 -0
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/src/tiptap_python_utils/contract/policy.py +0 -0
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/src/tiptap_python_utils/exceptions.py +0 -0
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/src/tiptap_python_utils/model/__init__.py +0 -0
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/src/tiptap_python_utils/model/payload.py +0 -0
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/src/tiptap_python_utils/model/registry.py +0 -0
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/src/tiptap_python_utils/py.typed +0 -0
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/src/tiptap_python_utils/select/__init__.py +0 -0
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/src/tiptap_python_utils/tasks/__init__.py +0 -0
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/src/tiptap_python_utils/tasks/query.py +0 -0
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/src/tiptap_python_utils/text/__init__.py +0 -0
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/src/tiptap_python_utils/text/extract.py +0 -0
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/src/tiptap_python_utils/tree/__init__.py +0 -0
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/src/tiptap_python_utils/tree/path.py +0 -0
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/src/tiptap_python_utils/types.py +0 -0
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/src/tiptap_python_utils/walk/__init__.py +0 -0
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/src/tiptap_python_utils/walk/traversal.py +0 -0
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/src/tiptap_python_utils.egg-info/dependency_links.txt +0 -0
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/src/tiptap_python_utils.egg-info/requires.txt +0 -0
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/src/tiptap_python_utils.egg-info/top_level.txt +0 -0
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/tests/test_codec_raw.py +0 -0
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/tests/test_content.py +0 -0
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/tests/test_extract.py +0 -0
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/tests/test_filter.py +0 -0
- {tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/tests/test_traverser.py +0 -0
{tiptap_python_utils-0.2.0/src/tiptap_python_utils.egg-info → tiptap_python_utils-0.4.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tiptap_python_utils
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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
|
|
224
|
-
|
|
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
|
|
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
|
-
|
|
248
|
-
|
|
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
|
|
173
|
-
|
|
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
|
|
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
|
-
|
|
197
|
-
|
|
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.
|
|
7
|
+
version = "0.4.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
|
-
"
|
|
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
|
|
{tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/src/tiptap_python_utils/model/base.py
RENAMED
|
@@ -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
|
|
{tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/src/tiptap_python_utils/model/nodes.py
RENAMED
|
@@ -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 (
|
{tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/src/tiptap_python_utils/select/selection.py
RENAMED
|
@@ -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,73 @@
|
|
|
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.
|
|
56
|
+
|
|
57
|
+
Preserves the target's per-copy identity (local id, sharedId) and its
|
|
58
|
+
per-copy ``place`` discriminator. The family-identical ``shared`` core
|
|
59
|
+
rides along from the canonical body unchanged.
|
|
60
|
+
"""
|
|
61
|
+
canonical = self[target.shared_id]
|
|
62
|
+
raw = canonical.raw()
|
|
63
|
+
attrs = dict(raw.get(key.ATTRS, {}))
|
|
64
|
+
attrs.pop(key.PLACE, None)
|
|
65
|
+
if target.id:
|
|
66
|
+
attrs[key.ID] = target.id
|
|
67
|
+
if target.shared_id:
|
|
68
|
+
attrs[key.SHARED_ID] = target.shared_id
|
|
69
|
+
target_attrs = target.raw().get(key.ATTRS, {})
|
|
70
|
+
if key.PLACE in target_attrs:
|
|
71
|
+
attrs[key.PLACE] = target_attrs[key.PLACE]
|
|
72
|
+
raw[key.ATTRS] = attrs
|
|
73
|
+
return codec.read_node(raw)
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
attrs.pop(key.SHARED, None)
|
|
17
|
+
attrs.pop(key.PLACE, None)
|
|
18
|
+
attrs.pop(key.TASK_CANONICAL_ID, None)
|
|
19
|
+
if attrs:
|
|
20
|
+
raw[key.ATTRS] = attrs
|
|
21
|
+
else:
|
|
22
|
+
raw.pop(key.ATTRS, None)
|
|
23
|
+
return json.dumps(raw, sort_keys=True)
|
{tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0/src/tiptap_python_utils.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tiptap_python_utils
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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
|
|
224
|
-
|
|
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
|
|
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
|
-
|
|
248
|
-
|
|
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,
|
{tiptap_python_utils-0.2.0 → tiptap_python_utils-0.4.0}/src/tiptap_python_utils.egg-info/SOURCES.txt
RENAMED
|
@@ -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
|
-
"
|
|
64
|
-
"
|
|
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(
|