bead 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 (231) hide show
  1. bead/__init__.py +11 -0
  2. bead/__main__.py +11 -0
  3. bead/active_learning/__init__.py +15 -0
  4. bead/active_learning/config.py +231 -0
  5. bead/active_learning/loop.py +566 -0
  6. bead/active_learning/models/__init__.py +24 -0
  7. bead/active_learning/models/base.py +852 -0
  8. bead/active_learning/models/binary.py +910 -0
  9. bead/active_learning/models/categorical.py +943 -0
  10. bead/active_learning/models/cloze.py +862 -0
  11. bead/active_learning/models/forced_choice.py +956 -0
  12. bead/active_learning/models/free_text.py +773 -0
  13. bead/active_learning/models/lora.py +365 -0
  14. bead/active_learning/models/magnitude.py +835 -0
  15. bead/active_learning/models/multi_select.py +795 -0
  16. bead/active_learning/models/ordinal_scale.py +811 -0
  17. bead/active_learning/models/peft_adapter.py +155 -0
  18. bead/active_learning/models/random_effects.py +639 -0
  19. bead/active_learning/selection.py +354 -0
  20. bead/active_learning/strategies.py +391 -0
  21. bead/active_learning/trainers/__init__.py +26 -0
  22. bead/active_learning/trainers/base.py +210 -0
  23. bead/active_learning/trainers/data_collator.py +172 -0
  24. bead/active_learning/trainers/dataset_utils.py +261 -0
  25. bead/active_learning/trainers/huggingface.py +304 -0
  26. bead/active_learning/trainers/lightning.py +324 -0
  27. bead/active_learning/trainers/metrics.py +424 -0
  28. bead/active_learning/trainers/mixed_effects.py +551 -0
  29. bead/active_learning/trainers/model_wrapper.py +509 -0
  30. bead/active_learning/trainers/registry.py +104 -0
  31. bead/adapters/__init__.py +11 -0
  32. bead/adapters/huggingface.py +61 -0
  33. bead/behavioral/__init__.py +116 -0
  34. bead/behavioral/analytics.py +646 -0
  35. bead/behavioral/extraction.py +343 -0
  36. bead/behavioral/merging.py +343 -0
  37. bead/cli/__init__.py +11 -0
  38. bead/cli/active_learning.py +513 -0
  39. bead/cli/active_learning_commands.py +779 -0
  40. bead/cli/completion.py +359 -0
  41. bead/cli/config.py +624 -0
  42. bead/cli/constraint_builders.py +286 -0
  43. bead/cli/deployment.py +859 -0
  44. bead/cli/deployment_trials.py +493 -0
  45. bead/cli/deployment_ui.py +332 -0
  46. bead/cli/display.py +378 -0
  47. bead/cli/items.py +960 -0
  48. bead/cli/items_factories.py +776 -0
  49. bead/cli/list_constraints.py +714 -0
  50. bead/cli/lists.py +490 -0
  51. bead/cli/main.py +430 -0
  52. bead/cli/models.py +877 -0
  53. bead/cli/resource_loaders.py +621 -0
  54. bead/cli/resources.py +1036 -0
  55. bead/cli/shell.py +356 -0
  56. bead/cli/simulate.py +840 -0
  57. bead/cli/templates.py +1158 -0
  58. bead/cli/training.py +1080 -0
  59. bead/cli/utils.py +614 -0
  60. bead/cli/workflow.py +1273 -0
  61. bead/config/__init__.py +68 -0
  62. bead/config/active_learning.py +1009 -0
  63. bead/config/config.py +192 -0
  64. bead/config/defaults.py +118 -0
  65. bead/config/deployment.py +217 -0
  66. bead/config/env.py +147 -0
  67. bead/config/item.py +45 -0
  68. bead/config/list.py +193 -0
  69. bead/config/loader.py +149 -0
  70. bead/config/logging.py +42 -0
  71. bead/config/model.py +49 -0
  72. bead/config/paths.py +46 -0
  73. bead/config/profiles.py +320 -0
  74. bead/config/resources.py +47 -0
  75. bead/config/serialization.py +210 -0
  76. bead/config/simulation.py +206 -0
  77. bead/config/template.py +238 -0
  78. bead/config/validation.py +267 -0
  79. bead/data/__init__.py +65 -0
  80. bead/data/base.py +87 -0
  81. bead/data/identifiers.py +97 -0
  82. bead/data/language_codes.py +61 -0
  83. bead/data/metadata.py +270 -0
  84. bead/data/range.py +123 -0
  85. bead/data/repository.py +358 -0
  86. bead/data/serialization.py +249 -0
  87. bead/data/timestamps.py +89 -0
  88. bead/data/validation.py +349 -0
  89. bead/data_collection/__init__.py +11 -0
  90. bead/data_collection/jatos.py +223 -0
  91. bead/data_collection/merger.py +154 -0
  92. bead/data_collection/prolific.py +198 -0
  93. bead/deployment/__init__.py +5 -0
  94. bead/deployment/distribution.py +402 -0
  95. bead/deployment/jatos/__init__.py +1 -0
  96. bead/deployment/jatos/api.py +200 -0
  97. bead/deployment/jatos/exporter.py +210 -0
  98. bead/deployment/jspsych/__init__.py +9 -0
  99. bead/deployment/jspsych/biome.json +44 -0
  100. bead/deployment/jspsych/config.py +411 -0
  101. bead/deployment/jspsych/generator.py +598 -0
  102. bead/deployment/jspsych/package.json +51 -0
  103. bead/deployment/jspsych/pnpm-lock.yaml +2141 -0
  104. bead/deployment/jspsych/randomizer.py +299 -0
  105. bead/deployment/jspsych/src/lib/list-distributor.test.ts +327 -0
  106. bead/deployment/jspsych/src/lib/list-distributor.ts +1282 -0
  107. bead/deployment/jspsych/src/lib/randomizer.test.ts +232 -0
  108. bead/deployment/jspsych/src/lib/randomizer.ts +367 -0
  109. bead/deployment/jspsych/src/plugins/cloze-dropdown.ts +252 -0
  110. bead/deployment/jspsych/src/plugins/forced-choice.ts +265 -0
  111. bead/deployment/jspsych/src/plugins/plugins.test.ts +141 -0
  112. bead/deployment/jspsych/src/plugins/rating.ts +248 -0
  113. bead/deployment/jspsych/src/slopit/index.ts +9 -0
  114. bead/deployment/jspsych/src/types/jatos.d.ts +256 -0
  115. bead/deployment/jspsych/src/types/jspsych.d.ts +228 -0
  116. bead/deployment/jspsych/templates/experiment.css +1 -0
  117. bead/deployment/jspsych/templates/experiment.js.template +289 -0
  118. bead/deployment/jspsych/templates/index.html +51 -0
  119. bead/deployment/jspsych/templates/randomizer.js +241 -0
  120. bead/deployment/jspsych/templates/randomizer.js.template +313 -0
  121. bead/deployment/jspsych/trials.py +723 -0
  122. bead/deployment/jspsych/tsconfig.json +23 -0
  123. bead/deployment/jspsych/tsup.config.ts +30 -0
  124. bead/deployment/jspsych/ui/__init__.py +1 -0
  125. bead/deployment/jspsych/ui/components.py +383 -0
  126. bead/deployment/jspsych/ui/styles.py +411 -0
  127. bead/dsl/__init__.py +80 -0
  128. bead/dsl/ast.py +168 -0
  129. bead/dsl/context.py +178 -0
  130. bead/dsl/errors.py +71 -0
  131. bead/dsl/evaluator.py +570 -0
  132. bead/dsl/grammar.lark +81 -0
  133. bead/dsl/parser.py +231 -0
  134. bead/dsl/stdlib.py +929 -0
  135. bead/evaluation/__init__.py +13 -0
  136. bead/evaluation/convergence.py +485 -0
  137. bead/evaluation/interannotator.py +398 -0
  138. bead/items/__init__.py +40 -0
  139. bead/items/adapters/__init__.py +70 -0
  140. bead/items/adapters/anthropic.py +224 -0
  141. bead/items/adapters/api_utils.py +167 -0
  142. bead/items/adapters/base.py +216 -0
  143. bead/items/adapters/google.py +259 -0
  144. bead/items/adapters/huggingface.py +1074 -0
  145. bead/items/adapters/openai.py +323 -0
  146. bead/items/adapters/registry.py +202 -0
  147. bead/items/adapters/sentence_transformers.py +224 -0
  148. bead/items/adapters/togetherai.py +309 -0
  149. bead/items/binary.py +515 -0
  150. bead/items/cache.py +558 -0
  151. bead/items/categorical.py +593 -0
  152. bead/items/cloze.py +757 -0
  153. bead/items/constructor.py +784 -0
  154. bead/items/forced_choice.py +413 -0
  155. bead/items/free_text.py +681 -0
  156. bead/items/generation.py +432 -0
  157. bead/items/item.py +396 -0
  158. bead/items/item_template.py +787 -0
  159. bead/items/magnitude.py +573 -0
  160. bead/items/multi_select.py +621 -0
  161. bead/items/ordinal_scale.py +569 -0
  162. bead/items/scoring.py +448 -0
  163. bead/items/validation.py +723 -0
  164. bead/lists/__init__.py +30 -0
  165. bead/lists/balancer.py +263 -0
  166. bead/lists/constraints.py +1067 -0
  167. bead/lists/experiment_list.py +286 -0
  168. bead/lists/list_collection.py +378 -0
  169. bead/lists/partitioner.py +1141 -0
  170. bead/lists/stratification.py +254 -0
  171. bead/participants/__init__.py +73 -0
  172. bead/participants/collection.py +699 -0
  173. bead/participants/merging.py +312 -0
  174. bead/participants/metadata_spec.py +491 -0
  175. bead/participants/models.py +276 -0
  176. bead/resources/__init__.py +29 -0
  177. bead/resources/adapters/__init__.py +19 -0
  178. bead/resources/adapters/base.py +104 -0
  179. bead/resources/adapters/cache.py +128 -0
  180. bead/resources/adapters/glazing.py +508 -0
  181. bead/resources/adapters/registry.py +117 -0
  182. bead/resources/adapters/unimorph.py +796 -0
  183. bead/resources/classification.py +856 -0
  184. bead/resources/constraint_builders.py +329 -0
  185. bead/resources/constraints.py +165 -0
  186. bead/resources/lexical_item.py +223 -0
  187. bead/resources/lexicon.py +744 -0
  188. bead/resources/loaders.py +209 -0
  189. bead/resources/template.py +441 -0
  190. bead/resources/template_collection.py +707 -0
  191. bead/resources/template_generation.py +349 -0
  192. bead/simulation/__init__.py +29 -0
  193. bead/simulation/annotators/__init__.py +15 -0
  194. bead/simulation/annotators/base.py +175 -0
  195. bead/simulation/annotators/distance_based.py +135 -0
  196. bead/simulation/annotators/lm_based.py +114 -0
  197. bead/simulation/annotators/oracle.py +182 -0
  198. bead/simulation/annotators/random.py +181 -0
  199. bead/simulation/dsl_extension/__init__.py +3 -0
  200. bead/simulation/noise_models/__init__.py +13 -0
  201. bead/simulation/noise_models/base.py +42 -0
  202. bead/simulation/noise_models/random_noise.py +82 -0
  203. bead/simulation/noise_models/systematic.py +132 -0
  204. bead/simulation/noise_models/temperature.py +86 -0
  205. bead/simulation/runner.py +144 -0
  206. bead/simulation/strategies/__init__.py +23 -0
  207. bead/simulation/strategies/base.py +123 -0
  208. bead/simulation/strategies/binary.py +103 -0
  209. bead/simulation/strategies/categorical.py +123 -0
  210. bead/simulation/strategies/cloze.py +224 -0
  211. bead/simulation/strategies/forced_choice.py +127 -0
  212. bead/simulation/strategies/free_text.py +105 -0
  213. bead/simulation/strategies/magnitude.py +116 -0
  214. bead/simulation/strategies/multi_select.py +129 -0
  215. bead/simulation/strategies/ordinal_scale.py +131 -0
  216. bead/templates/__init__.py +27 -0
  217. bead/templates/adapters/__init__.py +17 -0
  218. bead/templates/adapters/base.py +128 -0
  219. bead/templates/adapters/cache.py +178 -0
  220. bead/templates/adapters/huggingface.py +312 -0
  221. bead/templates/combinatorics.py +103 -0
  222. bead/templates/filler.py +605 -0
  223. bead/templates/renderers.py +177 -0
  224. bead/templates/resolver.py +178 -0
  225. bead/templates/strategies.py +1806 -0
  226. bead/templates/streaming.py +195 -0
  227. bead-0.1.0.dist-info/METADATA +212 -0
  228. bead-0.1.0.dist-info/RECORD +231 -0
  229. bead-0.1.0.dist-info/WHEEL +4 -0
  230. bead-0.1.0.dist-info/entry_points.txt +2 -0
  231. bead-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,332 @@
