moops 0.13.3__tar.gz → 0.13.4__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.
- {moops-0.13.3 → moops-0.13.4}/PKG-INFO +13 -2
- {moops-0.13.3 → moops-0.13.4}/README.md +9 -1
- {moops-0.13.3 → moops-0.13.4}/pyproject.toml +5 -1
- {moops-0.13.3 → moops-0.13.4}/src/moops/_list_options.py +13 -1
- moops-0.13.4/src/moops/_status.py +265 -0
- {moops-0.13.3 → moops-0.13.4}/src/moops/group.py +43 -0
- {moops-0.13.3 → moops-0.13.4}/src/moops/__init__.py +0 -0
- {moops-0.13.3 → moops-0.13.4}/src/moops/_choice_options.py +0 -0
- {moops-0.13.3 → moops-0.13.4}/src/moops/_control_mirroring.py +0 -0
- {moops-0.13.3 → moops-0.13.4}/src/moops/_custom_element.py +0 -0
- {moops-0.13.3 → moops-0.13.4}/src/moops/_embed.py +0 -0
- {moops-0.13.3 → moops-0.13.4}/src/moops/_input_map.py +0 -0
- {moops-0.13.3 → moops-0.13.4}/src/moops/_interface_utils.py +0 -0
- {moops-0.13.3 → moops-0.13.4}/src/moops/_list_controls.py +0 -0
- {moops-0.13.3 → moops-0.13.4}/src/moops/_marimo_controls.py +0 -0
- {moops-0.13.3 → moops-0.13.4}/src/moops/_markdown.py +0 -0
- {moops-0.13.3 → moops-0.13.4}/src/moops/_naming.py +0 -0
- {moops-0.13.3 → moops-0.13.4}/src/moops/_options.py +0 -0
- {moops-0.13.3 → moops-0.13.4}/src/moops/_parse.py +0 -0
- {moops-0.13.3 → moops-0.13.4}/src/moops/_preset_state.py +0 -0
- {moops-0.13.3 → moops-0.13.4}/src/moops/_presets_ui.py +0 -0
- {moops-0.13.3 → moops-0.13.4}/src/moops/_query_params.py +0 -0
- {moops-0.13.3 → moops-0.13.4}/src/moops/_run.py +0 -0
- {moops-0.13.3 → moops-0.13.4}/src/moops/_run_button.py +0 -0
- {moops-0.13.3 → moops-0.13.4}/src/moops/_subgroup_registry.py +0 -0
- {moops-0.13.3 → moops-0.13.4}/src/moops/_terminal_graphics.py +0 -0
- {moops-0.13.3 → moops-0.13.4}/src/moops/_text_wrap.py +0 -0
- {moops-0.13.3 → moops-0.13.4}/src/moops/_ui_workarounds.py +0 -0
- {moops-0.13.3 → moops-0.13.4}/src/moops/_value_resolution.py +0 -0
- {moops-0.13.3 → moops-0.13.4}/src/moops/_variant.py +0 -0
- {moops-0.13.3 → moops-0.13.4}/src/moops/interface.py +0 -0
- {moops-0.13.3 → moops-0.13.4}/src/moops/presets.py +0 -0
- {moops-0.13.3 → moops-0.13.4}/src/moops/workarounds.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: moops
|
|
3
|
-
Version: 0.13.
|
|
3
|
+
Version: 0.13.4
|
|
4
4
|
Summary: Write Marimo notebooks that also work as CLI scripts, with unified UI controls
|
|
5
5
|
Keywords: marimo,notebooks,cli,testing
|
|
6
6
|
Author: Yair Chuchem
|
|
@@ -13,12 +13,15 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
15
|
Requires-Dist: marimo>=0.23.1
|
|
16
|
+
Requires-Dist: tqdm>=4.67.1 ; extra == 'status'
|
|
16
17
|
Requires-Dist: tabulate>=0.10.0 ; extra == 'table'
|
|
17
18
|
Requires-Dist: hypothesis ; extra == 'test'
|
|
18
19
|
Requires-Python: >=3.10
|
|
19
20
|
Project-URL: Homepage, https://github.com/yairchu/moops
|
|
20
21
|
Project-URL: Repository, https://github.com/yairchu/moops
|
|
21
22
|
Project-URL: Issues, https://github.com/yairchu/moops/issues
|
|
23
|
+
Project-URL: Agent skill, https://github.com/yairchu/moops/tree/main/skills/migrate-marimo-to-moops
|
|
24
|
+
Provides-Extra: status
|
|
22
25
|
Provides-Extra: table
|
|
23
26
|
Provides-Extra: test
|
|
24
27
|
Description-Content-Type: text/markdown
|
|
@@ -47,6 +50,14 @@ Using `moops`, both implementations are merged into one.
|
|
|
47
50
|
|
|
48
51
|
Now your notebook doubles as a CLI script
|
|
49
52
|
|
|
53
|
+
## Agent-assisted migrations
|
|
54
|
+
|
|
55
|
+
If you use a coding agent, moops includes an agent skill for migrating existing
|
|
56
|
+
marimo notebooks: [`migrate-marimo-to-moops`](skills/migrate-marimo-to-moops/SKILL.md).
|
|
57
|
+
|
|
58
|
+
Install or point your agent at that skill, then ask it to migrate a notebook to
|
|
59
|
+
moops while preserving the notebook's behavior.
|
|
60
|
+
|
|
50
61
|
## Running notebooks from Python
|
|
51
62
|
|
|
52
63
|
Notebooks can also be called from Python with `moops.run`.
|
|
@@ -196,7 +207,7 @@ def test_name_casing_preserves_alphanumeric_count(kwargs):
|
|
|
196
207
|
|
|
197
208
|
The `examples/` directory is grouped by topic:
|
|
198
209
|
|
|
199
|
-
* `basics/` — small notebooks covering options, flags, and
|
|
210
|
+
* `basics/` — small notebooks covering options, flags, file inputs, and status output
|
|
200
211
|
* `custom_controls/` — `args.custom()` and mirroring it via `controls_from`
|
|
201
212
|
* `composition/` — embedding and varying notebooks (`embed`, `variant_embed`)
|
|
202
213
|
* `game_of_life/` — a worked multi-notebook example
|
|
@@ -22,6 +22,14 @@ Using `moops`, both implementations are merged into one.
|
|
|
22
22
|
|
|
23
23
|
Now your notebook doubles as a CLI script
|
|
24
24
|
|
|
25
|
+
## Agent-assisted migrations
|
|
26
|
+
|
|
27
|
+
If you use a coding agent, moops includes an agent skill for migrating existing
|
|
28
|
+
marimo notebooks: [`migrate-marimo-to-moops`](skills/migrate-marimo-to-moops/SKILL.md).
|
|
29
|
+
|
|
30
|
+
Install or point your agent at that skill, then ask it to migrate a notebook to
|
|
31
|
+
moops while preserving the notebook's behavior.
|
|
32
|
+
|
|
25
33
|
## Running notebooks from Python
|
|
26
34
|
|
|
27
35
|
Notebooks can also be called from Python with `moops.run`.
|
|
@@ -171,7 +179,7 @@ def test_name_casing_preserves_alphanumeric_count(kwargs):
|
|
|
171
179
|
|
|
172
180
|
The `examples/` directory is grouped by topic:
|
|
173
181
|
|
|
174
|
-
* `basics/` — small notebooks covering options, flags, and
|
|
182
|
+
* `basics/` — small notebooks covering options, flags, file inputs, and status output
|
|
175
183
|
* `custom_controls/` — `args.custom()` and mirroring it via `controls_from`
|
|
176
184
|
* `composition/` — embedding and varying notebooks (`embed`, `variant_embed`)
|
|
177
185
|
* `game_of_life/` — a worked multi-notebook example
|
|
@@ -4,7 +4,7 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "moops"
|
|
7
|
-
version = "0.13.
|
|
7
|
+
version = "0.13.4"
|
|
8
8
|
description = "Write Marimo notebooks that also work as CLI scripts, with unified UI controls"
|
|
9
9
|
license = "MIT"
|
|
10
10
|
readme = "README.md"
|
|
@@ -31,11 +31,14 @@ dependencies = [
|
|
|
31
31
|
test = ["hypothesis"]
|
|
32
32
|
# Group.table() uses pandas.to_markdown(), which requires tabulate for CLI output.
|
|
33
33
|
table = ["tabulate>=0.10.0"]
|
|
34
|
+
# Group.progress_bar() and Group.spinner() use tqdm on the CLI when installed.
|
|
35
|
+
status = ["tqdm>=4.67.1"]
|
|
34
36
|
|
|
35
37
|
[project.urls]
|
|
36
38
|
Homepage = "https://github.com/yairchu/moops"
|
|
37
39
|
Repository = "https://github.com/yairchu/moops"
|
|
38
40
|
Issues = "https://github.com/yairchu/moops/issues"
|
|
41
|
+
"Agent skill" = "https://github.com/yairchu/moops/tree/main/skills/migrate-marimo-to-moops"
|
|
39
42
|
|
|
40
43
|
[dependency-groups]
|
|
41
44
|
dev = [
|
|
@@ -47,6 +50,7 @@ dev = [
|
|
|
47
50
|
"pylint>=3.0",
|
|
48
51
|
"pymarkdownlnt>=0.9.37",
|
|
49
52
|
"matplotlib>=3.10.9",
|
|
53
|
+
"tqdm>=4.67.1",
|
|
50
54
|
"xarray>=2025.6.1",
|
|
51
55
|
]
|
|
52
56
|
|
|
@@ -81,6 +81,13 @@ def _build_list_ui(
|
|
|
81
81
|
if 0 <= idx < len(current):
|
|
82
82
|
set_items([*current[:idx], *current[idx + 1 :]])
|
|
83
83
|
|
|
84
|
+
def duplicate_at(idx: int) -> None:
|
|
85
|
+
current = current_items()
|
|
86
|
+
if 0 <= idx < len(current):
|
|
87
|
+
set_items(
|
|
88
|
+
[*current[: idx + 1], copy.deepcopy(current[idx]), *current[idx + 1 :]]
|
|
89
|
+
)
|
|
90
|
+
|
|
84
91
|
def move(idx: int, delta: int) -> None:
|
|
85
92
|
current = current_items()
|
|
86
93
|
target = idx + delta
|
|
@@ -97,6 +104,11 @@ def _build_list_ui(
|
|
|
97
104
|
tooltip="Insert an item here",
|
|
98
105
|
on_click=lambda _, idx=i: insert_at(idx),
|
|
99
106
|
)
|
|
107
|
+
duplicate_btn = mo.ui.button(
|
|
108
|
+
label="⧉",
|
|
109
|
+
tooltip="Duplicate this item",
|
|
110
|
+
on_click=lambda _, idx=i: duplicate_at(idx),
|
|
111
|
+
)
|
|
100
112
|
up_btn = mo.ui.button(
|
|
101
113
|
label="↑",
|
|
102
114
|
tooltip="Move up",
|
|
@@ -116,7 +128,7 @@ def _build_list_ui(
|
|
|
116
128
|
on_click=lambda _, idx=i: remove_at(idx),
|
|
117
129
|
)
|
|
118
130
|
controls = mo.hstack(
|
|
119
|
-
[insert_btn, up_btn, down_btn, item_remove_btn],
|
|
131
|
+
[insert_btn, duplicate_btn, up_btn, down_btn, item_remove_btn],
|
|
120
132
|
justify="start",
|
|
121
133
|
)
|
|
122
134
|
rows.append(mo.vstack([controls, element]))
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import collections.abc
|
|
4
|
+
import itertools
|
|
5
|
+
import sys
|
|
6
|
+
import threading
|
|
7
|
+
import typing
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
from tqdm.auto import tqdm
|
|
11
|
+
except ImportError:
|
|
12
|
+
tqdm = None # type: ignore[assignment]
|
|
13
|
+
|
|
14
|
+
S = typing.TypeVar("S")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DisabledSpinner:
|
|
18
|
+
def __enter__(self) -> DisabledSpinner:
|
|
19
|
+
return self
|
|
20
|
+
|
|
21
|
+
def __exit__(
|
|
22
|
+
self,
|
|
23
|
+
exc_type: type[BaseException] | None,
|
|
24
|
+
exc_value: BaseException | None,
|
|
25
|
+
traceback: typing.Any,
|
|
26
|
+
) -> None:
|
|
27
|
+
del exc_type, exc_value, traceback
|
|
28
|
+
|
|
29
|
+
def update(
|
|
30
|
+
self,
|
|
31
|
+
*,
|
|
32
|
+
title: str | None = None,
|
|
33
|
+
subtitle: str | None = None,
|
|
34
|
+
) -> None:
|
|
35
|
+
del title, subtitle
|
|
36
|
+
|
|
37
|
+
def close(self) -> None:
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ProgressBar(typing.Generic[S]):
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
collection: (
|
|
45
|
+
collections.abc.Collection[S]
|
|
46
|
+
| collections.abc.Iterator[S]
|
|
47
|
+
| collections.abc.AsyncIterable[S]
|
|
48
|
+
| None
|
|
49
|
+
) = None,
|
|
50
|
+
*,
|
|
51
|
+
title: str | None = None,
|
|
52
|
+
subtitle: str | None = None,
|
|
53
|
+
completion_title: str | None = None,
|
|
54
|
+
completion_subtitle: str | None = None,
|
|
55
|
+
total: int | None = None,
|
|
56
|
+
remove_on_exit: bool = False,
|
|
57
|
+
disabled: bool = False,
|
|
58
|
+
**kwargs: typing.Any,
|
|
59
|
+
) -> None:
|
|
60
|
+
explicit_total = total is not None
|
|
61
|
+
if collection is not None and total is None:
|
|
62
|
+
if not isinstance(collection, collections.abc.Sized):
|
|
63
|
+
raise TypeError("Cannot determine length; pass total")
|
|
64
|
+
total = len(collection)
|
|
65
|
+
if total is None:
|
|
66
|
+
raise ValueError("total is required")
|
|
67
|
+
self._collection = collection
|
|
68
|
+
self._title = title
|
|
69
|
+
self._subtitle = subtitle
|
|
70
|
+
self._completion_title = completion_title
|
|
71
|
+
self._completion_subtitle = completion_subtitle
|
|
72
|
+
self._total = total
|
|
73
|
+
self._current = 0
|
|
74
|
+
self._step = (
|
|
75
|
+
collection.step if explicit_total and isinstance(collection, range) else 1
|
|
76
|
+
)
|
|
77
|
+
self._disabled = disabled
|
|
78
|
+
self._closed = False
|
|
79
|
+
self._bar = (
|
|
80
|
+
None
|
|
81
|
+
if tqdm is None
|
|
82
|
+
else tqdm(
|
|
83
|
+
total=total,
|
|
84
|
+
desc=self._desc(),
|
|
85
|
+
disable=disabled,
|
|
86
|
+
leave=not remove_on_exit,
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
if self._bar is None:
|
|
90
|
+
self._emit()
|
|
91
|
+
|
|
92
|
+
def __iter__(self) -> collections.abc.Iterator[S]:
|
|
93
|
+
if self._collection is None:
|
|
94
|
+
raise RuntimeError("progress_bar needs a collection to iterate")
|
|
95
|
+
if isinstance(self._collection, collections.abc.AsyncIterable):
|
|
96
|
+
raise RuntimeError("Use async for with async collections")
|
|
97
|
+
try:
|
|
98
|
+
for item in self._collection:
|
|
99
|
+
yield item
|
|
100
|
+
self.update(increment=self._step)
|
|
101
|
+
finally:
|
|
102
|
+
self.close()
|
|
103
|
+
|
|
104
|
+
async def __aiter__(self) -> collections.abc.AsyncIterator[S]:
|
|
105
|
+
if self._collection is None:
|
|
106
|
+
raise RuntimeError("progress_bar needs a collection to iterate")
|
|
107
|
+
if not isinstance(self._collection, collections.abc.AsyncIterable):
|
|
108
|
+
raise RuntimeError("Use for with sync collections")
|
|
109
|
+
try:
|
|
110
|
+
async for item in self._collection:
|
|
111
|
+
yield item
|
|
112
|
+
self.update()
|
|
113
|
+
finally:
|
|
114
|
+
self.close()
|
|
115
|
+
|
|
116
|
+
def __enter__(self) -> ProgressBar[S]:
|
|
117
|
+
return self
|
|
118
|
+
|
|
119
|
+
def __exit__(
|
|
120
|
+
self,
|
|
121
|
+
exc_type: type[BaseException] | None,
|
|
122
|
+
exc_value: BaseException | None,
|
|
123
|
+
traceback: typing.Any,
|
|
124
|
+
) -> None:
|
|
125
|
+
del exc_type, exc_value, traceback
|
|
126
|
+
self.close()
|
|
127
|
+
|
|
128
|
+
def update(
|
|
129
|
+
self,
|
|
130
|
+
*,
|
|
131
|
+
increment: int = 1,
|
|
132
|
+
title: str | None = None,
|
|
133
|
+
subtitle: str | None = None,
|
|
134
|
+
) -> None:
|
|
135
|
+
self._current += increment
|
|
136
|
+
self._title = self._title if title is None else title
|
|
137
|
+
self._subtitle = self._subtitle if subtitle is None else subtitle
|
|
138
|
+
if self._bar is not None:
|
|
139
|
+
self._bar.set_description_str(self._desc(), refresh=False)
|
|
140
|
+
self._bar.update(increment)
|
|
141
|
+
else:
|
|
142
|
+
self._emit()
|
|
143
|
+
|
|
144
|
+
def close(self) -> None:
|
|
145
|
+
if self._closed:
|
|
146
|
+
return
|
|
147
|
+
self._closed = True
|
|
148
|
+
if self._completion_title is not None or self._completion_subtitle is not None:
|
|
149
|
+
self.update(
|
|
150
|
+
increment=0,
|
|
151
|
+
title=self._completion_title,
|
|
152
|
+
subtitle=self._completion_subtitle,
|
|
153
|
+
)
|
|
154
|
+
if self._bar is not None:
|
|
155
|
+
self._bar.close()
|
|
156
|
+
|
|
157
|
+
def _desc(self) -> str:
|
|
158
|
+
label = self._title or "Progress"
|
|
159
|
+
detail = f" - {self._subtitle}" if self._subtitle else ""
|
|
160
|
+
return f"{label}{detail}"
|
|
161
|
+
|
|
162
|
+
def _emit(self) -> None:
|
|
163
|
+
if not self._disabled:
|
|
164
|
+
print(f"{self._desc()}: {self._current}/{self._total}")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class Spinner:
|
|
168
|
+
def __init__(
|
|
169
|
+
self,
|
|
170
|
+
title: str | None = None,
|
|
171
|
+
subtitle: str | None = None,
|
|
172
|
+
remove_on_exit: bool = True,
|
|
173
|
+
*,
|
|
174
|
+
disabled: bool = False,
|
|
175
|
+
) -> None:
|
|
176
|
+
self._title = title
|
|
177
|
+
self._subtitle = subtitle
|
|
178
|
+
self._remove_on_exit = remove_on_exit
|
|
179
|
+
self._disabled = disabled
|
|
180
|
+
self._stream = sys.stderr
|
|
181
|
+
self._closed = False
|
|
182
|
+
self._rendered_len = 0
|
|
183
|
+
self._frames = itertools.cycle("-\\|/")
|
|
184
|
+
self._stop = threading.Event()
|
|
185
|
+
self._thread: threading.Thread | None = None
|
|
186
|
+
if self._can_animate():
|
|
187
|
+
self._render_frame(next(self._frames))
|
|
188
|
+
self._thread = threading.Thread(target=self._animate, daemon=True)
|
|
189
|
+
self._thread.start()
|
|
190
|
+
else:
|
|
191
|
+
self._emit()
|
|
192
|
+
|
|
193
|
+
def __enter__(self) -> Spinner:
|
|
194
|
+
return self
|
|
195
|
+
|
|
196
|
+
def __exit__(
|
|
197
|
+
self,
|
|
198
|
+
exc_type: type[BaseException] | None,
|
|
199
|
+
exc_value: BaseException | None,
|
|
200
|
+
traceback: typing.Any,
|
|
201
|
+
) -> None:
|
|
202
|
+
del exc_type, exc_value, traceback
|
|
203
|
+
self.close()
|
|
204
|
+
|
|
205
|
+
def close(self) -> None:
|
|
206
|
+
if self._closed:
|
|
207
|
+
return
|
|
208
|
+
self._closed = True
|
|
209
|
+
if self._thread is not None:
|
|
210
|
+
self._stop.set()
|
|
211
|
+
self._thread.join(timeout=0.5)
|
|
212
|
+
if self._remove_on_exit:
|
|
213
|
+
self._clear_line()
|
|
214
|
+
else:
|
|
215
|
+
self._write_line(self._desc(), newline=True)
|
|
216
|
+
return
|
|
217
|
+
if not self._remove_on_exit:
|
|
218
|
+
print("Done")
|
|
219
|
+
|
|
220
|
+
def update(
|
|
221
|
+
self,
|
|
222
|
+
*,
|
|
223
|
+
title: str | None = None,
|
|
224
|
+
subtitle: str | None = None,
|
|
225
|
+
) -> None:
|
|
226
|
+
self._title = self._title if title is None else title
|
|
227
|
+
self._subtitle = self._subtitle if subtitle is None else subtitle
|
|
228
|
+
if self._thread is not None:
|
|
229
|
+
self._render_frame(next(self._frames))
|
|
230
|
+
else:
|
|
231
|
+
self._emit()
|
|
232
|
+
|
|
233
|
+
def _desc(self) -> str:
|
|
234
|
+
label = self._title or "Working"
|
|
235
|
+
detail = f" - {self._subtitle}" if self._subtitle else ""
|
|
236
|
+
return f"{label}{detail}"
|
|
237
|
+
|
|
238
|
+
def _emit(self) -> None:
|
|
239
|
+
if not self._disabled:
|
|
240
|
+
print(self._desc())
|
|
241
|
+
|
|
242
|
+
def _can_animate(self) -> bool:
|
|
243
|
+
return (
|
|
244
|
+
not self._disabled
|
|
245
|
+
and hasattr(self._stream, "isatty")
|
|
246
|
+
and self._stream.isatty()
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
def _animate(self) -> None:
|
|
250
|
+
while not self._stop.wait(0.1):
|
|
251
|
+
self._render_frame(next(self._frames))
|
|
252
|
+
|
|
253
|
+
def _render_frame(self, frame: str) -> None:
|
|
254
|
+
self._write_line(f"{frame} {self._desc()}")
|
|
255
|
+
|
|
256
|
+
def _write_line(self, text: str, *, newline: bool = False) -> None:
|
|
257
|
+
padded = text.ljust(self._rendered_len)
|
|
258
|
+
self._stream.write("\r" + padded + ("\n" if newline else ""))
|
|
259
|
+
self._stream.flush()
|
|
260
|
+
self._rendered_len = max(self._rendered_len, len(text))
|
|
261
|
+
|
|
262
|
+
def _clear_line(self) -> None:
|
|
263
|
+
if self._rendered_len:
|
|
264
|
+
self._stream.write("\r" + (" " * self._rendered_len) + "\r")
|
|
265
|
+
self._stream.flush()
|
|
@@ -21,6 +21,7 @@ from . import (
|
|
|
21
21
|
_parse,
|
|
22
22
|
_preset_state,
|
|
23
23
|
_query_params,
|
|
24
|
+
_status,
|
|
24
25
|
_terminal_graphics,
|
|
25
26
|
_value_resolution,
|
|
26
27
|
_variant,
|
|
@@ -33,6 +34,7 @@ from ._ui_workarounds import FileBrowserWithInitialSelection
|
|
|
33
34
|
from .presets import Presets
|
|
34
35
|
|
|
35
36
|
Numeric = int | float
|
|
37
|
+
S = typing.TypeVar("S")
|
|
36
38
|
|
|
37
39
|
|
|
38
40
|
class OutputMode(enum.Enum):
|
|
@@ -410,6 +412,47 @@ class Group:
|
|
|
410
412
|
print()
|
|
411
413
|
return None
|
|
412
414
|
|
|
415
|
+
def progress_bar(
|
|
416
|
+
self,
|
|
417
|
+
collection: typing.Collection[S]
|
|
418
|
+
| typing.Iterator[S]
|
|
419
|
+
| typing.AsyncIterable[S]
|
|
420
|
+
| None = None,
|
|
421
|
+
*,
|
|
422
|
+
disabled: bool = False,
|
|
423
|
+
**kwargs: typing.Any,
|
|
424
|
+
) -> typing.Any:
|
|
425
|
+
"""Show progress in notebooks or plain status lines on the CLI."""
|
|
426
|
+
return typing.cast(
|
|
427
|
+
typing.Any,
|
|
428
|
+
mo.status.progress_bar
|
|
429
|
+
if self.output_mode is OutputMode.NOTEBOOK
|
|
430
|
+
else _status.ProgressBar,
|
|
431
|
+
)(
|
|
432
|
+
collection,
|
|
433
|
+
disabled=disabled or self.is_interface_query or self.output_mode is None,
|
|
434
|
+
**kwargs,
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
def spinner(
|
|
438
|
+
self,
|
|
439
|
+
title: str | None = None,
|
|
440
|
+
subtitle: str | None = None,
|
|
441
|
+
remove_on_exit: bool = True,
|
|
442
|
+
) -> typing.Any:
|
|
443
|
+
"""Show a spinner in notebooks or a plain status line on the CLI."""
|
|
444
|
+
if self.is_interface_query or self.output_mode is None:
|
|
445
|
+
return _status.DisabledSpinner()
|
|
446
|
+
return (
|
|
447
|
+
mo.status.spinner
|
|
448
|
+
if self.output_mode is OutputMode.NOTEBOOK
|
|
449
|
+
else _status.Spinner
|
|
450
|
+
)(
|
|
451
|
+
title=title,
|
|
452
|
+
subtitle=subtitle,
|
|
453
|
+
remove_on_exit=remove_on_exit,
|
|
454
|
+
)
|
|
455
|
+
|
|
413
456
|
def _bool_control(
|
|
414
457
|
self,
|
|
415
458
|
widget: typing.Literal["switch", "checkbox"],
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|