diff --git a/.agents/scripts/prepare-marimo-example.sh b/.agents/scripts/prepare-marimo-example.sh new file mode 100755 index 00000000..459ffae0 --- /dev/null +++ b/.agents/scripts/prepare-marimo-example.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + echo "usage: $0 notebook.ipynb --name example-name [--force] [--fail-on-check]" >&2 +} + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd -- "$script_dir/../.." && pwd)" + +input="${1:-}" +if [[ -z "$input" || "$input" == "-h" || "$input" == "--help" ]]; then + usage + if [[ -z "$input" ]]; then + exit 1 + fi + exit 0 +fi +shift + +name="" +force=0 +fail_on_check=0 +while (($#)); do + case "$1" in + --name) + shift + (($#)) || { + echo "missing value for --name" >&2 + exit 1 + } + name="$1" + ;; + --force) + force=1 + ;; + --fail-on-check) + fail_on_check=1 + ;; + *) + echo "unknown argument: $1" >&2 + exit 1 + ;; + esac + shift +done + +[[ -n "$name" ]] || { + usage + exit 1 +} + +[[ "$input" == *.ipynb ]] || { + echo "input must be a .ipynb file: $input" >&2 + exit 1 +} + +[[ -f "$input" ]] || { + echo "input file does not exist: $input" >&2 + exit 1 +} + +input_dir="$(cd -- "$(dirname -- "$input")" && pwd)" +input="$input_dir/$(basename -- "$input")" + +slug="$name" +[[ "$slug" =~ ^[A-Za-z0-9][A-Za-z0-9_-]*$ ]] || { + echo "--name must be a slug like 'mnist-registry' or 'mnist_registry' (no paths, dots, or spaces): $slug" >&2 + exit 1 +} + +module_name="${slug//-/_}" +target_dir="examples/marimo/$slug" +target_py="$target_dir/$module_name.py" +debug_dir="$target_dir/.conversion" +report="$debug_dir/conversion-report.md" +check_output="$debug_dir/marimo-check.txt" + +cd "$repo_root" +if [[ -e "$target_py" && "$force" -eq 0 ]]; then + echo "target notebook already exists: $target_py" >&2 + echo "pass --force to overwrite it" >&2 + exit 1 +fi +mkdir -p "$target_dir" "$debug_dir" + +uvx marimo convert "$input" -o "$target_py" + +check_status=0 +uvx marimo check "$target_py" > "$check_output" 2>&1 || check_status=$? + +cat > "$report" <&2 +fi + +if ((fail_on_check)); then + exit "$check_status" +fi diff --git a/.agents/skills/README.md b/.agents/skills/README.md index d732329b..e2efd80b 100644 --- a/.agents/skills/README.md +++ b/.agents/skills/README.md @@ -9,20 +9,11 @@ plus optional `references/` files. | Skill | Purpose | | --- | --- | -| [`marimo-example-notebook`](marimo-example-notebook/SKILL.md) | **Start here** for creating or refactoring example notebooks in this repo. Encodes wandb/examples conventions and best practices. | -| [`marimo-notebook`](marimo-notebook/SKILL.md) | General marimo notebook format and mechanics (vendored). | -| [`jupyter-to-marimo`](jupyter-to-marimo/SKILL.md) | Converting existing Jupyter notebooks to marimo (vendored). | +| [`marimo-wandb-notebooks`](marimo-wandb-notebooks/SKILL.md) | **Start here** for creating or refactoring example notebooks in this repo. Encodes wandb/examples conventions and best practices. | -## Vendored skills +## Scripts -`marimo-notebook` and `jupyter-to-marimo` are vendored verbatim from -[marimo-team/skills](https://github.com/marimo-team/skills) (Apache-2.0, -LICENSE included in each directory) so agents can use them without -network access. - -- Upstream commit: `62d78d97278e0517c2270a8fbafd3f95a59df9cd` -- Vendored: 2026-07-20 - -To refresh, re-copy `marimo-notebook/` and `jupyter-to-marimo/` -from upstream and update the commit SHA above. Do not edit vendored files -in place — repo-specific guidance belongs in `marimo-example-notebook`. +Use [`../scripts/prepare-marimo-example.sh`](../scripts/prepare-marimo-example.sh) +to create the initial marimo notebook from a Jupyter `.ipynb`, capture +`marimo check` output, and write a temporary `.conversion/` report directory +for the polishing pass. diff --git a/.agents/skills/jupyter-to-marimo/LICENSE b/.agents/skills/jupyter-to-marimo/LICENSE deleted file mode 100644 index 261eeb9e..00000000 --- a/.agents/skills/jupyter-to-marimo/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/.agents/skills/jupyter-to-marimo/SKILL.md b/.agents/skills/jupyter-to-marimo/SKILL.md deleted file mode 100644 index 140fd0ea..00000000 --- a/.agents/skills/jupyter-to-marimo/SKILL.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -name: jupyter-to-marimo -description: Convert a Jupyter notebook (.ipynb) to a marimo notebook (.py). ---- - -# Converting Jupyter Notebooks to Marimo - -**IMPORTANT**: When asked to translate a notebook, ALWAYS run `uvx marimo convert -o ` FIRST before reading any files. This saves precious tokens - reading large notebooks can consume 30k+ tokens, while the converted .py file is much smaller and easier to work with. - -## Steps - -1. **Convert using the CLI** - -Run the marimo convert command via `uvx` so no install is needed: - -```bash -uvx marimo convert -o -``` - -This generates a marimo-compatible `.py` file from the Jupyter notebook. - -2. **Run `marimo check` on the output** - -```bash -uvx marimo check -``` - -Fix any issues that are reported before continuing. - -3. **Review and clean up the converted notebook** - -Read the generated `.py` file and apply the following improvements: - -- Ensure the script metadata block lists all required packages. The converter may miss some. -- Drop leftover Jupyter artifacts like `display()` calls, or `%magic` commands that don't apply in marimo. -- Make sure the final expression of each cell is the value to render. Indented or conditional expressions won't display. -- If the original notebook requires environment variables via an input, consider adding the `EnvConfig` widget from wigglystuff. Details can be found [here](https://koaning.github.io/wigglystuff/reference/env-config.md). -- If the original notebook uses ipywidgets, see `references/widgets.md` for a full mapping of ipywidgets to marimo equivalents, including patterns for callbacks, linking, and anywidget integration. -- If the notebook contains LaTeX, see `references/latex.md` for how to port MathJax syntax to KaTeX (which marimo uses). - -4. **Run `marimo check` again** after your edits to confirm nothing was broken. - diff --git a/.agents/skills/jupyter-to-marimo/references/latex.md b/.agents/skills/jupyter-to-marimo/references/latex.md deleted file mode 100644 index 682b4995..00000000 --- a/.agents/skills/jupyter-to-marimo/references/latex.md +++ /dev/null @@ -1,43 +0,0 @@ -# Porting LaTeX from Jupyter to marimo - -Jupyter uses **MathJax**. marimo uses **KaTeX** (faster, slightly narrower coverage, silent errors). - -## Use raw strings - -LaTeX lives in Python strings in marimo, so use `r"..."` to preserve backslashes: - -```python -mo.md(r"$\frac{1}{2}$") # correct -mo.md("$\frac{1}{2}$") # wrong — \f is a form-feed character -``` - -## Jupyter (MathJax) → marimo (KaTeX) - -| Category | Jupyter (MathJax) | marimo (KaTeX) | -| --- | --- | --- | -| Text | `\mbox`, `\bbox` | `\text{}` | -| Text style | `\textsc`, `\textsl` | `\text{}` | -| Environments | `\begin{eqnarray}` | `\begin{align}` | -| | `\begin{multline}` | `\begin{gather}` | -| References | `\label`, `\eqref`, `\ref` | `\tag{}` for manual numbering | -| Arrays | `\cline`, `\multicolumn`, `\hfill`, `\vline` | not supported | -| Macros | `\DeclareMathOperator` | `\operatorname{}` inline | -| | `\newenvironment` | not supported | -| Spacing | `\mspace`, `\setlength`, `\strut`, `\rotatebox` | not supported | -| Conditionals | `\if`, `\else`, `\fi`, `\ifx` | not supported | - -**These DO work** in KaTeX (despite outdated claims): `\newcommand`, `\def`, `\hbox`, `\hskip`, `\cal`, `\pmb`, `\begin{equation}`, `\begin{split}`, `\operatorname*`. - -## Migration checklist - -1. Find-replace `\mbox{` → `\text{` -2. Use raw strings (`r"..."`) -3. Replace `\begin{eqnarray}` → `\begin{align}` -4. Replace `\DeclareMathOperator` → `\operatorname{}` -5. Remove `\label`/`\eqref` → use `\tag{}` if needed -6. Visually verify — KaTeX fails silently - -## References - -- [KaTeX Support Table](https://katex.org/docs/support_table) — definitive command lookup -- [KaTeX Unsupported Features](https://github.com/KaTeX/KaTeX/wiki/Things-that-KaTeX-does-not-(yet)-support) diff --git a/.agents/skills/jupyter-to-marimo/references/widgets.md b/.agents/skills/jupyter-to-marimo/references/widgets.md deleted file mode 100644 index c86b1e54..00000000 --- a/.agents/skills/jupyter-to-marimo/references/widgets.md +++ /dev/null @@ -1,227 +0,0 @@ -# Porting ipywidgets to marimo - -Jupyter uses **ipywidgets** with imperative callbacks (`observe`, `link`, `jslink`). marimo uses **reactive cells** — a widget's `.value` automatically triggers downstream cells when it changes, so most callback/linking patterns become unnecessary. - -## Widget mapping - -| ipywidget | marimo | Notes | -| --- | --- | --- | -| `IntSlider` | `mo.ui.slider(start, stop, step=1)` | | -| `FloatSlider` | `mo.ui.slider(start, stop, step=0.1)` | | -| `FloatLogSlider` | `mo.ui.slider(steps=np.logspace(...))` | Use `steps` for non-linear | -| `IntRangeSlider` | `mo.ui.range_slider(start, stop)` | | -| `FloatRangeSlider` | `mo.ui.range_slider(start, stop, step=0.1)` | | -| `IntText` | `mo.ui.number()` | | -| `FloatText` | `mo.ui.number()` | | -| `BoundedIntText` | `mo.ui.number(start, stop)` | | -| `BoundedFloatText` | `mo.ui.number(start, stop)` | | -| `IntProgress` | `mo.status.progress_bar(...)` | Not a UI element; display only | -| `FloatProgress` | `mo.status.progress_bar(...)` | Not a UI element; display only | -| `Checkbox` | `mo.ui.checkbox()` | | -| `ToggleButton` | `mo.ui.switch()` | | -| `Valid` | `mo.md("✓" if valid else "✗")` | No direct equivalent | -| `Dropdown` | `mo.ui.dropdown(options)` | | -| `RadioButtons` | `mo.ui.radio(options)` | | -| `Select` | `mo.ui.dropdown(options)` | | -| `SelectMultiple` | `mo.ui.multiselect(options)` | | -| `SelectionSlider` | `mo.ui.slider(steps=options)` | Use `steps` param | -| `SelectionRangeSlider` | `mo.ui.range_slider(steps=options)` | Use `steps` param | -| `ToggleButtons` | `mo.ui.radio(options, inline=True)` | | -| `Text` | `mo.ui.text()` | | -| `Textarea` | `mo.ui.text_area()` | | -| `Combobox` | `mo.ui.dropdown(options, searchable=True)` | Closest match | -| `Password` | `mo.ui.text(kind="password")` | | -| `Label` | `mo.md("text")` | | -| `HTML` | `mo.Html("...")` | | -| `HTMLMath` | `mo.md(r"$...$")` | See `references/latex.md` | -| `Image` | `mo.image(src)` | | -| `Video` | `mo.video(src)` | | -| `Audio` | `mo.audio(src)` | | -| `DatePicker` | `mo.ui.date()` | | -| `TimePicker` | — | No equivalent; use anywidget | -| `DatetimePicker` | `mo.ui.datetime()` | | -| `NaiveDatetimePicker` | `mo.ui.datetime()` | | -| `ColorPicker` | — | No equivalent; use anywidget | -| `FileUpload` | `mo.ui.file()` | | -| `Button` | `mo.ui.button()` | Use `on_click` or `value` counter pattern | -| `Output` | Cell output / `mo.output.replace()` | See "Output widget" below | -| `Play` | `mo.ui.refresh()` | Periodic refresh, not step-based | -| `TagsInput` | — | No equivalent; use anywidget | -| `ColorsInput` | — | No equivalent; use anywidget | -| `FloatsInput` | — | No equivalent; use anywidget | -| `IntsInput` | — | No equivalent; use anywidget | -| `HBox` | `mo.hstack([...])` | | -| `VBox` | `mo.vstack([...])` | | -| `Box` | `mo.hstack([...])` or `mo.vstack([...])` | | -| `GridBox` | `mo.hstack([...], widths="equal")` | Or use CSS grid | -| `Accordion` | `mo.accordion({...})` | | -| `Tab` | `mo.ui.tabs({...})` | | -| `Stack` | `mo.ui.tabs({...})` or `mo.carousel([...])` | Shows one child at a time | -| `AppLayout` | `mo.sidebar(...)` + stacks | Compose with layout helpers | -| `TwoByTwoLayout` | Nested `mo.vstack`/`mo.hstack` | | -| `GridspecLayout` | CSS grid via `mo.Html` | | -| `Controller` | — | No equivalent; use anywidget | - -## Replacing `interact` / `interactive` - -ipywidgets `interact` auto-generates UI from a function signature. In marimo, just create the UI elements and use their values. - -Reactivity is automatic: - -```python -# Jupyter -from ipywidgets import interact -@interact(x=(0, 10), y=["a", "b", "c"]) -def f(x=5, y="a"): - print(x, y) - -# marimo -# cell 1 -x = mo.ui.slider(0, 10, value=5) -y = mo.ui.dropdown(["a", "b", "c"], value="a") -mo.hstack([x, y]) - -# cell 2 — automatically re-runs when x or y change -print(x.value, y.value) -``` - -## Output widget - -Jupyter's `Output` widget captures display output into a container. In marimo, each cell's last expression is its output. For dynamic output: - -```python -# Jupyter -out = widgets.Output() -with out: - print("captured") - -# marimo — just use cell output, or: -mo.output.replace(result) -# or redirect stdout: -with mo.redirect_stdout(): - print("goes to cell output") -``` - -## Replacing `observe` callbacks - -ipywidgets use `.observe()` to react to changes. In marimo, split across cells and rely on reactivity: - -```python -# Jupyter -slider = widgets.IntSlider(value=5) -output = widgets.Output() -def on_change(change): - with output: - output.clear_output() - print(f"Value: {change['new']}") -slider.observe(on_change, names=['value']) - -# marimo -# cell 1 -slider = mo.ui.slider(0, 10, value=5) -slider - -# cell 2 — automatically re-runs when slider changes -f"Value: {slider.value}" -``` - -## Replacing `link` / `jslink` - -ipywidgets use `link()` or `jslink()` to synchronize widget values. In marimo, use `mo.state` to share state across multiple widgets, or use cell reactivity for directional binding. - -### Bidirectional sync via `mo.state` (lifting state up) - -Lift shared state into `mo.state` and wire each widget's `on_change` to the setter. This works with native `mo.ui` elements but **not** with anywidgets (use directional binding or `.observe()` for those). - -```python -# Jupyter -widgets.jslink((slider, 'value'), (text, 'value')) -``` - -```python -# marimo — lift state up into mo.state - -# cell 1 -get_x, set_x = mo.state(0) - -# cell 2 -x = mo.ui.slider( - 0, 10, value=get_x(), on_change=set_x, label="$x$:" -) - -# cell 3 -x_plus_one = mo.ui.number( - 1, 11, - value=get_x() + 1, - on_change=lambda v: set_x(v - 1), - label="$x + 1$:", -) - -# cell 4 -[x, x_plus_one] -``` - -### Directional binding via cell reactivity - -When one widget should drive another (not bidirectional), just read and assign across cells: - -```python -# cell 1 -slider = mo.ui.slider(0, 10) -counter = mo.ui.anywidget(Counter(count=0)) # wrap the anywidget for display -mo.vstack([slider, counter]) - -# cell 2 — runs when slider changes, updates counter -counter.widget.count = slider.value -``` - -## Custom widgets / anywidget integration - -For ipywidgets with **no marimo equivalent** (marked "—" above), check if the widget is an anywidget or has an anywidget-compatible version. If so, wrap it with `mo.ui.anywidget()`. - -If it is not an anywidget, let the user know they should check whether it's a candidate for the [anywidget spec](https://anywidget.dev) — most ipywidgets can be ported. For building custom anywidgets from scratch, see [ANYWIDGET.md](../../marimo-notebook/references/ANYWIDGET.md). - -### Wrapping an existing anywidget - -```python -# cell 1 -from some_library import CustomWidget -widget = mo.ui.anywidget(CustomWidget(param=42)) -widget - -# cell 2 -widget.value # dict of all synced traits, reactively updates -``` - -### Observing individual traits on an anywidget - -When you need granular reactivity on specific traits (not the whole `.value` dict), use `mo.state` with `.observe()`: - -```python -# cell 1 -class Counter(anywidget.AnyWidget): - _esm = "..." - _css = "..." - count = traitlets.Int(0).tag(sync=True) - -counter = Counter(count=0) - -# create granular state subscriber -get_count, set_count = mo.state(counter.count) -counter.observe(lambda _: set_count(counter.count), names=["count"]) - -counter - -# cell 2 -get_count() # reactively updates when count trait changes -``` - -## Migration checklist - -1. Replace each ipywidget with its marimo equivalent from the table above -2. Remove all `.observe()` callbacks — split logic across reactive cells instead -3. Remove all `link()` / `jslink()` calls — use `mo.state` for bidirectional sync or cell reactivity for directional binding -4. Replace `interact`/`interactive` with explicit `mo.ui` elements -5. Replace `Output` widget with cell output or `mo.output.replace()` -6. Replace layout containers (`HBox`, `VBox`, etc.) with `mo.hstack`, `mo.vstack`, `mo.accordion`, `mo.ui.tabs` -7. For widgets with no equivalent, wrap with `mo.ui.anywidget()` or flag as anywidget candidate diff --git a/.agents/skills/marimo-example-notebook/SKILL.md b/.agents/skills/marimo-example-notebook/SKILL.md deleted file mode 100644 index f405904d..00000000 --- a/.agents/skills/marimo-example-notebook/SKILL.md +++ /dev/null @@ -1,142 +0,0 @@ ---- -name: marimo-example-notebook -description: Create or refactor a high-quality marimo example notebook for the wandb/examples repo. Use whenever adding a new example under examples/marimo/, converting a Jupyter example to marimo, or reviewing/refactoring an existing marimo example. Encodes this repo's structure conventions and W&B integration patterns. ---- - -# marimo example notebooks for wandb/examples - -Read [`../marimo-notebook/SKILL.md`](../marimo-notebook/SKILL.md) first for -the marimo file format, reactivity rules, and `marimo check`. If converting -an existing Jupyter notebook, also read -[`../jupyter-to-marimo/SKILL.md`](../jupyter-to-marimo/SKILL.md). This skill -layers repo-specific conventions on top of those. - -The canonical exemplar is -`examples/marimo/mnist-registry/mnist_registry.py` — when in doubt, match -its structure. - -## Repo conventions - -- Each example lives in its own directory: `examples/marimo//`, - with the notebook as `.py`. -- **The `.py` file is the source of truth.** A workflow generates the - markdown export; never hand-edit a generated `.md` next to a notebook. -- Start the file with a PEP 723 script header (pinned lower bounds, e.g. - `"marimo>=0.9"`, `"wandb>=0.18"`) followed by a module docstring that says - what the notebook builds and how to run it: - - ```python - """One-paragraph summary of what the notebook builds. - - Run: - - uvx marimo edit .py --sandbox - """ - ``` - -- Runtime droppings (`data/`, `wandb/`, `artifacts/`, `__marimo__/`, model - weights) must not be committed. - -## Notebook structure - -Order the notebook as a narrative the reader scrolls through top to bottom: - -1. **Setup cell** — `with app.setup(hide_code=True):` holds all imports, - constants, and environment detection (e.g. device selection). One place, - not scattered across cells. -2. **Intro prose** — a markdown cell (`hide_code=True`) with the title, - "What you will build", and "Prerequisites". Prose goes in markdown cells, - never mixed into code cells. -3. **`mo.outline()`** in its own cell near the top, so readers see the - notebook's shape at a glance. -4. **Configuration** — all UI controls batched into a single submittable - form (see below). -5. **The pipeline** — logic cells that consume the form, interleaved with - `## Section` markdown cells. -6. **Verify and next steps** — a closing markdown cell telling the reader - exactly what to look at (in the W&B UI: which charts, tabs, panels) and - what to try next. -7. **Helper functions** — `@app.function` / `@app.class_definition` cells - under a `## Helper functions` section at the bottom. - -## Gate execution once, then let the graph run - -Batch every control into one form so nothing expensive runs until the user -submits: - -```python -form = ( - mo.md( - """ - **Training.** - - {epochs} {batch_size} - ... - """ - ) - .batch(epochs=epochs, batch_size=batch_size, ...) - .form(submit_button_label="Train model", bordered=False) -) -form -``` - -`form.value` is `None` until submit. Gate **one** cell on it, with a message -that tells the reader what will happen: - -```python -mo.stop( - form.value is None, - mo.md("Fill in the form above and click **Train model** to ..."), -) -cfg = form.value -``` - -Every downstream cell references names defined *after* the gate (`cfg`, -`run`, `model`, ...), so marimo's dependency graph holds them all back until -the form is submitted. Do **not** re-check the button/form in later cells, -wrap cells in `if` guards, or use `mo.ui.run_button()` when a form fits — -one `mo.stop()` replaces all of that. - -## Separate logic from presentation - -- Heavy lifting (loading data, training, logging, saving artifacts) goes in - named `@app.function` helpers; the cell body becomes a short, readable - call: `model, history, final_acc, best_acc = run_training(...)`. -- View cells (`hide_code=True`) render results and contain no logic worth - reading. -- Push temporaries into functions to keep notebook globals to a minimum — - marimo notebooks work best with few globals, and every global name is - reserved across the whole file. -- Present results with real components — `mo.ui.table(rows, selection=None)` - for tabular results, `mo.callout(..., kind="success"/"warn"/"danger")` for - status, `mo.vstack` for grouping — not markdown with emoji. - -## W&B integration patterns - -- **Auth**: offer a `mo.ui.text(kind="password")` API-key field that falls - back to ambient login (`wandb login`, `WANDB_API_KEY`, netrc) when blank. - Never write the key into the run config. -- **Re-runs**: marimo keeps the kernel alive across form re-submits, so - finish any prior run first: `if wandb.run is not None: wandb.finish()`. -- **Entity**: include an entity field and explain that accounts created - after May 2024 have no personal entity — the run must go to a team. -- **Surface the run URL immediately** after `wandb.init` so readers can - watch metrics stream: `mo.md(f"**Run started:** [`{run.name}`]({run.url})")`. -- **Expected failures become guidance, not tracebacks.** Wrap only the calls - that fail for account-setup reasons (`wandb.init`, registry linking) and - render a `mo.callout(kind="danger")` that names the likely cause and the - fix. Let everything else fail naturally — no try/except for control flow. -- A recoverable step (e.g. registry linking) should capture its outcome in a - status value that a separate view cell renders, so the pipeline completes - either way. -- Group metrics into UI sections with slash-prefixed names - (`Training/loss`), and put headline numbers in `run.summary`. - -## Before handing back - -- `uvx marimo check ` passes. -- Globals audit: anything only used inside one step should live in a helper. -- The notebook reads top-to-bottom as a tutorial; every code cell either - shows output or is a named helper. -- Fresh-eyes test: a reader with a new W&B account can follow Prerequisites, - submit the form, and verify the result from "Verify and next steps" alone. diff --git a/.agents/skills/marimo-notebook/LICENSE b/.agents/skills/marimo-notebook/LICENSE deleted file mode 100644 index 261eeb9e..00000000 --- a/.agents/skills/marimo-notebook/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/.agents/skills/marimo-notebook/SKILL.md b/.agents/skills/marimo-notebook/SKILL.md deleted file mode 100644 index 0f42a6d8..00000000 --- a/.agents/skills/marimo-notebook/SKILL.md +++ /dev/null @@ -1,282 +0,0 @@ ---- -name: marimo-notebook -description: Write a marimo notebook in a Python file in the right format. ---- - -# Notes for marimo Notebooks - -marimo uses Python to create notebooks, unlike Jupyter which uses JSON. Here's an example notebook: - -```python -# /// script -# dependencies = [ -# "marimo", -# "numpy==2.4.3", -# ] -# requires-python = ">=3.14" -# /// - -import marimo - -__generated_with = "0.20.4" -app = marimo.App(width="medium") - - -@app.cell -def _(): - import marimo as mo - import numpy as np - - return mo, np - - -@app.cell -def _(): - print("hello world") - return - - -@app.cell -def _(np, slider): - np.array([1,2,3]) + slider.value - return - - -@app.cell -def _(mo): - slider = mo.ui.slider(1, 10, 1, label="number to add") - slider - return (slider,) - - -@app.cell -def _(): - return - - -if __name__ == "__main__": - app.run() - -``` - -Notice how the notebook is structured with functions that represent cell contents. Each cell is defined with the `@app.cell` decorator and the inputs/outputs of the function are the inputs/outputs of the cell. marimo usually takes care of the dependencies between cells automatically. - -## Running Marimo Notebooks - -```bash -# Run as script (non-interactive, for testing) -uv run - -# Run interactively in browser -uv run marimo run - -# Edit interactively -uv run marimo edit -``` - -## Script Mode Detection - -Use `mo.app_meta().mode == "script"` to detect CLI vs interactive: - -```python -@app.cell -def _(mo): - is_script_mode = mo.app_meta().mode == "script" - return (is_script_mode,) -``` - -## Key Principle: Keep It Simple - -**Show all UI elements always.** Only change the data source in script mode. - -- Sliders, buttons, widgets should always be created and displayed -- In script mode, just use synthetic/default data instead of waiting for user input -- Don't wrap everything in `if not is_script_mode` conditionals -- Don't use try/except for normal control flow - -### Good Pattern - -```python -# Always show the widget -@app.cell -def _(ScatterWidget, mo): - scatter_widget = mo.ui.anywidget(ScatterWidget()) - scatter_widget - return (scatter_widget,) - -# Only change data source based on mode -@app.cell -def _(is_script_mode, make_moons, scatter_widget, np, torch): - if is_script_mode: - # Use synthetic data for testing - X, y = make_moons(n_samples=200, noise=0.2) - X_data = torch.tensor(X, dtype=torch.float32) - y_data = torch.tensor(y) - data_error = None - else: - # Use widget data in interactive mode - X, y = scatter_widget.widget.data_as_X_y - # ... process data ... - return X_data, y_data, data_error - -# Always show sliders - use their .value in both modes -@app.cell -def _(mo): - lr_slider = mo.ui.slider(start=0.001, stop=0.1, value=0.01) - lr_slider - return (lr_slider,) - -# Auto-run in script mode, wait for button in interactive -@app.cell -def _(is_script_mode, train_button, lr_slider, run_training, X_data, y_data): - if is_script_mode: - # Auto-run with slider defaults - results = run_training(X_data, y_data, lr=lr_slider.value) - else: - # Wait for button click - if train_button.value: - results = run_training(X_data, y_data, lr=lr_slider.value) - return (results,) -``` - -## State and Reactivity - -Variables between cells define the reactivity of the notebook for 99% of the use-cases out there. No special state management needed. Don't mutate objects across cells (e.g., `my_list.append()`); create new objects instead. Avoid `mo.state()` unless you need bidirectional UI sync or accumulated callback state. See [STATE.md](references/STATE.md) for details. - -## Don't Guard Cells with `if` Statements - -Marimo's reactivity means cells only run when their dependencies are ready. Don't add unnecessary guards: - -```python -# BAD - the if statement prevents the chart from showing -@app.cell -def _(plt, training_results): - if training_results: # WRONG - don't do this - fig, ax = plt.subplots() - ax.plot(training_results['losses']) - fig - return - -# GOOD - let marimo handle the dependency -@app.cell -def _(plt, training_results): - fig, ax = plt.subplots() - ax.plot(training_results['losses']) - fig - return -``` - -The cell won't run until `training_results` has a value anyway. - -## Don't Use try/except for Control Flow - -Don't wrap code in try/except blocks unless you're handling a specific, expected exception. Let errors surface naturally. - -```python -# BAD - hiding errors behind try/except -@app.cell -def _(scatter_widget, np, torch): - try: - X, y = scatter_widget.widget.data_as_X_y - X = np.array(X, dtype=np.float32) - # ... - except Exception as e: - return None, None, f"Error: {e}" - -# GOOD - let it fail if something is wrong -@app.cell -def _(scatter_widget, np, torch): - X, y = scatter_widget.widget.data_as_X_y - X = np.array(X, dtype=np.float32) - # ... -``` - -Only use try/except when: -- You're handling a specific, known exception type -- The exception is expected in normal operation (e.g., file not found) -- You have a meaningful recovery action - -## Cell Output Rendering - -Marimo only renders the **final expression** of a cell. Indented or conditional expressions won't render: - -```python -# BAD - indented expression won't render -@app.cell -def _(mo, condition): - if condition: - mo.md("This won't show!") # WRONG - indented - return - -# GOOD - final expression renders -@app.cell -def _(mo, condition): - result = mo.md("Shown!") if condition else mo.md("Also shown!") - result # This renders because it's the final expression - return -``` - -## PEP 723 Dependencies - -Notebooks created via `marimo edit --sandbox` have these dependencies added to the top of the file automatically but it is a good practice to make sure these exist when creating a notebook too: - -```python -# /// script -# requires-python = ">=3.12" -# dependencies = [ -# "marimo", -# "torch>=2.0.0", -# ] -# /// -``` - -## marimo check - -When working on a notebook it is important to check if the notebook can run. That's why marimo provides a `check` command that acts as a linter to find common mistakes. - -```bash -uvx marimo check -``` - -Make sure these are checked before handing a notebook back to the user. - -**Important**: you have a tendency to over-do variables with an underscore prefix. You should only apply this to one or two variables at most. Consider creating a new variable instead of prefixing entire cells in marimo. - -## api docs - -If the user specifically wants you to use a marimo function, you can locally check the docs via: - -``` -uv run --with marimo python -c "import marimo as mo; help(mo.ui.form)" -``` - -## tests - -By default, marimo discovers and executes tests inside your notebook. -When the optional `pytest` dependency is present, marimo runs `pytest` on cells that -consist exclusively of test code - i.e. functions whose names start with `test_`. -If the user asks you to add tests, make sure the `pytest` dependency is added and that -there is a cell that contains only test code. - -For more information on testing with pytest see [PYTEST.md](references/PYTEST.md) - -Once tests are added, you can run pytest from the commandline on the notebook to run pytest. - -``` -pytest -``` - -## Additional resources - -- For marimo notebooks that run in width=columns [COLUMNS.md](references/COLUMNS.md) -- For SQL use in marimo see [SQL.md](references/SQL.md) -- For UI elements in marimo [UI.md](references/UI.md) -- For exposing functions/classes as top level imports [TOP-LEVEL-IMPORTS.md](references/TOP-LEVEL-IMPORTS.md) -- For exporting notebooks (PDF, HTML, markdown, etc.) [EXPORTS.md](references/EXPORTS.md) -- For state management and reactivity [STATE.md](references/STATE.md) -- For deployment of marimo notebooks [DEPLOYMENT.md](references/DEPLOYMENT.md) -- For custom interactive widgets with anywidget [ANYWIDGET.md](references/ANYWIDGET.md) -- For external editing and `--watch` mode [WATCHING.md](references/WATCHING.md) -- For expensive notebooks (caching, lazy eval, mo.stop) [EXPENSIVE.md](references/EXPENSIVE.md) -- For configuration (pyproject.toml, marimo.toml) [CONFIGURATION.md](references/CONFIGURATION.md) -- For reactivity model (DAG, variable scoping, mutations) [REACTIVITY.md](references/REACTIVITY.md) diff --git a/.agents/skills/marimo-notebook/references/ANYWIDGET.md b/.agents/skills/marimo-notebook/references/ANYWIDGET.md deleted file mode 100644 index 6289c56f..00000000 --- a/.agents/skills/marimo-notebook/references/ANYWIDGET.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -name: anywidget-generator -description: Generate anywidget components for marimo notebooks. ---- - -When writing an anywidget use vanilla javascript in `_esm` and do not forget about `_css`. The css should look bespoke in light mode and dark mode. Keep the css small unless explicitly asked to go the extra mile. When you display the widget it must be wrapped via `widget = mo.ui.anywidget(OriginalAnywidget())`. You can also point `_esm` and `_css` to external files if needed using pathlib. This makes sense if the widget does a lot of elaborate JavaScript or CSS. - - -import anywidget -import traitlets - - -class CounterWidget(anywidget.AnyWidget): - _esm = """ - // Define the main render function - function render({ model, el }) { - let count = () => model.get("number"); - let btn = document.createElement("button"); - btn.innerHTML = `count is ${count()}`; - btn.addEventListener("click", () => { - model.set("number", count() + 1); - model.save_changes(); - }); - model.on("change:number", () => { - btn.innerHTML = `count is ${count()}`; - }); - el.appendChild(btn); - } - // Important! We must export at the bottom here! - export default { render }; - """ - _css = """button{ - font-size: 14px; - }""" - number = traitlets.Int(0).tag(sync=True) - -widget = mo.ui.anywidget(CounterWidget()) -widget - -# Grabbing the widget from another cell, `.value` is a dictionary. -print(widget.value["number"]) - - -The above is a minimal example that could work for a simple counter widget. In general the widget can become much larger because of all the JavaScript and CSS required. Unless the widget is dead simple, you should consider using external files for `_esm` and `_css` using pathlib. - -When sharing the anywidget, keep the example minimal. No need to combine it with marimo ui elements unless explicitly stated to do so. - -## Best Practices - -Unless specifically told otherwise, assume the following: - -1. **Use vanilla JavaScript in `_esm`**: - - Define a `render` function that takes `{ model, el }` as parameters - - Use `model.get()` to read trait values - - Use `model.set()` and `model.save_changes()` to update traits - - Listen to changes with `model.on("change:traitname", callback)` - - Export default with `export default { render };` at the bottom - - All widgets inherit from `anywidget.AnyWidget`, so `widget.observe(handler)` - remains the standard way to react to state changes. - - Python constructors tend to validate bounds, lengths, or choice counts; let the - raised `ValueError/TraitError` guide you instead of duplicating the logic. - -2. **Include `_css` styling**: - - Keep CSS minimal unless explicitly asked for more - - Make it look bespoke in both light and dark mode - - Use CSS media query for dark mode: `@media (prefers-color-scheme: dark) { ... }` - -3. **Wrap the widget for display**: - - Always wrap with marimo: `widget = mo.ui.anywidget(OriginalAnywidget())` - - Access values via `widget.value` which returns a dictionary - -4. **Keep examples minimal**: - - Add a marimo notebook that highlights the core utility - - Show basic usage only - - Don't combine with other marimo UI elements unless explicitly requested - -5. **External file paths**: When using pathlib for external `_esm`/`_css` files, keep paths relative to the project directory, consider using `Path(__file__)` for this. Do not read files outside the project (e.g., `~/.ssh`, `~/.env`, `/etc/`) or embed their contents in widget output. - -Dumber is better. Prefer obvious, direct code over clever abstractions—someone -new to the project should be able to read the code top-to-bottom and grok it -without needing to look up framework magic or trace through indirection. diff --git a/.agents/skills/marimo-notebook/references/COLUMNS.md b/.agents/skills/marimo-notebook/references/COLUMNS.md deleted file mode 100644 index 15d56529..00000000 --- a/.agents/skills/marimo-notebook/references/COLUMNS.md +++ /dev/null @@ -1,58 +0,0 @@ -A user may specify that they want to have a notebook with multiple columns. Below is an example of a notebook that does just that. - -```python -# /// script -# dependencies = ["marimo"] -# requires-python = ">=3.14" -# /// - -import marimo - -__generated_with = "0.23.9" -app = marimo.App(width="columns") - - -@app.cell(column=0, hide_code=True) -def _(mo): - mo.md(r""" - ## Column 1: Cool stuff - - This is where the user will first look. Put plots/inputs here typically. - """) - return - - -@app.cell -def _(): - # This cell is in column 1 - return - - -@app.cell -def _(mo): - # This cell is in column 1 as well - mo.ui.slider(1, 10, 1) - return - - -@app.cell(column=1, hide_code=True) -def _(mo): - mo.md(r""" - ## Column 2: Boilerplate - """) - return - - -@app.cell -def _(): - # This cell is in column 2 - import marimo as mo - - return (mo,) - - -if __name__ == "__main__": - app.run() -``` - -Notice the `@app.cell(column=0)` decorator? Every cell that follows sits in that column. Then, when we see `@app.cell(column=1)` the cells no longer fall into column 0 but they go into column 1. diff --git a/.agents/skills/marimo-notebook/references/CONFIGURATION.md b/.agents/skills/marimo-notebook/references/CONFIGURATION.md deleted file mode 100644 index 5b926132..00000000 --- a/.agents/skills/marimo-notebook/references/CONFIGURATION.md +++ /dev/null @@ -1,37 +0,0 @@ -# Configuration - -## Two Scopes - -1. **App config** — per-notebook, stored in the `.py` file header. Configure via the gear icon (top-right): notebook width, title, custom CSS, custom HTML head. -2. **User config** — global, typically stored in `~/.config/marimo/marimo.toml`. Runtime, display, hotkeys, autosave, formatting, server settings. - -## Priority (Highest → Lowest) - -1. PEP 723 script metadata block in the notebook file -2. `pyproject.toml` — project-level overrides -3. User config (`marimo.toml`) — global defaults - -## pyproject.toml - -```toml -[tool.marimo.formatting] -line_length = 120 - -[tool.marimo.display] -default_width = "full" - -[tool.marimo.runtime] -default_sql_output = "native" -watcher_on_save = "autorun" -``` - -## Config Discovery - -marimo searches for `.marimo.toml` in: current directory → parent directories → home directory → XDG config directory. - -## Useful Commands - -```bash -marimo config show # view current config and file location -marimo config describe # list all available config options -``` diff --git a/.agents/skills/marimo-notebook/references/DEPLOYMENT.md b/.agents/skills/marimo-notebook/references/DEPLOYMENT.md deleted file mode 100644 index ab0b7867..00000000 --- a/.agents/skills/marimo-notebook/references/DEPLOYMENT.md +++ /dev/null @@ -1,41 +0,0 @@ -## Running notebooks - -You can deploy a single marimo notebook as a web app: - -```bash -uvx marimo run --sandbox notebook.py -``` - -The `--sandbox` flag makes sure the notebook runs in an isolated UV environment. - -Or deploy a folder of notebooks as a web app with multiple notebooks. Also here, you can use the `--sandbox` flag to run each notebook in its own isolated environment, using the PEP 723 dependencies declared in each notebook: - -```bash -uvx marimo run --sandbox -``` - -### Thumbnails - -When you host multiple notebooks you may want to generate thumbnails. You can generate OpenGraph thumbnails for notebooks using: - -```bash -uvx marimo export thumbnail notebook.py -uvx marimo export thumbnail folder/ -``` - -Thumbnails are stored at `__marimo__/assets//opengraph.png`. The user may also put screenshots there manually. - -Besides images, you can also add metadata to the notebooks by adding to the PEP 723 Dependencies on top of the file. These will appear in an overview if the user deploys a folder of notebooks. - -``` -# /// script -# requires-python = ">=3.12" -# dependencies = [ -# "marimo", -# "polars==1.37.1", -# ] -# [tool.marimo.opengraph] -# title = "My dashboard" -# description = "Tracking my portfolio over time" -# /// -``` diff --git a/.agents/skills/marimo-notebook/references/EXPENSIVE.md b/.agents/skills/marimo-notebook/references/EXPENSIVE.md deleted file mode 100644 index 6c560868..00000000 --- a/.agents/skills/marimo-notebook/references/EXPENSIVE.md +++ /dev/null @@ -1,88 +0,0 @@ -# Expensive Notebooks - -## mo.stop() - -Halt cell execution when a condition is met: - -```python -@app.cell -def _(mo, data): - mo.stop(data is None, mo.md("Waiting for data...")) - # Only runs if data is not None - result = process(data) - return (result,) -``` - -Pair with `mo.ui.run_button()` for manual triggers: - -```python -@app.cell -def _(mo): - run_btn = mo.ui.run_button(label="Run analysis") - run_btn - return (run_btn,) - -@app.cell -def _(mo, run_btn, data): - mo.stop(not run_btn.value) - result = expensive_analysis(data) - return (result,) -``` - -## mo.cache - -In-memory cache for the current session. Results are reused when inputs match: - -```python -@mo.cache -def fetch_data(query: str): - return db.execute(query) -``` - -Works as a decorator or context manager. - -## mo.persistent_cache - -Disk-based cache that persists across notebook restarts: - -```python -@mo.persistent_cache -def train_model(params): - return heavy_training(params) -``` - -## mo.lazy() - -Defer rendering and computation until needed: - -```python -# Only render table when it scrolls into view -mo.lazy(mo.ui.table(large_df)) - -# Only compute when tab is selected -mo.ui.tabs({ - "Summary": summary, - "Details": mo.lazy(lambda: expensive_query()), -}) -``` - -## Runtime Configuration - -- Disable autorun on cell changes for long-running notebooks -- Disable startup autorun to prevent automatic execution on open -- Disable individual cells temporarily during editing - -## Memory Management - -Wrap intermediate computations in functions so local variables get freed: - -```python -@app.cell -def _(): - def _compute(): - large_data = load_everything() - result = summarize(large_data) - return result # large_data is freed here - output = _compute() - return (output,) -``` diff --git a/.agents/skills/marimo-notebook/references/EXPORTS.md b/.agents/skills/marimo-notebook/references/EXPORTS.md deleted file mode 100644 index df29a420..00000000 --- a/.agents/skills/marimo-notebook/references/EXPORTS.md +++ /dev/null @@ -1,66 +0,0 @@ -marimo can export notebooks to several formats via the CLI. - -``` -> uvx marimo export --help -Usage: marimo export [OPTIONS] - COMMAND [ARGS]... - - Export a notebook to various formats. - -Options: - -h, --help Show this message and exit. - -Commands: - html Run a notebook and export it as an HTML file. - html-wasm Export a notebook as a WASM- powered marimo notebook. - ipynb Export a marimo notebook as a Jupyter notebook - md Export a marimo notebook as a code fenced markdown file - pdf Export a marimo notebook as a PDF file. - script Export a marimo notebook as a flat script - session Execute a notebook or directory of notebooks and export session snapshots. - thumbnail Generate OpenGraph thumbnails for notebooks. -``` - -You can learn more about each option by calling the command with the `--help` flag. - -## PDF Export - -Many people may be interested in exporting to a PDF. - -```bash -uvx marimo export pdf notebook.py -o notebook.pdf -``` - -PDF export uses `nbformat` and `nbconvert` under the hood. By default it uses the WebPDF exporter which requires Chromium. Install the dependencies: - -```bash -uv pip install nbformat nbconvert -playwright install chromium -``` - -Useful flags: - -- `--no-include-inputs` — hide code cells, show only outputs -- `--no-include-outputs` — include only code, skip outputs -- `--as=slides` — export as a slide deck PDF (uses reveal.js slide boundaries) -- `--raster-scale 4.0` — controls output sharpness (1.0–4.0, default 4.0) -- `--raster-server=live` — use when a widget needs a running Python kernel to render (recommended for slides) - -## Script Export - -```bash -uvx marimo export script notebook.py -o notebook.script.py -``` - -Flattens the notebook into a plain Python script in topological order. - -## Common Flags - -These flags work across most export subcommands: - -- `-o`, `--output` — output file path -- `--watch` — re-export automatically when the notebook file changes -- `--sandbox` — run in an isolated `uv` environment -- `-f`, `--force` — overwrite if output file already exists -- `--` — pass CLI arguments to the notebook, e.g. `uvx marimo export html notebook.py -o out.html -- --arg value` -- `-y` automatic yes to prompts on the terminal `uvx marimo -y CMD ...` diff --git a/.agents/skills/marimo-notebook/references/PYTEST.md b/.agents/skills/marimo-notebook/references/PYTEST.md deleted file mode 100644 index 2c709925..00000000 --- a/.agents/skills/marimo-notebook/references/PYTEST.md +++ /dev/null @@ -1,212 +0,0 @@ -# Testing with pytest - -## Testing in notebook - -When `pytest` is present, marimo runs `pytest` on cells that -consist exclusively of test code - i.e. functions whose names start with `test_`, -classes whose names start with `Test`, or functions decorated with `@pytest.fixture`. -If a cell mixes in anything else (helper functions, constants, variables, imports, etc.), -that cell is skipped by the test runner (we recommend you move helpers to another cell). - -For example: - -```python -@app.cell -def __(): - import pytest - def inc(x): - return x + 1 - return inc, pytest - -@app.cell -def __(inc, pytest): - class TestBlock: - @staticmethod - def test_fails(): - assert inc(3) == 5, "This test fails" - - @staticmethod - def test_sanity(): - assert inc(3) == 4, "This test passes" - - @pytest.mark.parametrize(("x", "y"), [(3, 4), (4, 5)]) - def test_parameterized(x, y): - assert inc(x) == y - return -``` - -Reactive tests can be disabled. You can disable this behavior with the `runtime.reactive_test` option in the -configuration file. - -## Testing at the command-line - -```bash -pytest -``` - -runs and tests all notebook cells whose names start with `test_`, or cells that -contain only `test_` functions and `Test` classes (just like in notebook tests). - -## Example - -Running `pytest` on - -```python -# content of test_notebook.py -import marimo - -__generated_with = "0.10.6" -app = marimo.App() - - -@app.cell -def _(): - def inc(x): - return x + 1 - return (inc,) - - -@app.cell -def test_fails(inc): - assert inc(3) == 5, "This test fails" - - -@app.cell -def test_sanity(inc): - assert inc(3) == 4, "This test passes" - -@app.cell -def collection_of_tests(inc, pytest): - @pytest.mark.parametrize(("x", "y"), [(3, 4), (4, 5)]) - def test_answer(x, y): - assert inc(x) == y, "These tests should pass." - -@app.cell -def imports(): - import pytest - return pytest -``` - -prints - -```pytest -============================= test session starts ============================== -platform linux -- Python 3.12.9, pytest-8.3.5, pluggy-1.5.0 -rootdir: /notebooks -configfile: pyproject.toml -collected 4 items - -test_notebook.py::test_fails FAILED [ 25%] -test_notebook.py::test_sanity PASSED [ 50%] -test_notebook.py::MarimoTestBlock_0::test_parameterized[3-4] PASSED [ 75%] -test_notebook.py::MarimoTestBlock_0::test_parameterized[4-5] PASSED [100%] - -=================================== FAILURES =================================== -__________________________________ test_fails __________________________________ - - # content of test_notebook.py - import marimo - - __generated_with = "0.10.6" - app = marimo.App() - - - @app.cell - def _(): - def inc(x): - return x + 1 - return (inc,) - - - @app.cell - def test_fails(inc): -> assert inc(3) == 5, "This test fails" -E AssertionError: This test fails -E assert 4 == 5 -E + where 4 = (3) - -test_notebook.py:17: AssertionError -=========================== short test summary info ============================ -FAILED test_notebook.py::test_fails - AssertionError: This test fails -========================= 1 failed, 3 passed in 0.82s ========================== -``` - -## Using Pytest Fixtures - - -marimo supports pytest fixtures, with one limitation: fixtures defined in one cell cannot be used in another cell, unless the fixtures were defined in the setup cell. - -**Fixtures defined in the setup cell**: - -```python -# test_notebook.py -import marimo -app = marimo.App() - -with app.setup: - from fixtures import db_connection, sample_data - -@app.cell -def _(sample_data): - def test_data_loaded(sample_data): - assert len(sample_data) > 0 -``` - -**Fixtures in the same cell as tests**: - -```python -@app.cell -def _(): - import pytest - return pytest - - -@app.cell -def _(pytest): - @pytest.fixture - def temp_file(): - import tempfile - with tempfile.NamedTemporaryFile() as f: - yield f - - def test_writes_to_file(temp_file): - temp_file.write(b"hello") - temp_file.seek(0) - assert temp_file.read() == b"hello" -``` - -**Class fixtures**: - -```python -@app.cell -def _(): - import pytest - return pytest - - -@app.cell -def _(pytest): - class TestDatabase: - @pytest.fixture(scope="class") - def connection(self): - return create_connection() - - def test_query(self, connection): - result = connection.query("SELECT 1") - assert result == 1 -``` - -**`conftest.py` fixtures** work as expected - pytest discovers them automatically. - -### Fixture Limitations - -Fixtures defined in one cell **cannot** be used by tests in a different cell. -This is because pytest collects tests **statically** by parsing the notebook -file without executing it. During collection, pytest can see module-level -fixtures (from `conftest.py` or imported modules) and fixtures defined in the -same scope as the test, but it cannot see fixtures defined in other cells. - -**Why?** Running the entire notebook just for fixture discovery would be -expensive, and static analysis cannot determine which fixtures will be -available after cell execution since cell order is determined at runtime by -marimo's dependency graph. \ No newline at end of file diff --git a/.agents/skills/marimo-notebook/references/REACTIVITY.md b/.agents/skills/marimo-notebook/references/REACTIVITY.md deleted file mode 100644 index 88ac57f9..00000000 --- a/.agents/skills/marimo-notebook/references/REACTIVITY.md +++ /dev/null @@ -1,68 +0,0 @@ -# Reactivity - -## The DAG - -marimo statically analyzes each cell to build a directed acyclic graph: - -- **References** = global variables the cell reads (function parameters) -- **Definitions** = global variables the cell creates (return tuple) - -When a cell runs, all cells that reference its definitions automatically run. Execution order is determined by the DAG, not cell position on the page. - -## Variable Uniqueness - -Every global variable must be defined by exactly one cell. This prevents ambiguity in the dependency graph. - -If you need the same name in multiple cells, use underscore-prefixed cell-local variables: - -```python -@app.cell -def _(): - _temp = expensive_computation() - result_a = summarize(_temp) - return (result_a,) - -@app.cell -def _(): - _temp = different_computation() # no conflict, _temp is cell-local - result_b = summarize(_temp) - return (result_b,) -``` - -## Mutations Are Not Tracked - -marimo does **not** detect mutations like `.append()`, attribute assignment, or in-place DataFrame operations across cells. - -```python -# BAD — mutation in another cell, marimo won't re-run dependents -# Cell 1 -items = [1, 2, 3] -# Cell 2 -items.append(4) # invisible to the DAG - -# GOOD — create a new variable -# Cell 2 -extended = items + [4] -``` - -Mutations within the same cell that defines the variable are fine: - -```python -@app.cell -def _(pd): - df = pd.DataFrame({"a": [1, 2]}) - df["b"] = [3, 4] # same cell, fine - return (df,) -``` - -## Deleting Cells - -Deleting a cell removes its global variables from memory. Cells that referenced those variables become invalidated. - -## Disabling Cells - -Disable a cell to prevent it and its dependents from running. Re-enabling triggers a re-run if upstream cells changed while it was disabled. - -## Lazy Evaluation - -Instead of auto-running dependents, mark them stale for manual execution. Configure in runtime settings or use `mo.lazy()` for specific elements. diff --git a/.agents/skills/marimo-notebook/references/SQL.md b/.agents/skills/marimo-notebook/references/SQL.md deleted file mode 100644 index 39ab93e8..00000000 --- a/.agents/skills/marimo-notebook/references/SQL.md +++ /dev/null @@ -1,65 +0,0 @@ -There are multiple ways to use SQL in marimo. Under the hood, a SQL cell is just a function call to `marimo.sql`. A cell looks like this: - -```python -@app.cell(hide_code=True) -def _(df, mo): - grouped = mo.sql( - f""" - SELECT category, AVG(value) as mean FROM df GROUP BY category ORDER BY mean; - """, - output=False - ) - return (grouped,) -``` - -`grouped` is a polars dataframe. By default marimo uses DuckDB in memory and can refer to dataframe variables that are in scope. - -This is what the signature is of `mo.sql`: - -```python -def sql(query: str, *, output: bool=True, engine: Optional[DBAPIConnection]=None) -> Any -``` - -Typically a `sql` call returns a polars dataframe, but the user can configure pandas as an alternative. - -Notice how a query string goes in with SQL and how you can pass a specific database engine. Be aware that different SQL engines may have different SQL dialects. - -## SQLAlchemy - -One possible engine is SQLAlchemy. - -```python -import sqlalchemy - -# Create an in-memory SQLite database with SQLAlchemy -sqlite_engine = sqlalchemy.create_engine("sqlite:///:memory:") -``` - -You can also use `SQLModel` with a similar connection string. - -## DuckDB - -You can also use DuckDB with a connection string. - -```python -import duckdb - -# Create a DuckDB connection -duckdb_conn = duckdb.connect("file.db", read_only=True) -``` - -## PyIceberg - -marimo supports data catalogs as well. - -```python -from pyiceberg.catalog.rest import RestCatalog - -catalog = RestCatalog( - name="catalog", - warehouse="1234567890", - uri="https://example.com", - token="my-token", -) -``` - diff --git a/.agents/skills/marimo-notebook/references/STATE.md b/.agents/skills/marimo-notebook/references/STATE.md deleted file mode 100644 index 82994f6b..00000000 --- a/.agents/skills/marimo-notebook/references/STATE.md +++ /dev/null @@ -1,94 +0,0 @@ -# State in marimo - -## Reactivity IS State Management - -In marimo, regular Python variables between cells are your state. When a cell assigns a variable, all cells that read it re-run automatically. Widget values (`widget.value`) work the same way — interact with a widget and dependent cells re-execute. No store, no session_state, no hooks needed. - -## Don't Mutate Objects Across Cells - -marimo does **not** track mutations like `my_list.append(42)` or `obj.value = 42`. - -```python -# BAD - mutation in another cell won't trigger re-runs -# Cell 1 -items = [1, 2, 3] - -# Cell 2 -items.append(4) # marimo won't know this happened - -# GOOD - create new objects instead -# Cell 1 -items = [1, 2, 3] - -# Cell 2 -extended_items = items + [4] -``` - -## You Probably Don't Need `mo.state()` - -In 99% of cases, built-in reactivity is enough: - -- **Reading widget values** — just use `widget.value` in another cell -- **Combining multiple inputs** — use `.batch().form()` -- **Conditional data** — use `if`/`else` in one cell - -## When You Do Need `mo.state()` - -Use it when you need **accumulated state from callbacks** or **bidirectional sync** between UI elements. - -```python -get_val, set_val = mo.state(initial_value) -``` - -- Read: `get_val()` -- Update: `set_val(new_value)` or `set_val(lambda d: d + [new_item])` -- The cell calling the setter does NOT re-run (unless `allow_self_loops=True`) - -### Example: todo list with accumulated state - -```python -# Cell 1 — declare state -@app.cell -def _(mo): - get_items, set_items = mo.state([]) - return get_items, set_items - -# Cell 2 — input form -@app.cell -def _(mo, set_items): - task = mo.ui.text(label="New task") - add = mo.ui.button( - label="Add", - on_click=lambda _: set_items(lambda d: d + [task.value]) - ) - mo.hstack([task, add]) - return - -# Cell 3 — display (re-runs when state changes) -@app.cell -def _(mo, get_items): - mo.md("\n".join(f"- {t}" for t in get_items())) - return -``` - -### Example: syncing two UI elements - -```python -@app.cell -def _(mo): - get_n, set_n = mo.state(50) - return get_n, set_n - -@app.cell -def _(mo, get_n, set_n): - slider = mo.ui.slider(0, 100, value=get_n(), on_change=set_n) - number = mo.ui.number(0, 100, value=get_n(), on_change=set_n) - mo.hstack([slider, number]) - return -``` - -## Warnings - -- Don't store `mo.ui` elements inside state — causes hard-to-diagnose bugs. -- Don't use `on_change` when you can just read `.value` from another cell. -- Write idempotent cells — same inputs should produce same outputs. diff --git a/.agents/skills/marimo-notebook/references/TOP-LEVEL-IMPORTS.md b/.agents/skills/marimo-notebook/references/TOP-LEVEL-IMPORTS.md deleted file mode 100644 index 0eaebae8..00000000 --- a/.agents/skills/marimo-notebook/references/TOP-LEVEL-IMPORTS.md +++ /dev/null @@ -1,57 +0,0 @@ -You can import top-level functions and classes defined in a marimo notebook into other Python scripts or notebooks using normal Python syntax, as long as your definitions satisfy the simple criteria described on this page. This makes your notebook code reusable, testable, and easier to edit in text editors of your choice. - -For a function or class to be saved at the top level of the notebook file, it must meet the following criteria: - -The cell must define just a single function or class. -The defined function or class can only refer to symbols defined in the setup cell, or to other top-level symbols. - -```python -# /// script -# dependencies = [ -# "marimo", -# "numpy==2.4.2", -# ] -# requires-python = ">=3.14" -# /// - -import marimo - -__generated_with = "0.19.11" -app = marimo.App(width="medium") - -# Define setup cell -with app.setup: - import numpy as np - - -# Define function cell -@app.function -def calculate_statistics(data): - """Calculate basic statistics for a dataset""" - return { - "mean": np.mean(data), - "median": np.median(data), - "std": np.std(data) - } - - -@app.cell -def _(): - import marimo as mo - - return - -if __name__ == "__main__": - app.run() -``` - -In this example, the setup cell is represented as a context manager `app.setup` and the cell that contains `calculate_statistics` is represented as a function decorator `@app.function`. You can now import `calculate_statistics` from other Python scripts or notebooks. There can be no more than one setup cell per notebook. - -```python -# In another_script.py -from my_notebook import calculate_statistics - -data = [1, 2, 3, 4, 5] -stats = calculate_statistics(data) -print(stats) -``` \ No newline at end of file diff --git a/.agents/skills/marimo-notebook/references/UI.md b/.agents/skills/marimo-notebook/references/UI.md deleted file mode 100644 index 44ae9bda..00000000 --- a/.agents/skills/marimo-notebook/references/UI.md +++ /dev/null @@ -1,92 +0,0 @@ -marimo has a rich set of UI components. - -* `mo.ui.altair_chart(altair_chart)` - create a reactive Altair chart -* `mo.ui.button(value=None, kind='primary')` - create a clickable button -* `mo.ui.run_button(label=None, tooltip=None, kind='primary')` - create a button that runs code -* `mo.ui.checkbox(label='', value=False)` - create a checkbox -* `mo.ui.chat(placeholder='', value=None)` - create a chat interface -* `mo.ui.date(value=None, label=None, full_width=False)` - create a date picker -* `mo.ui.dropdown(options, value=None, label=None, full_width=False)` - create a dropdown menu -* `mo.ui.file(label='', multiple=False, full_width=False)` - create a file upload element -* `mo.ui.number(value=None, label=None, full_width=False)` - create a number input -* `mo.ui.radio(options, value=None, label=None, full_width=False)` - create radio buttons -* `mo.ui.refresh(options: List[str], default_interval: str)` - create a refresh control -* `mo.ui.slider(start, stop, value=None, label=None, full_width=False, step=None)` - create a slider -* `mo.ui.range_slider(start, stop, value=None, label=None, full_width=False, step=None)` - create a range slider -* `mo.ui.table(data, columns=None, on_select=None, sortable=True, filterable=True)` - create an interactive table -* `mo.ui.text(value='', label=None, full_width=False)` - create a text input -* `mo.ui.text_area(value='', label=None, full_width=False)` - create a multi-line text input -* `mo.ui.data_explorer(df)` - create an interactive dataframe explorer -* `mo.ui.dataframe(df)` - display a dataframe with search, filter, and sort capabilities -* `mo.ui.plotly(plotly_figure)` - create a reactive Plotly chart (supports scatter, treemap, and sunburst) -* `mo.ui.tabs(elements: dict[str, mo.ui.Element])` - create a tabbed interface from a dictionary -* `mo.ui.array(elements: list[mo.ui.Element])` - create an array of UI elements -* `mo.ui.form(element: mo.ui.Element, label='', bordered=True)` - wrap an element in a form - -As always, you can learn more about the available inputs to all these components via `uv run --with marimo python -c "import marimo as mo; help(mo.ui.form)"` - -## Forms - -You can compose multiple UI elements into a single form using `.batch().form()`. The `.batch()` method binds named UI elements into a markdown template, and `.form()` adds a submit button so values are only sent on submit. - -```python -form = ( - mo.md( - """ - **Choose an option** - - {choice} - - **Enter some text** - - {text} - - **Enable feature** - - {flag} - """ - ) - .batch( - choice=mo.ui.dropdown(options=["A", "B", "C"]), - text=mo.ui.text(), - flag=mo.ui.checkbox(), - ) - .form( - submit_button_label="Submit", - show_clear_button=True, # optional - clear_on_submit=False, # keep values after submit - ) -) - -form -``` - -You can also add validation to a form using the `validate` parameter. Return an error string to block submission, or `None` to allow it. - -```python -group_by_form = mo.ui.dropdown( - options=df_columns, - label="Select column to filter for duplicate analysis", - allow_select_none=True, - value=None, # start with nothing selected - searchable=True, -).form( - submit_button_label="Apply", - validate=lambda v: ( - "Please select a column and press Apply." - if v is None else None - ), -) -``` - -However, the user may also want to use other components. Popular alternatives include the `ScatterWidget` from the `drawdata` library, `moutils`, and `wigglystuff`. - -For custom classes and static HTML representations you can also use the `_display_` method. - -```python -class Dice: - def _display_(self): - import random - - return f"You rolled {random.randint(0, 7)}" -``` diff --git a/.agents/skills/marimo-notebook/references/WATCHING.md b/.agents/skills/marimo-notebook/references/WATCHING.md deleted file mode 100644 index e16a70d2..00000000 --- a/.agents/skills/marimo-notebook/references/WATCHING.md +++ /dev/null @@ -1,55 +0,0 @@ -# External Editing and Watch Mode - -## The Problem - -marimo loads the notebook file into memory at startup. After that, it works from its in-memory state and does **not** watch the file for external changes. If you edit the `.py` file externally (vim, VSCode, another agent), marimo won't see it. When any cell is saved in the marimo UI, it writes its in-memory version back to disk, **overwriting your external edits**. - -## Solution: --watch - -```bash -marimo edit --watch notebook.py -``` - -This monitors the file for changes and streams them to the browser editor. By default, synced code appears as "stale" — the user manually runs cells via the "Run" button or the `runStale` hotkey. - -For apps: - -```bash -marimo run --watch notebook.py -``` - -This auto-refreshes when file changes are detected. - -## Auto-Execute After External Edits - -Add to `pyproject.toml` so affected cells run automatically when the file changes: - -```toml -[tool.marimo.runtime] -watcher_on_save = "autorun" -``` - -## Install watchdog for Performance - -Without `watchdog`, marimo falls back to polling: - -```bash -pip install watchdog -``` - -## Module Autoreloading - -Watch imported `.py` modules for changes (not just the notebook file): - -1. Enable in notebook settings → Runtime → Module Autoreloading -2. Two modes: - - **Autorun**: automatically executes cells affected by module changes - - **Lazy**: marks affected cells as stale for manual execution - -The reloader tracks changes recursively through the import chain. - -Use case: develop logic in Python modules, use the notebook as an orchestrating DAG. - -## Responding to other files - -marimo has `mo.watch.file` and `mo.watch.directory` utilities that can cause cells to update when a file/folder updates. diff --git a/.agents/skills/marimo-wandb-notebooks/SKILL.md b/.agents/skills/marimo-wandb-notebooks/SKILL.md new file mode 100644 index 00000000..e239cb89 --- /dev/null +++ b/.agents/skills/marimo-wandb-notebooks/SKILL.md @@ -0,0 +1,86 @@ +--- +name: marimo-wandb-notebooks +description: Create, convert, review, or refactor repo-ready marimo example notebooks for wandb/examples. +--- + +# marimo example notebooks for wandb/examples + +## Read order + +| Situation | Do this | +| --- | --- | +| Always | Read [`references/marimo-idioms.md`](references/marimo-idioms.md). | +| Starting from an existing marimo `.py` | Do not run `prepare-marimo-example.sh`. Inspect the `.py`, run `uvx marimo check `, and polish against the repo conventions below. | +| Starting from `.ipynb` | Run [`../../scripts/prepare-marimo-example.sh`](../../scripts/prepare-marimo-example.sh) ` --name `, then read [`references/conversion-cleanup.md`](references/conversion-cleanup.md) with `.conversion/conversion-report.md` and `.conversion/marimo-check.txt`. | +| Notebook uses W&B | Read [`references/wandb-patterns.md`](references/wandb-patterns.md). | + +The canonical exemplar is +`examples/marimo/mnist-registry/mnist_registry.py` — when in doubt, match +its structure. + +## Repo conventions + +- Each example lives in its own directory: `examples/marimo//`, + with the notebook as `.py`. +- **The `.py` file is the source of truth.** A workflow generates the + markdown export; never hand-edit a generated `.md` next to a notebook. +- Start the file with a PEP 723 script header (pinned lower bounds, e.g. + `"marimo>=0.9"`, `"wandb>=0.18"`) followed by a module docstring that says + what the notebook builds and how to run it: + + ```python + """One-paragraph summary of what the notebook builds. + + Run: + + uvx marimo edit .py --sandbox + """ + ``` + +- Runtime droppings (`data/`, `wandb/`, `artifacts/`, `__marimo__/`, model + weights) must not be committed. + +## Notebook structure + +Order the notebook as a narrative the reader scrolls through top to bottom: + +1. **Setup cell** — `with app.setup(hide_code=True):` holds all imports, + constants, and environment detection (e.g. device selection). One place, + not scattered across cells. +2. **Intro prose** — a markdown cell (`hide_code=True`) with the title, + "What you will build", and "Prerequisites". Prose goes in markdown cells, + never mixed into code cells. +3. **`mo.outline()`** in its own cell near the top, so readers see the + notebook's shape at a glance. +4. **Configuration** — all UI controls batched into a single submittable + form (see below). +5. **The pipeline** — logic cells that consume the form, interleaved with + `## Section` markdown cells. +6. **Verify and next steps** — a closing markdown cell telling the reader + exactly what to look at (in the W&B UI: which charts, tabs, panels) and + what to try next. +7. **Helper functions** — `@app.function` / `@app.class_definition` cells + under a `## Helper functions` section at the bottom. + +## Gate execution once, then let the graph run + +Batch expensive controls into one form, gate once with `mo.stop`, and let +downstream cells depend on names defined after the gate. See +[`references/marimo-idioms.md`](references/marimo-idioms.md) for the detailed +pattern. + +## Separate logic from presentation + +Put heavy work in named helpers and keep view cells focused on rendering. See +[`references/marimo-idioms.md`](references/marimo-idioms.md) for details. + +## Final verification + +- `uvx marimo check ` passes. +- Globals audit: anything only used inside one step should live in a helper. +- The notebook reads top-to-bottom as a tutorial; every code cell either + shows output or is a named helper. +- Fresh-eyes test: a reader with a new W&B account can follow Prerequisites, + submit the form, and verify the result from "Verify and next steps" alone. +- `.conversion/` files are temporary debugging artifacts and must not be + referenced by the final notebook or docs. diff --git a/.agents/skills/marimo-wandb-notebooks/references/conversion-cleanup.md b/.agents/skills/marimo-wandb-notebooks/references/conversion-cleanup.md new file mode 100644 index 00000000..56b8d0a3 --- /dev/null +++ b/.agents/skills/marimo-wandb-notebooks/references/conversion-cleanup.md @@ -0,0 +1,47 @@ +# Conversion Cleanup + +Use this after `.agents/scripts/prepare-marimo-example.sh` creates the initial +marimo notebook from a Jupyter `.ipynb`. + +## Start From The Report + +- Read `.conversion/conversion-report.md` in the generated example directory. +- Read `.conversion/marimo-check.txt` before editing the notebook. +- Fix `marimo check` issues first; they often point to converted cells that + need to be split, reordered, or moved into helpers. + +## Common Converter Leftovers + +- Ensure the PEP 723 script metadata lists every runtime package the notebook + imports. The converter may miss dependencies. +- Remove Jupyter-only artifacts such as `%magic` commands, shell escapes, and + unnecessary `display()` calls. +- Make the intended output the final expression of each display cell. Indented + or conditional expressions will not render as cell output. +- Replace notebook-global scratch variables with local variables inside helper + functions when they are only used in one step. +- Prefer explicit markdown cells for prose. Do not leave tutorial text inside + code comments or string literals in logic cells. + +## Widget Cleanup + +- Replace ipywidgets with native `mo.ui` components when there is a direct + equivalent. +- Replace `interact`, `observe`, `link`, and `jslink` patterns with marimo + reactivity. Split UI definition, value consumption, and rendering into + separate cells. +- Use `mo.ui.anywidget()` only when no native marimo component fits. + +## LaTeX Cleanup + +- Use raw strings for markdown containing LaTeX, such as `mo.md(r"$x^2$")`. +- Replace MathJax-only constructs with KaTeX-compatible syntax. +- Visually verify math-heavy outputs because KaTeX failures can be quiet. + +## Final Check + +Run: + +```bash +uvx marimo check examples/marimo//.py +``` diff --git a/.agents/skills/marimo-wandb-notebooks/references/marimo-idioms.md b/.agents/skills/marimo-wandb-notebooks/references/marimo-idioms.md new file mode 100644 index 00000000..67c7b0c6 --- /dev/null +++ b/.agents/skills/marimo-wandb-notebooks/references/marimo-idioms.md @@ -0,0 +1,104 @@ +# Marimo Idioms + +Use this as a short review checklist when writing or polishing marimo notebooks +for this repo. + +## Notebook Shape + +- A marimo notebook is a Python file. Cells are functions decorated with + `@app.cell`; dependencies are the function arguments and return values. +- Use a single setup cell for imports, constants, and environment detection. +- Add PEP 723 script metadata at the top so `uvx marimo ... --sandbox` can + recreate the runtime environment. +- Keep globals scarce. Every returned name is notebook-wide, so move + step-local scratch work into helper functions. + +## Reactivity + +- Let the dependency graph do the work. A cell runs when its inputs are ready. +- Do not mutate shared objects across cells, such as `items.append(...)` or + in-place DataFrame edits. Create a new value instead. +- Avoid `mo.state()` unless you need bidirectional UI sync or accumulated + callback state. Most notebooks only need ordinary variables and `.value`. +- Do not wrap downstream cells in repeated `if form.value` or button checks. + Gate once with `mo.stop`, then make later cells depend on names defined after + the gate. + +## Gating Expensive Work + +Batch every control into one form so nothing expensive runs until the user +submits: + +```python +form = ( + mo.md( + """ + **Training.** + + {epochs} {batch_size} + ... + """ + ) + .batch(epochs=epochs, batch_size=batch_size, ...) + .form(submit_button_label="Train model", bordered=False) +) +form +``` + +`form.value` is `None` until submit. Gate one cell on it, with a message that +tells the reader what will happen: + +```python +mo.stop( + form.value is None, + mo.md("Fill in the form above and click **Train model** to ..."), +) +cfg = form.value +``` + +Every downstream cell references names defined after the gate, such as `cfg`, +`run`, or `model`, so marimo's dependency graph holds them all back until the +form is submitted. Do not re-check the form in later cells, wrap cells in `if` +guards, or use `mo.ui.run_button()` when a form fits. + +## Rendering + +- The final expression of a cell is what renders. +- Indented expressions inside `if`, `for`, `with`, or helper blocks do not + become the cell output. Assign the display object, then put it last. +- Use markdown cells for prose. Use view cells for rendering. Keep heavy logic + in named helpers. + +## Logic And Presentation + +- Heavy lifting, such as loading data, training, logging, and saving artifacts, + goes in named `@app.function` helpers. The cell body should become a short, + readable call like `model, history = run_training(...)`. +- View cells, often `hide_code=True`, render results and contain no logic worth + reading. +- Push temporaries into functions to keep notebook globals to a minimum. Every + returned name is reserved across the whole file. +- Present results with real components, such as `mo.ui.table`, + `mo.callout(kind="success")`, `mo.vstack`, and `mo.hstack`, instead of + formatting complex UI as markdown. + +## UI + +- Prefer a single submittable form for controls that trigger expensive work. +- Show widgets directly; downstream cells should read `.value`. +- Prefer native `mo.ui` components before reaching for anywidget. + +## Error Handling + +- Do not use `try`/`except` for normal control flow. +- Let unexpected programming errors surface. +- Catch only specific, expected failures where the notebook can give useful + recovery guidance, such as W&B auth or account setup problems. + +## Verification + +Run this before handing back: + +```bash +uvx marimo check +``` diff --git a/.agents/skills/marimo-wandb-notebooks/references/wandb-patterns.md b/.agents/skills/marimo-wandb-notebooks/references/wandb-patterns.md new file mode 100644 index 00000000..c7de7228 --- /dev/null +++ b/.agents/skills/marimo-wandb-notebooks/references/wandb-patterns.md @@ -0,0 +1,49 @@ +# W&B Patterns + +Use these patterns when a marimo example initializes W&B runs, logs metrics, or +links artifacts and registry entries. + +## Authentication + +- Offer a `mo.ui.text(kind="password")` API-key field. +- Fall back to ambient login when the field is blank, such as `wandb login`, + `WANDB_API_KEY`, or netrc. +- Never write the API key into the run config. + +## Runs And Reruns + +- marimo keeps the kernel alive across form re-submits, so finish any prior run + before starting a new one: + + ```python + if wandb.run is not None: + wandb.finish() + ``` + +- Surface the run URL immediately after `wandb.init` so readers can watch + metrics stream: + + ```python + mo.md(f"**Run started:** [`{run.name}`]({run.url})") + ``` + +- Group metrics into UI sections with slash-prefixed names, such as + `Training/loss`, and put headline numbers in `run.summary`. + +## Entity + +- Include an entity field. +- Explain that accounts created after May 2024 have no personal entity; the run + must go to a team. + +## Expected Failures + +- Expected failures should become guidance, not tracebacks. +- Wrap only calls that fail for account-setup reasons, such as `wandb.init` or + registry linking. +- Render a `mo.callout(kind="danger")` that names the likely cause and fix. +- Let everything else fail naturally. Do not use `try`/`except` for normal + control flow. +- A recoverable step, such as registry linking, should capture its outcome in a + status value that a separate view cell renders, so the pipeline completes + either way. diff --git a/.gitignore b/.gitignore index c6a6c704..50573998 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,5 @@ MNIST/* log.txt colabs/log.txt artifacts -examples/jax/jax-llm/proteins-base/ \ No newline at end of file +examples/marimo/*/.conversion/ +examples/jax/jax-llm/proteins-base/