1
+ """UI customization commands for jsPsych deployment.
2
+
3
+ This module provides commands for customizing the appearance of jsPsych
4
+ experiments using Material Design themes and custom CSS.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+ from typing import Literal
11
+
12
+ import click
13
+ from rich.console import Console
14
+ from rich.panel import Panel
15
+
16
+ from bead.cli.utils import print_error, print_info, print_success
17
+ from bead.deployment.jspsych.ui.styles import MaterialDesignStylesheet
18
+
19
+ console = Console()
20
+
21
+
22
+ @click.group(name="ui")
23
+ def deployment_ui() -> None:
24
+ r"""UI customization commands for jsPsych experiments.
25
+
26
+ Commands for applying themes, generating CSS, and customizing the
27
+ appearance of jsPsych experiments.
28
+
29
+ \b
30
+ Examples:
31
+ $ bead deployment ui generate-css experiment/css/custom.css \\
32
+ --theme dark --primary-color "#1976D2"
33
+ $ bead deployment ui customize experiment/ \\
34
+ --theme dark --primary-color "#1976D2"
35
+ """
36
+
37
+
38
+ @click.command()
39
+ @click.argument("output_file", type=click.Path(path_type=Path))
40
+ @click.option(
41
+ "--theme",
42
+ type=click.Choice(["light", "dark", "auto"], case_sensitive=False),
43
+ default="light",
44
+ help="Color theme (light, dark, or auto for system preference)",
45
+ )
46
+ @click.option(
47
+ "--primary-color",
48
+ default="#6200EE",
49
+ help="Primary color as hex code (default: Material Purple)",
50
+ )
51
+ @click.option(
52
+ "--secondary-color",
53
+ default="#03DAC6",
54
+ help="Secondary color as hex code (default: Material Teal)",
55
+ )
56
+ @click.pass_context
57
+ def generate_css(
58
+ ctx: click.Context,
59
+ output_file: Path,
60
+ theme: Literal["light", "dark", "auto"],
61
+ primary_color: str,
62
+ secondary_color: str,
63
+ ) -> None:
64
+ r"""Generate Material Design CSS for jsPsych experiment.
65
+
66
+ Parameters
67
+ ----------
68
+ ctx : click.Context
69
+ Click context object.
70
+ output_file : Path
71
+ Output path for generated CSS file.
72
+ theme : Literal["light", "dark", "auto"]
73
+ Color theme.
74
+ primary_color : str
75
+ Primary color as hex code.
76
+ secondary_color : str
77
+ Secondary color as hex code.
78
+
79
+ Examples
80
+ --------
81
+ $ bead deployment ui generate-css experiment/css/custom.css
82
+
83
+ $ bead deployment ui generate-css experiment/css/dark.css \\
84
+ --theme dark --primary-color "#1976D2"
85
+
86
+ $ bead deployment ui generate-css experiment/css/material.css \\
87
+ --theme auto --primary-color "#6200EE" --secondary-color "#03DAC6"
88
+ """
89
+ try:
90
+ print_info(f"Generating Material Design CSS (theme: {theme})")
91
+
92
+ # Validate color codes
93
+ if not _is_valid_hex_color(primary_color):
94
+ print_error(f"Invalid primary color: {primary_color}")
95
+ print_info("Color must be a valid hex code (e.g., #1976D2)")
96
+ ctx.exit(1)
97
+
98
+ if not _is_valid_hex_color(secondary_color):
99
+ print_error(f"Invalid secondary color: {secondary_color}")
100
+ print_info("Color must be a valid hex code (e.g., #03DAC6)")
101
+ ctx.exit(1)
102
+
103
+ # Create output directory if needed
104
+ output_file.parent.mkdir(parents=True, exist_ok=True)
105
+
106
+ # Generate CSS
107
+ stylesheet = MaterialDesignStylesheet()
108
+ css = stylesheet.generate_css(
109
+ theme=theme,
110
+ primary_color=primary_color,
111
+ secondary_color=secondary_color,
112
+ )
113
+
114
+ # Write to file
115
+ output_file.write_text(css, encoding="utf-8")
116
+
117
+ print_success(f"Generated CSS: {output_file}")
118
+
119
+ # Show preview
120
+ preview_panel = Panel(
121
+ f"[cyan]Theme:[/cyan] {theme}\n"
122
+ f"[cyan]Primary:[/cyan] {primary_color}\n"
123
+ f"[cyan]Secondary:[/cyan] {secondary_color}\n"
124
+ f"[cyan]Lines:[/cyan] {len(css.splitlines())}",
125
+ title="[bold]CSS Preview[/bold]",
126
+ border_style="green",
127
+ )
128
+ console.print(preview_panel)
129
+
130
+ except Exception as e:
131
+ print_error(f"Failed to generate CSS: {e}")
132
+ ctx.exit(1)
133
+
134
+
135
+ @click.command()
136
+ @click.argument("experiment_dir", type=click.Path(exists=True, path_type=Path))
137
+ @click.option(
138
+ "--theme",
139
+ type=click.Choice(["light", "dark", "auto"], case_sensitive=False),
140
+ default="light",
141
+ help="Color theme (light, dark, or auto for system preference)",
142
+ )
143
+ @click.option(
144
+ "--primary-color",
145
+ default="#6200EE",
146
+ help="Primary color as hex code (default: Material Purple)",
147
+ )
148
+ @click.option(
149
+ "--secondary-color",
150
+ default="#03DAC6",
151
+ help="Secondary color as hex code (default: Material Teal)",
152
+ )
153
+ @click.option(
154
+ "--css-file",
155
+ type=click.Path(exists=True, path_type=Path),
156
+ help="Path to custom CSS file to include (optional)",
157
+ )
158
+ @click.option(
159
+ "--output-name",
160
+ default="experiment.css",
161
+ help="Output CSS filename (default: experiment.css)",
162
+ )
163
+ @click.pass_context
164
+ def customize(
165
+ ctx: click.Context,
166
+ experiment_dir: Path,
167
+ theme: Literal["light", "dark", "auto"],
168
+ primary_color: str,
169
+ secondary_color: str,
170
+ css_file: Path | None,
171
+ output_name: str,
172
+ ) -> None:
173
+ r"""Apply UI customization to jsPsych experiment directory.
174
+
175
+ Generates Material Design CSS and optionally merges with custom CSS.
176
+ Writes the final CSS to the experiment's css/ directory.
177
+
178
+ Parameters
179
+ ----------
180
+ ctx : click.Context
181
+ Click context object.
182
+ experiment_dir : Path
183
+ Path to experiment directory.
184
+ theme : Literal["light", "dark", "auto"]
185
+ Color theme.
186
+ primary_color : str
187
+ Primary color as hex code.
188
+ secondary_color : str
189
+ Secondary color as hex code.
190
+ css_file : Path | None
191
+ Optional path to custom CSS file.
192
+ output_name : str
193
+ Output CSS filename.
194
+
195
+ Examples
196
+ --------
197
+ $ bead deployment ui customize experiment/
198
+
199
+ $ bead deployment ui customize experiment/ \\
200
+ --theme dark --primary-color "#1976D2"
201
+
202
+ $ bead deployment ui customize experiment/ \\
203
+ --theme auto --css-file custom.css --output-name styles.css
204
+ """
205
+ try:
206
+ print_info(f"Customizing UI for experiment: {experiment_dir}")
207
+
208
+ # Validate color codes
209
+ if not _is_valid_hex_color(primary_color):
210
+ print_error(f"Invalid primary color: {primary_color}")
211
+ print_info("Color must be a valid hex code (e.g., #1976D2)")
212
+ ctx.exit(1)
213
+
214
+ if not _is_valid_hex_color(secondary_color):
215
+ print_error(f"Invalid secondary color: {secondary_color}")
216
+ print_info("Color must be a valid hex code (e.g., #03DAC6)")
217
+ ctx.exit(1)
218
+
219
+ # Create css directory if needed
220
+ css_dir = experiment_dir / "css"
221
+ css_dir.mkdir(parents=True, exist_ok=True)
222
+
223
+ # Generate Material Design CSS
224
+ stylesheet = MaterialDesignStylesheet()
225
+ material_css = stylesheet.generate_css(
226
+ theme=theme,
227
+ primary_color=primary_color,
228
+ secondary_color=secondary_color,
229
+ )
230
+
231
+ # Merge with custom CSS if provided
232
+ final_css = material_css
233
+ if css_file:
234
+ print_info(f"Merging with custom CSS: {css_file}")
235
+ custom_css = css_file.read_text(encoding="utf-8")
236
+ final_css = material_css + "\n\n/* Custom CSS */\n\n" + custom_css
237
+
238
+ # Write to output file
239
+ output_path = css_dir / output_name
240
+ output_path.write_text(final_css, encoding="utf-8")
241
+
242
+ print_success(f"CSS written to: {output_path}")
243
+
244
+ # Show summary
245
+ summary_panel = Panel(
246
+ f"[cyan]Theme:[/cyan] {theme}\n"
247
+ f"[cyan]Primary:[/cyan] {primary_color}\n"
248
+ f"[cyan]Secondary:[/cyan] {secondary_color}\n"
249
+ f"[cyan]Custom CSS:[/cyan] {'Yes' if css_file else 'No'}\n"
250
+ f"[cyan]Output:[/cyan] {output_path.relative_to(experiment_dir)}\n"
251
+ f"[cyan]Lines:[/cyan] {len(final_css.splitlines())}",
252
+ title="[bold]UI Customization Summary[/bold]",
253
+ border_style="green",
254
+ )
255
+ console.print(summary_panel)
256
+
257
+ # Update index.html to reference the CSS file
258
+ index_html = experiment_dir / "index.html"
259
+ if index_html.exists():
260
+ _update_index_html_css_reference(index_html, output_name)
261
+ print_success("Updated index.html CSS reference")
262
+ else:
263
+ print_info("index.html not found - skipping CSS reference update")
264
+
265
+ except Exception as e:
266
+ print_error(f"Failed to customize UI: {e}")
267
+ ctx.exit(1)
268
+
269
+
270
+ def _is_valid_hex_color(color: str) -> bool:
271
+ """Validate hex color code.
272
+
273
+ Parameters
274
+ ----------
275
+ color : str
276
+ Color string to validate.
277
+
278
+ Returns
279
+ -------
280
+ bool
281
+ True if valid hex color, False otherwise.
282
+
283
+ Examples
284
+ --------
285
+ >>> _is_valid_hex_color("#1976D2")
286
+ True
287
+ >>> _is_valid_hex_color("1976D2")
288
+ False
289
+ >>> _is_valid_hex_color("#GGG")
290
+ False
291
+ """
292
+ if not color.startswith("#"):
293
+ return False
294
+ hex_part = color[1:]
295
+ if len(hex_part) not in (3, 6):
296
+ return False
297
+ try:
298
+ int(hex_part, 16)
299
+ return True
300
+ except ValueError:
301
+ return False
302
+
303
+
304
+ def _update_index_html_css_reference(index_html: Path, css_filename: str) -> None:
305
+ """Update index.html to reference the CSS file.
306
+
307
+ Parameters
308
+ ----------
309
+ index_html : Path
310
+ Path to index.html file.
311
+ css_filename : str
312
+ CSS filename to reference.
313
+ """
314
+ html_content = index_html.read_text(encoding="utf-8")
315
+
316
+ # Check if CSS link already exists
317
+ css_link = f'<link rel="stylesheet" href="css/{css_filename}">'
318
+ if css_link in html_content:
319
+ return # Already has the correct reference
320
+
321
+ # Find </head> tag and insert CSS link before it
322
+ if "</head>" in html_content:
323
+ html_content = html_content.replace(
324
+ "</head>",
325
+ f" {css_link}\n</head>",
326
+ )
327
+ index_html.write_text(html_content, encoding="utf-8")
328
+
329
+
330
+ # Register commands
331
+ deployment_ui.add_command(generate_css)
332
+ deployment_ui.add_command(customize)
bead/cli/display.py ADDED
@@ -0,0 +1,378 @@
1
+ """Rich display utilities for CLI commands.
2
+
3
+ This module provides centralized Rich display utilities for beautiful terminal output
4
+ across all bead CLI commands. All CLI modules should import from this module for
5
+ consistent formatting.
6
+
7
+ Examples
8
+ --------
9
+ >>> from bead.cli.display import print_header, print_success, create_summary_table
10
+ >>> print_header("Stage 1: Resources")
11
+ >>> print_success("Loaded 1,234 items")
12
+ >>> table = create_summary_table({"Items": "1,234", "Time": "2.3s"})
13
+ >>> console.print(table)
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from pathlib import Path
19
+ from typing import Any
20
+
21
+ from rich.console import Console
22
+ from rich.live import Live
23
+ from rich.panel import Panel
24
+ from rich.progress import (
25
+ BarColumn,
26
+ Progress,
27
+ SpinnerColumn,
28
+ TextColumn,
29
+ TimeElapsedColumn,
30
+ )
31
+ from rich.spinner import Spinner
32
+ from rich.table import Table
33
+ from rich.traceback import install
34
+
35
+ # Install rich traceback globally for CLI
36
+ install(show_locals=True, width=100, word_wrap=True)
37
+
38
+ # Shared console instance
39
+ console = Console()
40
+
41
+
42
+ def print_header(text: str) -> None:
43
+ """Print command header with horizontal rule.
44
+
45
+ Parameters
46
+ ----------
47
+ text : str
48
+ Header text to display.
49
+
50
+ Examples
51
+ --------
52
+ >>> print_header("Stage 1: Resources")
53
+ ═══════════════════════════════════════
54
+ Stage 1: Resources
55
+ ═══════════════════════════════════════
56
+ """
57
+ console.rule(f"[bold]{text}[/bold]")
58
+
59
+
60
+ def print_success(text: str) -> None:
61
+ """Print success message with green checkmark.
62
+
63
+ Parameters
64
+ ----------
65
+ text : str
66
+ Success message to display.
67
+
68
+ Examples
69
+ --------
70
+ >>> print_success("Loaded 1,234 items")
71
+ ✓ Loaded 1,234 items
72
+ """
73
+ console.print(f"[green]✓[/green] {text}")
74
+
75
+
76
+ def print_error(text: str) -> None:
77
+ """Print error message with red X.
78
+
79
+ Parameters
80
+ ----------
81
+ text : str
82
+ Error message to display.
83
+
84
+ Examples
85
+ --------
86
+ >>> print_error("Failed to load config")
87
+ ✗ Failed to load config
88
+ """
89
+ console.print(f"[red]✗[/red] {text}")
90
+
91
+
92
+ def print_warning(text: str) -> None:
93
+ """Print warning message with yellow warning sign.
94
+
95
+ Parameters
96
+ ----------
97
+ text : str
98
+ Warning message to display.
99
+
100
+ Examples
101
+ --------
102
+ >>> print_warning("Using default strategy")
103
+ ⚠ Warning: Using default strategy
104
+ """
105
+ console.print(f"[yellow]⚠[/yellow] {text}")
106
+
107
+
108
+ def print_info(text: str) -> None:
109
+ """Print info message with blue info icon.
110
+
111
+ Parameters
112
+ ----------
113
+ text : str
114
+ Info message to display.
115
+
116
+ Examples
117
+ --------
118
+ >>> print_info("Next step: Run partition command")
119
+ ℹ Next step: Run partition command
120
+ """
121
+ console.print(f"[blue]ℹ[/blue] {text}")
122
+
123
+
124
+ def create_summary_table(
125
+ data: dict[str, str],
126
+ title: str | None = None,
127
+ show_header: bool = False,
128
+ ) -> Table:
129
+ """Create formatted summary table.
130
+
131
+ Parameters
132
+ ----------
133
+ data : dict[str, str]
134
+ Dictionary mapping metric names to values.
135
+ title : str | None, optional
136
+ Table title. Default is None.
137
+ show_header : bool, optional
138
+ Whether to show table header. Default is False.
139
+
140
+ Returns
141
+ -------
142
+ Table
143
+ Formatted Rich Table object.
144
+
145
+ Examples
146
+ --------
147
+ >>> table = create_summary_table({
148
+ ... "Items processed": "1,234",
149
+ ... "Success rate": "98.5%",
150
+ ... "Time": "45.2s"
151
+ ... }, title="Summary")
152
+ >>> console.print(table)
153
+ """
154
+ table = Table(show_header=show_header, title=title)
155
+ table.add_column("Metric", style="cyan", no_wrap=True)
156
+ table.add_column("Value", justify="right", style="green")
157
+
158
+ for key, value in data.items():
159
+ table.add_row(key, value)
160
+
161
+ return table
162
+
163
+
164
+ def create_progress() -> Progress:
165
+ """Create standard progress bar for CLI operations.
166
+
167
+ Returns
168
+ -------
169
+ Progress
170
+ Configured Rich Progress instance.
171
+
172
+ Examples
173
+ --------
174
+ >>> with create_progress() as progress:
175
+ ... task = progress.add_task("Loading items...", total=1000)
176
+ ... for i in range(1000):
177
+ ... progress.advance(task)
178
+ """
179
+ return Progress(
180
+ SpinnerColumn(),
181
+ TextColumn("[progress.description]{task.description}"),
182
+ BarColumn(),
183
+ TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
184
+ TimeElapsedColumn(),
185
+ console=console,
186
+ )
187
+
188
+
189
+ def create_spinner_progress() -> Progress:
190
+ """Create spinner-only progress (for indeterminate operations).
191
+
192
+ Returns
193
+ -------
194
+ Progress
195
+ Configured Rich Progress instance with spinner only.
196
+
197
+ Examples
198
+ --------
199
+ >>> with create_spinner_progress() as progress:
200
+ ... progress.add_task("Loading model weights...", total=None)
201
+ """
202
+ return Progress(
203
+ SpinnerColumn(),
204
+ TextColumn("[progress.description]{task.description}"),
205
+ console=console,
206
+ )
207
+
208
+
209
+ def create_live_status(message: str) -> Live:
210
+ """Create live status display with spinner.
211
+
212
+ Parameters
213
+ ----------
214
+ message : str
215
+ Status message to display.
216
+
217
+ Returns
218
+ -------
219
+ Live
220
+ Rich Live instance for status updates.
221
+
222
+ Examples
223
+ --------
224
+ >>> with create_live_status("Training model..."):
225
+ ... model.train()
226
+ """
227
+ return Live(Spinner("dots", text=message), console=console)
228
+
229
+
230
+ def create_panel(
231
+ content: str,
232
+ title: str | None = None,
233
+ style: str = "cyan",
234
+ ) -> Panel:
235
+ """Create formatted panel for important messages.
236
+
237
+ Parameters
238
+ ----------
239
+ content : str
240
+ Panel content.
241
+ title : str | None, optional
242
+ Panel title. Default is None.
243
+ style : str, optional
244
+ Panel border style color. Default is "cyan".
245
+
246
+ Returns
247
+ -------
248
+ Panel
249
+ Rich Panel object.
250
+
251
+ Examples
252
+ --------
253
+ >>> panel = create_panel(
254
+ ... "Generating lexicons from VerbNet...",
255
+ ... title="In Progress"
256
+ ... )
257
+ >>> console.print(panel)
258
+ """
259
+ return Panel(content, title=f"[{style}]{title}[/{style}]" if title else None)
260
+
261
+
262
+ def display_file_stats(file_path: Path, count: int, item_type: str = "items") -> None:
263
+ """Display statistics for a saved file.
264
+
265
+ Parameters
266
+ ----------
267
+ file_path : Path
268
+ Path to saved file.
269
+ count : int
270
+ Number of items in file.
271
+ item_type : str, optional
272
+ Type of items (for display). Default is "items".
273
+
274
+ Examples
275
+ --------
276
+ >>> display_file_stats(Path("items.jsonl"), 1234, "items")
277
+ ✓ Saved 1,234 items to items.jsonl
278
+ """
279
+ print_success(f"Saved {count:,} {item_type} to {file_path}")
280
+
281
+
282
+ def display_validation_errors(
283
+ errors: list[str],
284
+ max_display: int = 10,
285
+ ) -> None:
286
+ """Display validation errors with truncation.
287
+
288
+ Parameters
289
+ ----------
290
+ errors : list[str]
291
+ List of error messages.
292
+ max_display : int, optional
293
+ Maximum number of errors to display. Default is 10.
294
+
295
+ Examples
296
+ --------
297
+ >>> errors = ["Line 1: Invalid JSON", "Line 5: Missing field"]
298
+ >>> display_validation_errors(errors)
299
+ """
300
+ print_error(f"Validation failed with {len(errors)} error(s):")
301
+ for error in errors[:max_display]:
302
+ console.print(f" [red]✗[/red] {error}")
303
+ if len(errors) > max_display:
304
+ console.print(f" ... and {len(errors) - max_display} more error(s)")
305
+
306
+
307
+ def confirm(
308
+ message: str,
309
+ default: bool = False,
310
+ ) -> bool:
311
+ """Prompt user for yes/no confirmation.
312
+
313
+ Parameters
314
+ ----------
315
+ message : str
316
+ Confirmation message.
317
+ default : bool, optional
318
+ Default value if user presses Enter. Default is False.
319
+
320
+ Returns
321
+ -------
322
+ bool
323
+ True if user confirmed, False otherwise.
324
+
325
+ Examples
326
+ --------
327
+ >>> if confirm("Delete all files?", default=False):
328
+ ... delete_files()
329
+ """
330
+ suffix = " [Y/n]: " if default else " [y/N]: "
331
+ response = console.input(f"[yellow]?[/yellow] {message}{suffix}")
332
+
333
+ if not response:
334
+ return default
335
+
336
+ return response.lower() in ("y", "yes")
337
+
338
+
339
+ def display_dry_run_summary(data: dict[str, Any]) -> None:
340
+ """Display dry run summary.
341
+
342
+ Parameters
343
+ ----------
344
+ data : dict[str, Any]
345
+ Dictionary of dry run information.
346
+
347
+ Examples
348
+ --------
349
+ >>> display_dry_run_summary({
350
+ ... "Templates": 26,
351
+ ... "Filled Templates": 1234,
352
+ ... "Output": "items.jsonl"
353
+ ... })
354
+ """
355
+ print_info("[DRY RUN] Preview of operation:")
356
+ for key, value in data.items():
357
+ console.print(f" {key}: {value}")
358
+ print_warning("[DRY RUN] No changes will be made")
359
+
360
+
361
+ # Export commonly used items
362
+ __all__ = [
363
+ "console",
364
+ "print_header",
365
+ "print_success",
366
+ "print_error",
367
+ "print_warning",
368
+ "print_info",
369
+ "create_summary_table",
370
+ "create_progress",
371
+ "create_spinner_progress",
372
+ "create_live_status",
373
+ "create_panel",
374
+ "display_file_stats",
375
+ "display_validation_errors",
376
+ "confirm",
377
+ "display_dry_run_summary",
378
+ ]