genome-spy-python 0.1.0__py3-none-any.whl

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 (64) hide show
  1. genome_spy/__init__.py +199 -0
  2. genome_spy/_chart_authoring.py +231 -0
  3. genome_spy/_conditions.py +72 -0
  4. genome_spy/_embed.py +87 -0
  5. genome_spy/_expressions.py +271 -0
  6. genome_spy/_parameters.py +267 -0
  7. genome_spy/_render.py +207 -0
  8. genome_spy/_utils.py +75 -0
  9. genome_spy/_widget.py +262 -0
  10. genome_spy/api.py +198 -0
  11. genome_spy/arrow.py +155 -0
  12. genome_spy/channels.py +193 -0
  13. genome_spy/chart.py +1240 -0
  14. genome_spy/data.py +56 -0
  15. genome_spy/data_transformers.py +267 -0
  16. genome_spy/datasets/__init__.py +189 -0
  17. genome_spy/datasets/_airway.py +219 -0
  18. genome_spy/datasets/_annotations.py +37 -0
  19. genome_spy/datasets/_gistic.py +43 -0
  20. genome_spy/datasets/_grammar.py +66 -0
  21. genome_spy/datasets/_hapmap.py +180 -0
  22. genome_spy/datasets/_mutation.py +289 -0
  23. genome_spy/datasets/_oncoprint.py +523 -0
  24. genome_spy/datasets/data/airway_metadata.csv +9 -0
  25. genome_spy/datasets/data/airway_scaledcounts.csv +38695 -0
  26. genome_spy/datasets/data/brca.maf.gz +0 -0
  27. genome_spy/datasets/data/hapmap_gwas.csv +14413 -0
  28. genome_spy/datasets/data/mutation_impact_reference.json +27 -0
  29. genome_spy/datasets/data/oncoprint_dataset3.json +266 -0
  30. genome_spy/datasets/data/p53_sequence_comparison.json.gz +0 -0
  31. genome_spy/datasets/data/pik3ca_mutations.json +1 -0
  32. genome_spy/datasets/data/pik3ca_tcga_brca_lollipop.json +38 -0
  33. genome_spy/datasets/data/refseq_gene_bodies.csv.gz +0 -0
  34. genome_spy/datasets/data/tal1_alphagenome_reference.json.gz +0 -0
  35. genome_spy/datasets/data/tcga.tsv +146 -0
  36. genome_spy/datasets/data/tcga_laml.maf.gz +0 -0
  37. genome_spy/datasets/data/tcga_laml_annot.tsv +201 -0
  38. genome_spy/datasets/data/tcga_laml_combined_oncoplot.json.gz +0 -0
  39. genome_spy/datasets/data/tcga_ov_gistic_lesions.tsv.gz +0 -0
  40. genome_spy/datasets/data/tcga_ov_gistic_scores.tsv.gz +0 -0
  41. genome_spy/helpers.py +185 -0
  42. genome_spy/jupyter.py +5 -0
  43. genome_spy/py.typed +0 -0
  44. genome_spy/schema/__init__.py +784 -0
  45. genome_spy/schema/_kwds.py +1394 -0
  46. genome_spy/schema/_typing.py +186 -0
  47. genome_spy/schema/capabilities.json +593 -0
  48. genome_spy/schema/channels.py +8943 -0
  49. genome_spy/schema/composition.py +1064 -0
  50. genome_spy/schema/core.py +51821 -0
  51. genome_spy/schema/ergonomics.py +2056 -0
  52. genome_spy/schema/expressions.py +476 -0
  53. genome_spy/schema/genome-spy-schema.json +33657 -0
  54. genome_spy/schema/lazy.py +326 -0
  55. genome_spy/schema/mixins.py +11684 -0
  56. genome_spy/schemapi.py +264 -0
  57. genome_spy/static/widget.js +345 -0
  58. genome_spy_python-0.1.0.dist-info/METADATA +185 -0
  59. genome_spy_python-0.1.0.dist-info/RECORD +64 -0
  60. genome_spy_python-0.1.0.dist-info/WHEEL +4 -0
  61. genome_spy_python-0.1.0.dist-info/licenses/LICENSE +21 -0
  62. genome_spy_python-0.1.0.dist-info/licenses/LICENSES/ALTAIR-BSD-3-Clause.txt +27 -0
  63. genome_spy_python-0.1.0.dist-info/licenses/LICENSES/GALLERY-DATA-MIT.txt +22 -0
  64. genome_spy_python-0.1.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +42 -0
