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
@@ -0,0 +1,271 @@
1
+ """Python authoring primitives for GenomeSpy expressions.
2
+
3
+ The operator model follows Vega-Altair's expression API while keeping the
4
+ serialized value a real ``str`` subclass. As a result, generated schema
5
+ signatures that accept expression strings also accept ``Expression`` objects
6
+ without transform-specific adapters or normalization rules.
7
+
8
+ Portions are adapted from Vega-Altair's expression runtime:
9
+ https://github.com/vega/altair/blob/main/altair/expr/core.py
10
+ Copyright (c) 2015-2025, Vega-Altair Developers. BSD-3-Clause license; see
11
+ ``LICENSES/ALTAIR-BSD-3-Clause.txt``.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import sys
17
+ from math import isinf, isnan
18
+ from typing import Any, Protocol, Self, TypeAlias, runtime_checkable
19
+
20
+
21
+ @runtime_checkable
22
+ class ExpressionOperand(Protocol):
23
+ """A value that can provide a GenomeSpy expression reference."""
24
+
25
+ def _to_expr(self) -> Expression:
26
+ """Return this value's expression representation."""
27
+
28
+
29
+ def _js_repr(value: Any) -> str:
30
+ """Return a JavaScript-safe expression representation."""
31
+ if value is True:
32
+ return "true"
33
+ if value is False:
34
+ return "false"
35
+ if value is None:
36
+ return "null"
37
+ if isinstance(value, ExpressionOperand):
38
+ return str(value._to_expr())
39
+ if isinstance(value, float):
40
+ if isnan(value):
41
+ return "NaN"
42
+ if isinf(value):
43
+ return "Infinity" if value > 0 else "-Infinity"
44
+ if isinstance(value, list | tuple):
45
+ return "[" + ",".join(_js_repr(item) for item in value) + "]"
46
+ if isinstance(value, dict):
47
+ items = (f"{_js_repr(key)}:{_js_repr(item)}" for key, item in value.items())
48
+ return "{" + ",".join(items) + "}"
49
+ numpy = sys.modules.get("numpy")
50
+ if numpy is not None and isinstance(value, numpy.generic):
51
+ return _js_repr(value.item())
52
+ return repr(value)
53
+
54
+
55
+ def _expression_string(value: str | ExpressionOperand) -> str:
56
+ """Return the string accepted by an upstream expression property."""
57
+ if isinstance(value, ExpressionOperand):
58
+ return str(value._to_expr())
59
+ return value
60
+
61
+
62
+ def _function_expression(name: str, *arguments: Any) -> Expression:
63
+ """Build a function-call expression."""
64
+ rendered = ",".join(_js_repr(argument) for argument in arguments)
65
+ return Expression(f"{name}({rendered})")
66
+
67
+
68
+ class ExpressionOperatorMixin:
69
+ """Python operators shared by expression strings and parameter handles."""
70
+
71
+ __hash__ = None # type: ignore[assignment]
72
+
73
+ def _to_expr(self) -> Expression:
74
+ raise NotImplementedError
75
+
76
+ def __bool__(self) -> bool:
77
+ raise TypeError(
78
+ "GenomeSpy expressions cannot be converted to bool; use &, |, and ~ "
79
+ "instead of Python's and, or, and not."
80
+ )
81
+
82
+ def __getattr__(self, name: str) -> Expression:
83
+ if name.startswith("__") and name.endswith("__"):
84
+ raise AttributeError(name)
85
+ return Expression(f"{self._to_expr()}.{name}")
86
+
87
+ def __getitem__(self, key: Any) -> Expression:
88
+ return Expression(f"{self._to_expr()}[{_js_repr(key)}]")
89
+
90
+ def _binary(self, operator: str, other: Any) -> Expression:
91
+ return Expression(f"({self._to_expr()} {operator} {_js_repr(other)})")
92
+
93
+ def _reverse_binary(self, operator: str, other: Any) -> Expression:
94
+ return Expression(f"({_js_repr(other)} {operator} {self._to_expr()})")
95
+
96
+ def __add__(self, other: Any) -> Expression:
97
+ return self._binary("+", other)
98
+
99
+ def __radd__(self, other: Any) -> Expression:
100
+ return self._reverse_binary("+", other)
101
+
102
+ def __sub__(self, other: Any) -> Expression:
103
+ return self._binary("-", other)
104
+
105
+ def __rsub__(self, other: Any) -> Expression:
106
+ return self._reverse_binary("-", other)
107
+
108
+ def __mul__(self, other: Any) -> Expression:
109
+ return self._binary("*", other)
110
+
111
+ def __rmul__(self, other: Any) -> Expression:
112
+ return self._reverse_binary("*", other)
113
+
114
+ def __truediv__(self, other: Any) -> Expression:
115
+ return self._binary("/", other)
116
+
117
+ def __rtruediv__(self, other: Any) -> Expression:
118
+ return self._reverse_binary("/", other)
119
+
120
+ def __mod__(self, other: Any) -> Expression:
121
+ return self._binary("%", other)
122
+
123
+ def __rmod__(self, other: Any) -> Expression:
124
+ return self._reverse_binary("%", other)
125
+
126
+ def __pow__(self, other: Any) -> Expression:
127
+ return _function_expression("pow", self, other)
128
+
129
+ def __rpow__(self, other: Any) -> Expression:
130
+ return _function_expression("pow", other, self)
131
+
132
+ def __neg__(self) -> Expression:
133
+ return Expression(f"(-{self._to_expr()})")
134
+
135
+ def __pos__(self) -> Expression:
136
+ return Expression(f"(+{self._to_expr()})")
137
+
138
+ def __eq__(self, other: object) -> Expression: # type: ignore[override]
139
+ return self._binary("===", other)
140
+
141
+ def __ne__(self, other: object) -> Expression: # type: ignore[override]
142
+ return self._binary("!==", other)
143
+
144
+ def __lt__(self, other: Any) -> Expression:
145
+ return self._binary("<", other)
146
+
147
+ def __le__(self, other: Any) -> Expression:
148
+ return self._binary("<=", other)
149
+
150
+ def __gt__(self, other: Any) -> Expression:
151
+ return self._binary(">", other)
152
+
153
+ def __ge__(self, other: Any) -> Expression:
154
+ return self._binary(">=", other)
155
+
156
+ def __and__(self, other: Any) -> Expression:
157
+ return self._binary("&&", other)
158
+
159
+ def __rand__(self, other: Any) -> Expression:
160
+ return self._reverse_binary("&&", other)
161
+
162
+ def __or__(self, other: Any) -> Expression:
163
+ return self._binary("||", other)
164
+
165
+ def __ror__(self, other: Any) -> Expression:
166
+ return self._reverse_binary("||", other)
167
+
168
+ def __invert__(self) -> Expression:
169
+ return Expression(f"(!{self._to_expr()})")
170
+
171
+ def __abs__(self) -> Expression:
172
+ return _function_expression("abs", self)
173
+
174
+
175
+ class Expression(ExpressionOperatorMixin, str): # type: ignore[misc]
176
+ """A composable GenomeSpy expression string.
177
+
178
+ Python operators build the JavaScript-like expression syntax understood by
179
+ GenomeSpy. The class remains a string for direct compatibility with every
180
+ generated schema property whose upstream type is ``string``.
181
+
182
+ Args:
183
+ value: Serialized expression source.
184
+
185
+ Returns:
186
+ A composable expression value.
187
+
188
+ Raises:
189
+ TypeError: If Python evaluates an expression as a boolean.
190
+
191
+ Example:
192
+ >>> from genome_spy import datum
193
+ >>> str((datum.score >= 10) & (datum.kind == "PASS"))
194
+ "((datum.score >= 10) && (datum.kind === 'PASS'))"
195
+ """
196
+
197
+ def __repr__(self) -> str:
198
+ return str(self)
199
+
200
+ def _to_expr(self) -> Expression:
201
+ return self
202
+
203
+ def copy(self) -> Self:
204
+ """Return an equivalent expression value.
205
+
206
+ Returns:
207
+ A new expression with the same serialized source.
208
+
209
+ Raises:
210
+ No exceptions are raised.
211
+
212
+ Example:
213
+ >>> from genome_spy import datum
214
+ >>> str(datum.x.copy())
215
+ 'datum.x'
216
+ """
217
+ return type(self)(self)
218
+
219
+ def to_dict(self) -> str:
220
+ """Return the schema-compatible expression string.
221
+
222
+ Returns:
223
+ The serialized expression source.
224
+
225
+ Raises:
226
+ No exceptions are raised.
227
+
228
+ Example:
229
+ >>> from genome_spy import datum
230
+ >>> datum.x.to_dict()
231
+ 'datum.x'
232
+ """
233
+ return str(self)
234
+
235
+
236
+ class DatumExpression:
237
+ """Expression-field behavior mixed into the generated datum helper."""
238
+
239
+ def __repr__(self) -> str:
240
+ return "datum"
241
+
242
+ def __getattr__(self, name: str) -> Expression:
243
+ if name.startswith("__") and name.endswith("__"):
244
+ raise AttributeError(name)
245
+ return Expression(f"datum.{name}")
246
+
247
+ def __getitem__(self, key: Any) -> Expression:
248
+ return Expression(f"datum[{_js_repr(key)}]")
249
+
250
+
251
+ IntoExpression: TypeAlias = (
252
+ str
253
+ | int
254
+ | float
255
+ | bool
256
+ | None
257
+ | Expression
258
+ | list[Any]
259
+ | tuple[Any, ...]
260
+ | dict[str, Any]
261
+ | ExpressionOperand
262
+ )
263
+
264
+
265
+ __all__ = [
266
+ "DatumExpression",
267
+ "Expression",
268
+ "ExpressionOperand",
269
+ "ExpressionOperatorMixin",
270
+ "IntoExpression",
271
+ ]
@@ -0,0 +1,267 @@
1
+ """Altair-style authoring handles for GenomeSpy parameters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ from typing import Any, Self, cast
8
+
9
+ from genome_spy._expressions import (
10
+ Expression,
11
+ ExpressionOperatorMixin,
12
+ _expression_string,
13
+ )
14
+ from genome_spy.schema import core
15
+ from genome_spy.schemapi import (
16
+ SchemaBase,
17
+ SchemaValidationError,
18
+ Undefined,
19
+ normalize_schema_value,
20
+ )
21
+
22
+
23
+ class Parameter(ExpressionOperatorMixin, core.ExprRef):
24
+ """Represent a declared GenomeSpy parameter in Python expressions.
25
+
26
+ A parameter handle keeps its generated declaration in :attr:`param` and
27
+ supplies the parameter name when used in an expression. Attach handles to
28
+ charts with :meth:`genome_spy.TopLevelSpec.add_params`.
29
+
30
+ Args:
31
+ param: Exact generated GenomeSpy parameter declaration.
32
+ empty: Whether an empty selection matches when consumed as a predicate.
33
+
34
+ Returns:
35
+ A reusable parameter authoring handle.
36
+
37
+ Raises:
38
+ TypeError: If the declaration has no string name.
39
+
40
+ Example:
41
+ >>> threshold = param("threshold", value=0.5)
42
+ >>> str(threshold * 2)
43
+ '(threshold * 2)'
44
+ """
45
+
46
+ def __init__(
47
+ self,
48
+ param: SchemaBase,
49
+ *,
50
+ empty: bool = True,
51
+ _name_is_explicit: bool = True,
52
+ ) -> None:
53
+ from genome_spy.schema.ergonomics import (
54
+ _PARAMETER_TYPES,
55
+ _SELECTION_PARAMETER_TYPES,
56
+ )
57
+
58
+ if not isinstance(param, _PARAMETER_TYPES):
59
+ raise TypeError(
60
+ "Parameter requires a generated GenomeSpy parameter declaration."
61
+ )
62
+ values = param.to_dict(validate=False)
63
+ name = values.get("name")
64
+ if not isinstance(name, str):
65
+ raise TypeError("A parameter declaration must have a string name.")
66
+ self.param = param
67
+ self.empty = empty
68
+ self._name_is_explicit = _name_is_explicit
69
+ self._is_selection = isinstance(
70
+ param, _SELECTION_PARAMETER_TYPES
71
+ ) or _matches_parameter_type(values, _SELECTION_PARAMETER_TYPES)
72
+ core.ExprRef.__init__(self, expr=name)
73
+
74
+ @property
75
+ def name(self) -> str:
76
+ """Return the declared GenomeSpy parameter name."""
77
+ return cast(str, self._kwds["expr"])
78
+
79
+ @property
80
+ def is_selection(self) -> bool:
81
+ """Return whether this handle represents a selection parameter."""
82
+ return self._is_selection
83
+
84
+ @property
85
+ def name_is_explicit(self) -> bool:
86
+ """Return whether the parameter name was supplied by the user."""
87
+ return self._name_is_explicit
88
+
89
+ def _to_expr(self) -> Expression:
90
+ if self.is_selection:
91
+ raise TypeError("Selection parameters cannot be used as expressions.")
92
+ return Expression(self.name)
93
+
94
+ def copy(self, *, deep: bool = True, **kwds: Any) -> Self:
95
+ """Return an equivalent parameter handle.
96
+
97
+ Args:
98
+ deep: Copy nested values in the generated declaration.
99
+ **kwds: Updates for the generated declaration.
100
+
101
+ Returns:
102
+ A parameter handle with the same declaration and authoring metadata.
103
+
104
+ Raises:
105
+ TypeError: If the copied declaration is not a generated parameter.
106
+
107
+ Example:
108
+ >>> threshold = param("threshold", value=0.5)
109
+ >>> threshold.copy().param.to_dict()
110
+ {'name': 'threshold', 'value': 0.5}
111
+ """
112
+ definition = self.param.copy(deep=deep, **kwds)
113
+ name_is_explicit = self.name_is_explicit or "name" in kwds
114
+ if not name_is_explicit and kwds:
115
+ values = definition.to_dict(validate=False)
116
+ values.pop("name", None)
117
+ values["name"] = _stable_parameter_name(values)
118
+ definition = type(definition)(**values)
119
+ return type(self)(
120
+ definition,
121
+ empty=self.empty,
122
+ _name_is_explicit=name_is_explicit,
123
+ )
124
+
125
+ def to_dict(self, *, validate: bool = True) -> dict[str, Any]:
126
+ """Return an expression reference for expression-capable parameters.
127
+
128
+ Args:
129
+ validate: Validate the expression reference against its schema.
130
+
131
+ Returns:
132
+ A GenomeSpy expression-reference mapping.
133
+
134
+ Raises:
135
+ TypeError: If this is a selection parameter, which requires an
136
+ explicit condition or filter context.
137
+
138
+ Example:
139
+ >>> param("opacity", value=0.5).to_dict()
140
+ {'expr': 'opacity'}
141
+ """
142
+ if self.is_selection:
143
+ raise TypeError(
144
+ "Selection parameters require a condition or filter context; "
145
+ "attach declarations with chart.add_params(selection)."
146
+ )
147
+ return core.ExprRef(expr=self.name).to_dict(validate=validate)
148
+
149
+
150
+ def _stable_parameter_name(properties: dict[str, Any]) -> str:
151
+ normalized = normalize_schema_value(properties, validate=False)
152
+ payload = json.dumps(
153
+ normalized,
154
+ sort_keys=True,
155
+ separators=(",", ":"),
156
+ ensure_ascii=True,
157
+ )
158
+ digest = hashlib.sha256(payload.encode("utf-8")).hexdigest()[:10]
159
+ return f"param_{digest}"
160
+
161
+
162
+ def _defined_properties(properties: dict[str, Any]) -> dict[str, Any]:
163
+ return {key: value for key, value in properties.items() if value is not Undefined}
164
+
165
+
166
+ def _matches_parameter_type(
167
+ properties: dict[str, Any],
168
+ variants: tuple[type[SchemaBase], ...],
169
+ ) -> bool:
170
+ for variant in variants:
171
+ try:
172
+ variant(**properties).to_dict(validate=True)
173
+ except SchemaValidationError:
174
+ continue
175
+ return True
176
+ return False
177
+
178
+
179
+ def _select_parameter_class(
180
+ properties: dict[str, Any],
181
+ variants: tuple[type[SchemaBase], ...],
182
+ ) -> type[SchemaBase]:
183
+ supplied = set(properties)
184
+ matches: list[type[SchemaBase]] = []
185
+ for variant in variants:
186
+ schema = variant.resolve_references()
187
+ allowed = set(schema.get("properties", {}))
188
+ required = set(schema.get("required", ()))
189
+ if required <= supplied <= allowed:
190
+ matches.append(variant)
191
+ if len(matches) > 1:
192
+ validated: list[type[SchemaBase]] = []
193
+ for variant in matches:
194
+ try:
195
+ variant(**properties).to_dict(validate=True)
196
+ except SchemaValidationError:
197
+ continue
198
+ validated.append(variant)
199
+ matches = validated
200
+ if len(matches) != 1:
201
+ names = ", ".join(sorted(supplied)) or "no arguments"
202
+ if not matches:
203
+ raise TypeError(f"No GenomeSpy parameter variant accepts: {names}.")
204
+ choices = ", ".join(variant.__name__ for variant in matches)
205
+ raise TypeError(
206
+ f"Ambiguous GenomeSpy parameter arguments ({names}): {choices}."
207
+ )
208
+ return matches[0]
209
+
210
+
211
+ def _make_parameter(
212
+ name: str | None = None,
213
+ /,
214
+ *,
215
+ _variants: tuple[type[SchemaBase], ...],
216
+ empty: bool = True,
217
+ **properties: Any,
218
+ ) -> Parameter:
219
+ """Create a reusable GenomeSpy parameter handle.
220
+
221
+ The supplied properties are matched against the concrete parameter leaves
222
+ in GenomeSpy's generated schema. The resulting declaration is available as
223
+ ``handle.param`` and is attached to charts with ``add_params()``.
224
+
225
+ Args:
226
+ name: Parameter name. A deterministic name is generated when omitted.
227
+ bind: Optional input binding for a value parameter.
228
+ description: Human-readable parameter description.
229
+ expr: Reactive expression for an expression parameter.
230
+ persist: Whether GenomeSpy App should persist the parameter.
231
+ push: Reuse and update an ancestor parameter with the same name.
232
+ ruler: Ruler configuration.
233
+ select: Point or interval selection configuration.
234
+ transition: Numeric interpolation configuration.
235
+ value: Initial parameter value.
236
+ empty: Whether an empty selection matches in conditions and filters.
237
+
238
+ Returns:
239
+ A reusable parameter handle containing the exact generated declaration.
240
+
241
+ Raises:
242
+ TypeError: If the supplied properties match no unique parameter branch.
243
+
244
+ Example:
245
+ >>> cutoff = param("cutoff", value=0.5)
246
+ >>> cutoff.param.to_dict()
247
+ {'name': 'cutoff', 'value': 0.5}
248
+ """
249
+ properties = _defined_properties(properties)
250
+ if "expr" in properties:
251
+ properties["expr"] = _expression_string(properties["expr"])
252
+ name_is_explicit = name is not None
253
+ properties["name"] = name or _stable_parameter_name(properties)
254
+ parameter_class = _select_parameter_class(properties, _variants)
255
+ definition = parameter_class(**properties)
256
+ return Parameter(
257
+ definition,
258
+ empty=empty,
259
+ _name_is_explicit=name_is_explicit,
260
+ )
261
+
262
+
263
+ def _unwrap_parameter(value: Parameter | SchemaBase) -> SchemaBase:
264
+ return value.param if isinstance(value, Parameter) else value
265
+
266
+
267
+ __all__ = ["Parameter"]