orion-ui 0.6.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.
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: orion-ui
3
+ Version: 0.6.0
4
+ Summary: Python authoring API for Orion-native notebook UI outputs
5
+ Author: Nicolas Fonteyne
6
+ License: Apache-2.0
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3 :: Only
9
+ Classifier: License :: OSI Approved :: Apache Software License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.8
@@ -0,0 +1,456 @@
1
+ """Python authoring API for Orion-native notebook UI outputs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __version__ = "0.6.0"
6
+
7
+ import html
8
+ import json
9
+ import uuid
10
+ from dataclasses import dataclass, field
11
+ from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence, Union
12
+
13
+ from . import _runtime, theme
14
+
15
+ ORION_UI_MIME_TYPE = "application/vnd.orion.ui+json"
16
+ StateValue = Union[str, int, float, bool]
17
+ JsonValue = Union[None, str, int, float, bool, List["JsonValue"], Dict[str, "JsonValue"]]
18
+
19
+ _COMPONENT_TYPES = {
20
+ "Page",
21
+ "Stack",
22
+ "Grid",
23
+ "Section",
24
+ "Card",
25
+ "Tabs",
26
+ "MarkdownCell",
27
+ "Output",
28
+ "Button",
29
+ "Input",
30
+ "Textarea",
31
+ "Select",
32
+ "Slider",
33
+ "Checkbox",
34
+ "Switch",
35
+ "Label",
36
+ "Badge",
37
+ "Separator",
38
+ }
39
+
40
+ _UNSET = object()
41
+
42
+
43
+ def _validate_json_value(value: Any, path: str) -> JsonValue:
44
+ """Return a JSON-compatible value or raise a helpful type error."""
45
+ if value is None or isinstance(value, (str, int, float, bool)):
46
+ return value
47
+ if isinstance(value, (list, tuple)):
48
+ return [_validate_json_value(entry, f"{path}[]") for entry in value]
49
+ if isinstance(value, Mapping):
50
+ result: Dict[str, JsonValue] = {}
51
+ for key, entry in value.items():
52
+ if not isinstance(key, str):
53
+ raise TypeError(f"{path} keys must be strings.")
54
+ result[key] = _validate_json_value(entry, f"{path}.{key}")
55
+ return result
56
+ raise TypeError(f"{path} must be JSON-serializable.")
57
+
58
+
59
+ def _props(**props: Any) -> Dict[str, JsonValue]:
60
+ """Validate and drop unset component props."""
61
+ return {
62
+ key: _validate_json_value(value, f"props.{key}")
63
+ for key, value in props.items()
64
+ if value is not None
65
+ }
66
+
67
+
68
+ def _coerce_children(children: Sequence[Any]) -> List["Component"]:
69
+ """Normalize child arguments into Component instances."""
70
+ normalized: List[Component] = []
71
+ for child in children:
72
+ if isinstance(child, Component):
73
+ normalized.append(child)
74
+ elif isinstance(child, str):
75
+ normalized.append(label(child))
76
+ else:
77
+ raise TypeError("Orion UI children must be components or strings.")
78
+ return normalized
79
+
80
+
81
+ def _value_type(value: StateValue) -> str:
82
+ """Return the Orion binding value type label for a Python value."""
83
+ if isinstance(value, bool):
84
+ return "boolean"
85
+ if isinstance(value, (int, float)):
86
+ return "number"
87
+ return "string"
88
+
89
+
90
+ @dataclass(frozen=True)
91
+ class Component:
92
+ """Declarative Orion UI component that can render as a notebook MIME bundle."""
93
+
94
+ type: str
95
+ props: Dict[str, JsonValue] = field(default_factory=dict)
96
+ children: List["Component"] = field(default_factory=list)
97
+ output_id: str = field(default_factory=lambda: f"orion-ui-{uuid.uuid4().hex}")
98
+
99
+ def __post_init__(self) -> None:
100
+ """Validate component type and JSON-compatible props."""
101
+ if self.type not in _COMPONENT_TYPES:
102
+ raise ValueError(f"Unsupported Orion UI component type: {self.type}")
103
+ _validate_json_value(self.props, "props")
104
+
105
+ def _to_node(self) -> Dict[str, JsonValue]:
106
+ """Serialize this component to the Orion UI primitive tree shape."""
107
+ return {
108
+ "type": self.type,
109
+ "props": dict(self.props),
110
+ "children": [child._to_node() for child in self.children],
111
+ }
112
+
113
+ def _collect_bindings(self) -> Dict[str, Dict[str, str]]:
114
+ """Collect Python-state bindings declared by this tree."""
115
+ bindings: Dict[str, Dict[str, str]] = {}
116
+ state_key = self.props.get("stateKey")
117
+ default_value = self.props.get("defaultValue")
118
+ if isinstance(state_key, str) and isinstance(default_value, (str, int, float, bool)):
119
+ bindings[state_key] = {
120
+ "kind": "python_state",
121
+ "valueType": _value_type(default_value),
122
+ }
123
+ for child in self.children:
124
+ bindings.update(child._collect_bindings())
125
+ return bindings
126
+
127
+ def _collect_state(self) -> Dict[str, StateValue]:
128
+ """Collect current runtime values for state keys used by this tree."""
129
+ state: Dict[str, StateValue] = {}
130
+ for key in self._collect_bindings():
131
+ value = _runtime.get_value(key)
132
+ if isinstance(value, (str, int, float, bool)):
133
+ state[key] = value
134
+ return state
135
+
136
+ def _payload(self) -> Dict[str, JsonValue]:
137
+ """Build the JSON payload consumed by Orion's MIME renderer."""
138
+ return {
139
+ "version": 1,
140
+ "id": self.output_id,
141
+ "root": self._to_node(),
142
+ "state": self._collect_state(),
143
+ "bindings": self._collect_bindings(),
144
+ }
145
+
146
+ def _repr_mimebundle_(self, include: Any = None, exclude: Any = None) -> Dict[str, Any]:
147
+ """Return Orion UI MIME plus static HTML/plain-text fallbacks."""
148
+ payload = self._payload()
149
+ return {
150
+ ORION_UI_MIME_TYPE: payload,
151
+ "text/html": self._repr_html_(),
152
+ "text/plain": repr(self),
153
+ }
154
+
155
+ def _repr_html_(self) -> str:
156
+ """Return a static fallback preview for non-Orion notebook frontends."""
157
+ return _render_static_html(self)
158
+
159
+ def __repr__(self) -> str:
160
+ """Return a concise plain-text representation for terminal contexts."""
161
+ return f"OrionUI({self.type}, id={self.output_id!r})"
162
+
163
+
164
+ def _component(component_type: str, children: Sequence[Any] = (), **props: Any) -> Component:
165
+ """Create a validated Orion UI component."""
166
+ return Component(component_type, _props(**props), _coerce_children(children))
167
+
168
+
169
+ def _control(
170
+ component_type: str,
171
+ key: str,
172
+ default_value: StateValue,
173
+ value: Any = _UNSET,
174
+ **props: Any,
175
+ ) -> Component:
176
+ """Create a state-bound control using default or forced value semantics."""
177
+ if not isinstance(key, str) or not key:
178
+ raise ValueError("Control key must be a non-empty string.")
179
+ if value is _UNSET:
180
+ define_default(key, default_value)
181
+ else:
182
+ if not isinstance(value, (str, int, float, bool)):
183
+ raise TypeError("Control value must be a string, number, or boolean.")
184
+ _runtime.set_value(key, value)
185
+ return _component(component_type, stateKey=key, defaultValue=default_value, **props)
186
+
187
+
188
+ def page(*children: Any, gap: str = "md", padding: str = "md") -> Component:
189
+ """Create a top-level page container."""
190
+ return _component("Page", children, gap=gap, padding=padding)
191
+
192
+
193
+ def stack(*children: Any, gap: str = "md", align: str = "stretch") -> Component:
194
+ """Create a vertical stack layout."""
195
+ return _component("Stack", children, gap=gap, align=align)
196
+
197
+
198
+ def grid(*children: Any, columns: int = 2, gap: str = "md") -> Component:
199
+ """Create a responsive grid layout."""
200
+ return _component("Grid", children, columns=columns, gap=gap)
201
+
202
+
203
+ def section(
204
+ *children: Any,
205
+ title: Optional[str] = None,
206
+ description: Optional[str] = None,
207
+ gap: str = "md",
208
+ padding: str = "md",
209
+ ) -> Component:
210
+ """Create a titled section."""
211
+ return _component(
212
+ "Section",
213
+ children,
214
+ title=title,
215
+ description=description,
216
+ gap=gap,
217
+ padding=padding,
218
+ )
219
+
220
+
221
+ def card(
222
+ *children: Any,
223
+ title: Optional[str] = None,
224
+ description: Optional[str] = None,
225
+ gap: str = "md",
226
+ ) -> Component:
227
+ """Create a card container."""
228
+ return _component("Card", children, title=title, description=description, gap=gap)
229
+
230
+
231
+ def tabs(*children: Any, default_value: Optional[str] = None) -> Component:
232
+ """Create a tab container whose children act as panels."""
233
+ return _component("Tabs", children, defaultValue=default_value)
234
+
235
+
236
+ def button(
237
+ label: str,
238
+ *,
239
+ action: Optional[Mapping[str, Any]] = None,
240
+ variant: Optional[str] = None,
241
+ size: Optional[str] = None,
242
+ ) -> Component:
243
+ """Create a button, optionally with an Orion declarative action."""
244
+ return _component("Button", label=label, action=action, variant=variant, size=size)
245
+
246
+
247
+ def input(
248
+ key: str,
249
+ *,
250
+ label: Optional[str] = None,
251
+ default_value: str = "",
252
+ value: Any = _UNSET,
253
+ placeholder: Optional[str] = None,
254
+ input_type: str = "text",
255
+ ) -> Component:
256
+ """Create a text input bound to Python state."""
257
+ return _control(
258
+ "Input",
259
+ key,
260
+ default_value,
261
+ value,
262
+ label=label,
263
+ placeholder=placeholder,
264
+ inputType=input_type,
265
+ )
266
+
267
+
268
+ def textarea(
269
+ key: str,
270
+ *,
271
+ label: Optional[str] = None,
272
+ default_value: str = "",
273
+ value: Any = _UNSET,
274
+ placeholder: Optional[str] = None,
275
+ ) -> Component:
276
+ """Create a textarea bound to Python state."""
277
+ return _control("Textarea", key, default_value, value, label=label, placeholder=placeholder)
278
+
279
+
280
+ def select(
281
+ key: str,
282
+ options: Iterable[Union[str, Mapping[str, str]]],
283
+ *,
284
+ label: Optional[str] = None,
285
+ default_value: Optional[str] = None,
286
+ value: Any = _UNSET,
287
+ placeholder: Optional[str] = None,
288
+ ) -> Component:
289
+ """Create a select control bound to Python state."""
290
+ option_list = list(options)
291
+ initial = default_value
292
+ if initial is None:
293
+ first = option_list[0] if option_list else ""
294
+ initial = first if isinstance(first, str) else first.get("value", "")
295
+ return _control(
296
+ "Select",
297
+ key,
298
+ initial,
299
+ value,
300
+ label=label,
301
+ options=option_list,
302
+ placeholder=placeholder,
303
+ )
304
+
305
+
306
+ def slider(
307
+ key: str,
308
+ *,
309
+ label: Optional[str] = None,
310
+ min: Union[int, float] = 0,
311
+ max: Union[int, float] = 100,
312
+ default_value: Union[int, float] = 0,
313
+ value: Any = _UNSET,
314
+ step: Union[int, float] = 1,
315
+ ) -> Component:
316
+ """Create a numeric slider bound to Python state."""
317
+ return _control(
318
+ "Slider",
319
+ key,
320
+ default_value,
321
+ value,
322
+ label=label,
323
+ min=min,
324
+ max=max,
325
+ step=step,
326
+ )
327
+
328
+
329
+ def checkbox(
330
+ key: str,
331
+ *,
332
+ label: Optional[str] = None,
333
+ default_value: bool = False,
334
+ value: Any = _UNSET,
335
+ ) -> Component:
336
+ """Create a checkbox bound to Python state."""
337
+ return _control("Checkbox", key, default_value, value, label=label)
338
+
339
+
340
+ def switch(
341
+ key: str,
342
+ *,
343
+ label: Optional[str] = None,
344
+ default_value: bool = False,
345
+ value: Any = _UNSET,
346
+ ) -> Component:
347
+ """Create a switch bound to Python state."""
348
+ return _control("Switch", key, default_value, value, label=label)
349
+
350
+
351
+ def label(text: str) -> Component:
352
+ """Create a text label."""
353
+ return _component("Label", text=text)
354
+
355
+
356
+ def badge(text: str, *, variant: Optional[str] = None) -> Component:
357
+ """Create a status badge."""
358
+ return _component("Badge", text=text, variant=variant)
359
+
360
+
361
+ def separator() -> Component:
362
+ """Create a horizontal separator."""
363
+ return _component("Separator")
364
+
365
+
366
+ def markdown_cell(cell_id: Optional[str] = None, *, text: Optional[str] = None) -> Component:
367
+ """Reference a markdown cell by Orion cell id, or render inline markdown text."""
368
+ return _component("MarkdownCell", cellId=cell_id, text=text)
369
+
370
+
371
+ def output(cell_id: str, output_index: int = 0) -> Component:
372
+ """Reference a notebook output by Orion cell id and zero-based output index."""
373
+ return _component("Output", cellId=cell_id, outputIndex=output_index)
374
+
375
+
376
+ def get(key: str, default: Optional[StateValue] = None) -> Optional[StateValue]:
377
+ """Return an Orion UI control value from Python runtime state."""
378
+ return _runtime.get_value(key, default)
379
+
380
+
381
+ def define_default(key: str, default: StateValue) -> StateValue:
382
+ """Define a default value without replacing an existing user-selected value."""
383
+ return _runtime.define_default(key, default)
384
+
385
+
386
+ def set(key: str, value: StateValue) -> None:
387
+ """Set an Orion UI control value in Python runtime state."""
388
+ _runtime.set_value(key, value)
389
+
390
+
391
+ def state() -> Dict[str, StateValue]:
392
+ """Return a shallow copy of Orion UI runtime state."""
393
+ return _runtime.state()
394
+
395
+
396
+ def _render_static_html(component: Component) -> str:
397
+ """Render a small static HTML fallback for non-Orion notebook frontends."""
398
+ title = html.escape(str(component.props.get("title", "")))
399
+ label_text = html.escape(str(component.props.get("label", component.props.get("text", ""))))
400
+ children = "".join(_render_static_html(child) for child in component.children)
401
+ shell_start = (
402
+ "<div style='border:1px solid #d4d4d8;border-radius:8px;padding:12px;"
403
+ "font-family:Inter,system-ui,sans-serif;margin:8px 0;'>"
404
+ )
405
+
406
+ if component.type == "Card":
407
+ header = f"<strong>{title}</strong>" if title else ""
408
+ return f"{shell_start}{header}<div>{children}</div></div>"
409
+ if component.type in {"Stack", "Page", "Section", "Grid", "Tabs"}:
410
+ header = f"<strong>{title}</strong>" if title else ""
411
+ return f"<div style='display:flex;flex-direction:column;gap:8px'>{header}{children}</div>"
412
+ if component.type == "Button":
413
+ return f"<button type='button'>{html.escape(str(component.props.get('label', 'Button')))}</button>"
414
+ if component.type in {"Input", "Textarea", "Select", "Slider", "Checkbox", "Switch"}:
415
+ value = html.escape(str(component.props.get("defaultValue", "")))
416
+ return f"<div><strong>{label_text}</strong>: <code>{value}</code></div>"
417
+ if component.type == "Badge":
418
+ return f"<span style='border:1px solid #d4d4d8;border-radius:999px;padding:2px 8px'>{label_text}</span>"
419
+ if component.type == "Label":
420
+ return f"<span>{label_text}</span>"
421
+ if component.type == "Separator":
422
+ return "<hr />"
423
+ if component.type == "MarkdownCell":
424
+ return f"<div>{html.escape(str(component.props.get('text', 'Markdown cell')))}</div>"
425
+ if component.type == "Output":
426
+ return "<div>Notebook output reference</div>"
427
+ return children
428
+
429
+
430
+ __all__ = [
431
+ "Component",
432
+ "ORION_UI_MIME_TYPE",
433
+ "badge",
434
+ "button",
435
+ "card",
436
+ "checkbox",
437
+ "define_default",
438
+ "get",
439
+ "grid",
440
+ "input",
441
+ "label",
442
+ "markdown_cell",
443
+ "output",
444
+ "page",
445
+ "section",
446
+ "select",
447
+ "separator",
448
+ "set",
449
+ "slider",
450
+ "stack",
451
+ "state",
452
+ "switch",
453
+ "tabs",
454
+ "textarea",
455
+ "theme",
456
+ ]
@@ -0,0 +1,49 @@
1
+ """Runtime state store for Orion UI notebook controls."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Dict, Optional, Union
6
+
7
+ StateValue = Union[str, int, float, bool]
8
+
9
+ _STATE: Dict[str, StateValue] = {}
10
+ _OUTPUT_STATE: Dict[str, Dict[str, StateValue]] = {}
11
+
12
+
13
+ def set_value(key: str, value: StateValue, output_id: Optional[str] = None) -> None:
14
+ """Set a control value in the process-local Orion UI state store."""
15
+ if not isinstance(key, str) or not key:
16
+ raise ValueError("Orion UI state key must be a non-empty string.")
17
+ if not isinstance(value, (str, int, float, bool)):
18
+ raise TypeError("Orion UI state values must be strings, numbers, or booleans.")
19
+
20
+ _STATE[key] = value
21
+ if output_id:
22
+ _OUTPUT_STATE.setdefault(output_id, {})[key] = value
23
+
24
+
25
+ def define_default(key: str, default: StateValue) -> StateValue:
26
+ """Set a default value only when a control key has no runtime state yet."""
27
+ if not isinstance(key, str) or not key:
28
+ raise ValueError("Orion UI state key must be a non-empty string.")
29
+ if not isinstance(default, (str, int, float, bool)):
30
+ raise TypeError("Orion UI default values must be strings, numbers, or booleans.")
31
+
32
+ if key not in _STATE:
33
+ _STATE[key] = default
34
+ return _STATE[key]
35
+
36
+
37
+ def get_value(key: str, default: Optional[StateValue] = None) -> Optional[StateValue]:
38
+ """Return a control value from the Orion UI state store."""
39
+ return _STATE.get(key, default)
40
+
41
+
42
+ def state() -> Dict[str, StateValue]:
43
+ """Return a shallow copy of all Orion UI control state."""
44
+ return dict(_STATE)
45
+
46
+
47
+ def output_state(output_id: str) -> Dict[str, StateValue]:
48
+ """Return state updates associated with one rendered Orion UI output."""
49
+ return dict(_OUTPUT_STATE.get(output_id, {}))
@@ -0,0 +1,56 @@
1
+ """Theme helpers for third-party visualization libraries."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Dict
6
+
7
+
8
+ def plotly(name: str = "orion", set_default: bool = True) -> Dict[str, Any]:
9
+ """Register an Orion-styled Plotly template and optionally make it default."""
10
+ try:
11
+ import plotly.io as pio
12
+ except ImportError as exc:
13
+ raise ImportError(
14
+ "ui.theme.plotly() requires Plotly. Install plotly to use this helper."
15
+ ) from exc
16
+
17
+ template: Dict[str, Any] = {
18
+ "layout": {
19
+ "paper_bgcolor": "rgba(0,0,0,0)",
20
+ "plot_bgcolor": "rgba(0,0,0,0)",
21
+ "font": {"family": "Inter, ui-sans-serif, system-ui", "color": "#18181b"},
22
+ "colorway": [
23
+ "#2563eb",
24
+ "#16a34a",
25
+ "#dc2626",
26
+ "#9333ea",
27
+ "#f59e0b",
28
+ "#0891b2",
29
+ ],
30
+ "margin": {"l": 48, "r": 24, "t": 48, "b": 44},
31
+ "xaxis": {
32
+ "gridcolor": "rgba(113,113,122,0.18)",
33
+ "zerolinecolor": "rgba(113,113,122,0.24)",
34
+ "linecolor": "rgba(113,113,122,0.35)",
35
+ "ticks": "outside",
36
+ },
37
+ "yaxis": {
38
+ "gridcolor": "rgba(113,113,122,0.18)",
39
+ "zerolinecolor": "rgba(113,113,122,0.24)",
40
+ "linecolor": "rgba(113,113,122,0.35)",
41
+ "ticks": "outside",
42
+ },
43
+ "legend": {
44
+ "orientation": "h",
45
+ "yanchor": "bottom",
46
+ "y": 1.02,
47
+ "xanchor": "right",
48
+ "x": 1,
49
+ },
50
+ }
51
+ }
52
+
53
+ pio.templates[name] = template
54
+ if set_default:
55
+ pio.templates.default = name
56
+ return template
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: orion-ui
3
+ Version: 0.6.0
4
+ Summary: Python authoring API for Orion-native notebook UI outputs
5
+ Author: Nicolas Fonteyne
6
+ License: Apache-2.0
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3 :: Only
9
+ Classifier: License :: OSI Approved :: Apache Software License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.8
@@ -0,0 +1,8 @@
1
+ pyproject.toml
2
+ orion_ui/__init__.py
3
+ orion_ui/_runtime.py
4
+ orion_ui/theme.py
5
+ orion_ui.egg-info/PKG-INFO
6
+ orion_ui.egg-info/SOURCES.txt
7
+ orion_ui.egg-info/dependency_links.txt
8
+ orion_ui.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ orion_ui
@@ -0,0 +1,21 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "orion-ui"
7
+ version = "0.6.0"
8
+ description = "Python authoring API for Orion-native notebook UI outputs"
9
+ requires-python = ">=3.8"
10
+ license = { text = "Apache-2.0" }
11
+ authors = [{ name = "Nicolas Fonteyne" }]
12
+ classifiers = [
13
+ "Programming Language :: Python :: 3",
14
+ "Programming Language :: Python :: 3 :: Only",
15
+ "License :: OSI Approved :: Apache Software License",
16
+ "Operating System :: OS Independent",
17
+ ]
18
+
19
+ [tool.setuptools.packages.find]
20
+ where = ["."]
21
+ include = ["orion_ui*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+