genome_spy/_render.py ADDED
@@ -0,0 +1,207 @@
1
+ """Private render-time preparation for binary dataframe transport."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from copy import deepcopy
6
+ from dataclasses import dataclass, field
7
+ from hashlib import sha256
8
+ from typing import Any, Callable, Literal, Protocol
9
+
10
+ from genome_spy._chart_authoring import normalize_data
11
+ from genome_spy.arrow import _try_to_arrow_ipc
12
+ from genome_spy.data_transformers import (
13
+ _consolidate,
14
+ _data_slots,
15
+ _DatasetConsolidation,
16
+ data_transformers,
17
+ )
18
+ from genome_spy.schema import Root
19
+
20
+
21
+ @dataclass(frozen=True, slots=True)
22
+ class _PreparedSpec:
23
+ """A JSON-ready spec and the binary Arrow payloads it references."""
24
+
25
+ spec: dict[str, Any]
26
+ buffers: dict[str, bytes]
27
+ consolidate_datasets: bool | None = None
28
+
29
+
30
+ _DatasetFormat = Literal["arrow", "records"]
31
+
32
+
33
+ @dataclass(frozen=True, slots=True)
34
+ class _LiveDataset:
35
+ """One named dataset synchronized by a live notebook widget."""
36
+
37
+ name: str
38
+ owner: str | None
39
+ scoped: bool
40
+ initial_payload: bytes | None
41
+ initial_format: _DatasetFormat | None
42
+
43
+
44
+ @dataclass(frozen=True, slots=True)
45
+ class _PreparedWidget:
46
+ """A runtime spec and its live named-dataset declarations."""
47
+
48
+ spec: dict[str, Any]
49
+ datasets: tuple[_LiveDataset, ...]
50
+
51
+
52
+ class _RenderSerializable(Protocol):
53
+ """Structural type for charts using the shared serialization traversal."""
54
+
55
+ def _to_dict(
56
+ self,
57
+ *,
58
+ include_schema: bool,
59
+ validate: bool,
60
+ normalize_chart_data: Callable[[Any], Any],
61
+ ) -> dict[str, Any]: ...
62
+
63
+
64
+ @dataclass(slots=True)
65
+ class _RenderContext:
66
+ """State shared by one render-preparation traversal."""
67
+
68
+ buffers: dict[str, bytes] = field(default_factory=dict)
69
+ _table_cache: dict[int, tuple[Any, dict[str, Any]]] = field(default_factory=dict)
70
+
71
+ def normalize_data(self, data: Any) -> Any:
72
+ """Return JSON data or a content-addressed Arrow data source."""
73
+ cached = self._table_cache.get(id(data))
74
+ if cached is not None and cached[0] is data:
75
+ return dict(cached[1])
76
+
77
+ payload = _try_to_arrow_ipc(data)
78
+ if payload is None:
79
+ return normalize_data(data)
80
+
81
+ identifier = sha256(payload).hexdigest()
82
+ self.buffers.setdefault(identifier, payload)
83
+ normalized = {"url": f"arrow://{identifier}", "format": {"type": "arrow"}}
84
+ self._table_cache[id(data)] = (data, normalized)
85
+ return dict(normalized)
86
+
87
+
88
+ def prepare_render(chart: _RenderSerializable) -> _PreparedSpec:
89
+ """Prepare one chart through its shared render-time serialization path."""
90
+ enabled = data_transformers.consolidate_datasets
91
+ context = _RenderContext()
92
+ collector = _DatasetConsolidation(chart) if enabled else None
93
+ spec = chart._to_dict(
94
+ include_schema=True,
95
+ validate=False,
96
+ normalize_chart_data=(
97
+ (lambda data: collector.source(context.normalize_data(data)))
98
+ if collector is not None
99
+ else context.normalize_data
100
+ ),
101
+ )
102
+ if collector is not None:
103
+ collector.finish(spec)
104
+ return _PreparedSpec(
105
+ spec=Root(**spec).to_dict(),
106
+ buffers=context.buffers,
107
+ consolidate_datasets=enabled,
108
+ )
109
+
110
+
111
+ def prepare_widget(chart: _RenderSerializable) -> _PreparedWidget:
112
+ """Prepare a chart for live named-dataset widget updates."""
113
+ return prepare_widget_spec(prepare_render(chart))
114
+
115
+
116
+ def prepare_widget_spec(prepared: _PreparedSpec) -> _PreparedWidget:
117
+ """Rewrite known eager sources in a prepared spec as named datasets."""
118
+ spec = deepcopy(prepared.spec)
119
+ enabled = prepared.consolidate_datasets
120
+ if enabled is None:
121
+ enabled = data_transformers.consolidate_datasets
122
+ if enabled:
123
+ _consolidate(spec)
124
+ root_datasets = spec.setdefault("datasets", {})
125
+ if not isinstance(root_datasets, dict):
126
+ raise TypeError("GenomeSpy root datasets must be a mapping.")
127
+
128
+ slots = [
129
+ (kind, dict(value) if kind == "datasets" else value, owner, scoped)
130
+ for kind, value, owner, scoped in _data_slots(spec)
131
+ if kind != "template"
132
+ ]
133
+ used_names: set[str] = set()
134
+ # Template references also reserve names, but their declarations belong to
135
+ # future import instances and must not be registered as live root datasets.
136
+ for kind, value, _, _ in _data_slots(spec, include_templates=True):
137
+ if kind == "datasets":
138
+ used_names.update(value)
139
+ elif kind == "data" and isinstance(value.get("name"), str):
140
+ used_names.add(value["name"])
141
+ generated_names: dict[str, str] = {}
142
+ datasets: list[_LiveDataset] = []
143
+
144
+ def register(
145
+ name: str,
146
+ *,
147
+ owner: str | None,
148
+ scoped: bool,
149
+ initial_payload: bytes | None = None,
150
+ initial_format: _DatasetFormat | None = None,
151
+ ) -> None:
152
+ datasets.append(
153
+ _LiveDataset(
154
+ name=name,
155
+ owner=owner,
156
+ scoped=scoped,
157
+ initial_payload=initial_payload,
158
+ initial_format=initial_format,
159
+ )
160
+ )
161
+
162
+ def generated_name(token: str) -> str:
163
+ existing = generated_names.get(token)
164
+ if existing is not None:
165
+ return existing
166
+ index = len(generated_names)
167
+ candidate = f"__genome_spy_python_data_{index}"
168
+ while candidate in used_names:
169
+ index += 1
170
+ candidate = f"__genome_spy_python_data_{index}"
171
+ used_names.add(candidate)
172
+ generated_names[token] = candidate
173
+ return candidate
174
+
175
+ for kind, data, owner, scoped in slots:
176
+ if kind == "datasets":
177
+ for name in data:
178
+ if isinstance(name, str) and name:
179
+ register(name, owner=owner, scoped=scoped)
180
+ else:
181
+ url = data.get("url")
182
+ if isinstance(url, str) and url.startswith("arrow://"):
183
+ token = url.removeprefix("arrow://")
184
+ payload = prepared.buffers.get(token)
185
+ if payload is None:
186
+ raise ValueError(f"No Arrow IPC payload provided for {token}.")
187
+ name = generated_name(
188
+ token if enabled else f"{token}:{len(generated_names)}"
189
+ )
190
+ if name not in root_datasets:
191
+ root_datasets[name] = []
192
+ register(
193
+ name,
194
+ owner=None,
195
+ scoped=False,
196
+ initial_payload=payload,
197
+ initial_format="arrow",
198
+ )
199
+ data.clear()
200
+ data["name"] = name
201
+ elif set(data) == {"values"} and isinstance(data["values"], list):
202
+ name = generated_name(f"records:{len(generated_names)}")
203
+ root_datasets[name] = data["values"]
204
+ register(name, owner=None, scoped=False)
205
+ data.clear()
206
+ data["name"] = name
207
+ return _PreparedWidget(spec=spec, datasets=tuple(datasets))
genome_spy/_utils.py ADDED
@@ -0,0 +1,75 @@
1
+ """Internal helpers for the first GenomeSpy Python API slice."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from collections.abc import Mapping
7
+ from typing import Any
8
+
9
+ TYPE_ALIASES: dict[str, str] = {
10
+ "q": "quantitative",
11
+ "quantitative": "quantitative",
12
+ "n": "nominal",
13
+ "nominal": "nominal",
14
+ "o": "ordinal",
15
+ "ordinal": "ordinal",
16
+ "i": "index",
17
+ "index": "index",
18
+ "g": "locus",
19
+ "l": "locus",
20
+ "locus": "locus",
21
+ }
22
+
23
+
24
+ def compact_json(data: Any) -> str:
25
+ """Serialize JSON using stable, notebook-friendly formatting."""
26
+ return json.dumps(data, separators=(",", ":"), sort_keys=True)
27
+
28
+
29
+ def pretty_json(data: Any) -> str:
30
+ """Serialize JSON for user-facing export."""
31
+ return json.dumps(data, indent=2)
32
+
33
+
34
+ class JsonSpec(dict[str, Any]):
35
+ """Dict-like object whose display representation is valid JSON."""
36
+
37
+ def __repr__(self) -> str:
38
+ return pretty_json(self)
39
+
40
+ def __str__(self) -> str:
41
+ return pretty_json(self)
42
+
43
+ def _repr_pretty_(self, printer: Any, cycle: bool) -> None:
44
+ """Pretty-print JSON in IPython/Jupyter text output."""
45
+ if cycle:
46
+ printer.text("JsonSpec(...)")
47
+ return
48
+ printer.text(pretty_json(self))
49
+
50
+ def _repr_mimebundle_(
51
+ self,
52
+ include: object | None = None,
53
+ exclude: object | None = None,
54
+ ) -> dict[str, str]:
55
+ """Display as indented JSON text in notebook frontends."""
56
+ del include, exclude
57
+ return {"text/plain": pretty_json(self)}
58
+
59
+
60
+ def is_mapping(value: Any) -> bool:
61
+ """Return whether ``value`` behaves like a mapping."""
62
+ return isinstance(value, Mapping)
63
+
64
+
65
+ def parse_shorthand(shorthand: str) -> dict[str, Any]:
66
+ """Parse a compact ``field:type`` channel shorthand."""
67
+ field, separator, channel_type = shorthand.rpartition(":")
68
+ if not separator:
69
+ return {"field": shorthand}
70
+
71
+ normalized_type = TYPE_ALIASES.get(channel_type.lower())
72
+ if normalized_type is None:
73
+ return {"field": shorthand}
74
+
75
+ return {"field": field, "type": normalized_type}
genome_spy/_widget.py ADDED
@@ -0,0 +1,262 @@
1
+ """Anywidget-backed notebook renderer for GenomeSpy charts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping, Sequence
6
+ from pathlib import Path
7
+ from typing import Any, Literal, cast
8
+
9
+ import anywidget
10
+ import traitlets
11
+
12
+ from genome_spy._embed import (
13
+ DEFAULT_CONTROLS,
14
+ DEFAULT_CONTROLS_MODULE_URL,
15
+ DEFAULT_EMBED_URL,
16
+ DEFAULT_INSPECTOR_MODULE_URL,
17
+ Controls,
18
+ control_definitions,
19
+ normalize_controls,
20
+ )
21
+ from genome_spy._chart_authoring import json_safe, records_from_data
22
+ from genome_spy._render import _PreparedSpec, prepare_widget_spec
23
+ from genome_spy.arrow import to_arrow_ipc
24
+ from genome_spy.schemapi import Undefined, UndefinedType
25
+
26
+ _ESM_PATH = Path(__file__).with_name("static") / "widget.js"
27
+ _DatasetFormat = Literal["arrow", "records"]
28
+
29
+
30
+ class JupyterChart(anywidget.AnyWidget):
31
+ """A lightweight anywidget wrapper around GenomeSpy's ``embed`` API."""
32
+
33
+ _esm = _ESM_PATH
34
+
35
+ spec = traitlets.Dict().tag(sync=True)
36
+ bundle_url = traitlets.Unicode(DEFAULT_EMBED_URL).tag(sync=True)
37
+ embed_options = traitlets.Dict(default_value={}).tag(sync=True)
38
+ controls = traitlets.List(
39
+ trait=traitlets.Unicode(), default_value=list[str](DEFAULT_CONTROLS)
40
+ ).tag(sync=True)
41
+ _control_definitions = traitlets.Dict(default_value=control_definitions()).tag(
42
+ sync=True
43
+ )
44
+ controls_module_url = traitlets.Unicode(DEFAULT_CONTROLS_MODULE_URL).tag(sync=True)
45
+ inspector_module_url = traitlets.Unicode(DEFAULT_INSPECTOR_MODULE_URL).tag(
46
+ sync=True
47
+ )
48
+ dataset_manifest = traitlets.List(trait=traitlets.Dict(), default_value=[]).tag(
49
+ sync=True
50
+ )
51
+ parameter_names = traitlets.List(trait=traitlets.Unicode(), default_value=[]).tag(
52
+ sync=True
53
+ )
54
+ parameter_values = traitlets.Dict(default_value={}).tag(sync=True)
55
+ enable_click_events = traitlets.Bool(False).tag(sync=True)
56
+ clicked_datum = traitlets.Dict(default_value={}).tag(sync=True)
57
+ click_revision = traitlets.Int(0).tag(sync=True)
58
+ error = traitlets.Unicode("").tag(sync=True)
59
+
60
+ def __init__(
61
+ self,
62
+ chart: Any,
63
+ *,
64
+ bundle_url: str = DEFAULT_EMBED_URL,
65
+ embed_options: dict[str, Any] | None = None,
66
+ controls: Controls | UndefinedType = Undefined,
67
+ controls_module_url: str = DEFAULT_CONTROLS_MODULE_URL,
68
+ inspector_module_url: str = DEFAULT_INSPECTOR_MODULE_URL,
69
+ parameter_names: Sequence[str] = (),
70
+ parameter_values: Mapping[str, Any] | None = None,
71
+ enable_click_events: bool = False,
72
+ **kwargs: Any,
73
+ ) -> None:
74
+ if hasattr(chart, "_prepare_widget"):
75
+ prepared = chart._prepare_widget()
76
+ else:
77
+ prepared = prepare_widget_spec(_PreparedSpec(spec=dict(chart), buffers={}))
78
+
79
+ manifest: list[dict[str, Any]] = []
80
+ for index, dataset in enumerate(prepared.datasets):
81
+ prefix = f"_dataset_{index}"
82
+ manifest.append(
83
+ {
84
+ "name": dataset.name,
85
+ "owner": dataset.owner,
86
+ "scoped": dataset.scoped,
87
+ "payload_trait": f"{prefix}_payload",
88
+ "format_trait": f"{prefix}_format",
89
+ "revision_trait": f"{prefix}_revision",
90
+ "initial_payload": dataset.initial_payload,
91
+ "initial_format": dataset.initial_format,
92
+ }
93
+ )
94
+
95
+ super().__init__(
96
+ spec=prepared.spec,
97
+ bundle_url=bundle_url,
98
+ embed_options=embed_options or {},
99
+ controls=list(normalize_controls(controls)),
100
+ controls_module_url=controls_module_url,
101
+ inspector_module_url=inspector_module_url,
102
+ dataset_manifest=[self._manifest_entry(entry) for entry in manifest],
103
+ parameter_names=list(parameter_names),
104
+ parameter_values=dict(parameter_values or {}),
105
+ enable_click_events=enable_click_events,
106
+ **kwargs,
107
+ )
108
+
109
+ for entry in manifest:
110
+ initial_payload = entry["initial_payload"]
111
+ initial_format = entry["initial_format"] or "records"
112
+ self.add_traits(
113
+ **{
114
+ entry["payload_trait"]: traitlets.Any(initial_payload).tag(
115
+ sync=True
116
+ ),
117
+ entry["format_trait"]: traitlets.Unicode(initial_format).tag(
118
+ sync=True
119
+ ),
120
+ entry["revision_trait"]: traitlets.Int(
121
+ 1 if initial_payload is not None else 0
122
+ ).tag(sync=True),
123
+ }
124
+ )
125
+
126
+ @property
127
+ def dataset_names(self) -> tuple[str, ...]:
128
+ """Return live dataset names in declaration order.
129
+
130
+ Description:
131
+ Names are taken from the widget's fixed runtime dataset manifest.
132
+ Repeated names indicate scoped declarations and require a unique
133
+ name before they can be addressed through :meth:`set_dataset`.
134
+
135
+ Returns:
136
+ Dataset names in declaration order.
137
+
138
+ Raises:
139
+ No exceptions are raised directly here.
140
+
141
+ Example:
142
+ >>> chart.widget().dataset_names
143
+ ('table',)
144
+ """
145
+ return tuple(str(entry["name"]) for entry in self.dataset_manifest)
146
+
147
+ def set_dataset(
148
+ self,
149
+ name: str,
150
+ data: object,
151
+ *,
152
+ format: _DatasetFormat = "arrow",
153
+ ) -> None:
154
+ """Replace one declared live dataset without recreating GenomeSpy.
155
+
156
+ Description:
157
+ Arrow is the default and transfers supported dataframe/table inputs
158
+ as a binary AnyWidget buffer. Use ``format="records"`` only for
159
+ ordinary JSON-compatible record lists.
160
+
161
+ Args:
162
+ name: Declared dataset name.
163
+ data: Table or records used as the replacement value.
164
+ format: Transport format, either ``"arrow"`` or ``"records"``.
165
+
166
+ Returns:
167
+ ``None`` after the synchronized transport state has been updated.
168
+
169
+ Raises:
170
+ KeyError: If no declared dataset has ``name``.
171
+ ValueError: If ``name`` is ambiguous or cannot be addressed.
172
+ TypeError: If ``data`` cannot be serialized using ``format``.
173
+
174
+ Example:
175
+ >>> view.set_dataset('table', dataframe)
176
+ """
177
+ entry = self._dataset_entry(name)
178
+ payload = self._serialize_dataset(data, format)
179
+ revision_trait = cast(str, entry["revision_trait"])
180
+ with self.hold_sync():
181
+ setattr(self, cast(str, entry["payload_trait"]), payload)
182
+ setattr(self, cast(str, entry["format_trait"]), format)
183
+ setattr(self, revision_trait, getattr(self, revision_trait) + 1)
184
+
185
+ def set_data(
186
+ self,
187
+ data: object,
188
+ *,
189
+ format: _DatasetFormat = "arrow",
190
+ ) -> None:
191
+ """Replace the only declared live dataset.
192
+
193
+ Description:
194
+ This is a convenience alias for :meth:`set_dataset` when the widget
195
+ has exactly one live dataset.
196
+
197
+ Args:
198
+ data: Table or records used as the replacement value.
199
+ format: Transport format, either ``"arrow"`` or ``"records"``.
200
+
201
+ Returns:
202
+ ``None`` after the synchronized transport state has been updated.
203
+
204
+ Raises:
205
+ ValueError: If the widget has zero or multiple live datasets.
206
+ TypeError: If ``data`` cannot be serialized using ``format``.
207
+
208
+ Example:
209
+ >>> view.set_data(dataframe)
210
+ """
211
+ if len(self.dataset_manifest) != 1:
212
+ names = ", ".join(self.dataset_names) or "none"
213
+ raise ValueError(
214
+ "set_data() requires exactly one live dataset; "
215
+ f"available datasets: {names}."
216
+ )
217
+ self.set_dataset(str(self.dataset_manifest[0]["name"]), data, format=format)
218
+
219
+ def _dataset_entry(self, name: str) -> dict[str, Any]:
220
+ """Return the unambiguous manifest entry for a public dataset name."""
221
+ matches = [entry for entry in self.dataset_manifest if entry["name"] == name]
222
+ if not matches:
223
+ available = ", ".join(self.dataset_names) or "none"
224
+ raise KeyError(
225
+ f"Unknown dataset {name!r}. Available datasets: {available}."
226
+ )
227
+ if len(matches) != 1:
228
+ raise ValueError(
229
+ f"Dataset {name!r} is declared in multiple scopes and cannot be "
230
+ "addressed by name alone."
231
+ )
232
+ entry = dict(matches[0])
233
+ if entry["scoped"] and entry["owner"] is None:
234
+ raise ValueError(
235
+ f"Dataset {name!r} is declared in an unnamed nested view. "
236
+ "Give its owner view a unique name before updating it."
237
+ )
238
+ return entry
239
+
240
+ @staticmethod
241
+ def _manifest_entry(entry: Mapping[str, Any]) -> dict[str, Any]:
242
+ """Return synchronized metadata without Python-only initial values."""
243
+ return {
244
+ key: value
245
+ for key, value in entry.items()
246
+ if key not in {"initial_payload", "initial_format"}
247
+ }
248
+
249
+ @staticmethod
250
+ def _serialize_dataset(data: object, format: _DatasetFormat) -> bytes | list[Any]:
251
+ """Serialize one dataset before mutating synchronized trait state."""
252
+ if format == "arrow":
253
+ return to_arrow_ipc(data)
254
+ if format == "records":
255
+ records = records_from_data(data)
256
+ if records is None:
257
+ raise TypeError(
258
+ "Record transport requires a list of records or a supported "
259
+ "table with record conversion."
260
+ )
261
+ return cast(list[Any], json_safe(records))
262
+ raise ValueError(f"Unsupported dataset format {format!r}.")