anylumino
anylumino is a Python library for building composable notebook widgets with
anywidget. It provides Lumino layout panels, menus, and toolbars, a family of
Astryx UI components for inputs, tables, and surfaces, and browser-native date
and time controls, all exposed as anywidgets that nest into one another.
It targets the anywidget 0.11 AFM composition API: Python passes child
anywidgets as synced widget references, and the frontend resolves each child
with host.getWidget(...) before mounting it into a Lumino layout widget or an
Astryx component slot.
The main benefit is composition. anylumino lets a notebook author build
larger reusable widgets from existing anywidgets, Astryx controls, charts,
tables, and outputs. A composed widget can own its layout, state, callbacks,
and domain methods while still being an anywidget that can be nested into
another layout.
It also ships a workbench that builds these UIs by chatting with a local coding agent, Claude Code or Codex, and exports the result as plain Python.
Documentation: https://faysou.github.io/anylumino/docs/
Quick start
import anylumino as al
import anylumino.astryx as ax
brand = ax.Brand(
"desk",
**{
"color-accent": ("#0057b8", "#79b8ff"),
"color-background-card": ("#ffffff", "#111827"),
"color-text-primary": ("#111827", "#f9fafb"),
"radius-container": "8px",
},
)
status = ax.Text("Ready")
orders = ax.Table(
rows=[
{"order_id": "O-1", "symbol": "AAPL", "qty": 10, "status": "NEW"},
{"order_id": "O-2", "symbol": "MSFT", "qty": 5, "status": "PARTIAL"},
],
columns={"order_id": "Order", "symbol": "Symbol", "qty": "Qty", "status": "Status"},
row_key="order_id",
selects="multiple",
sortable=True,
sort_key="symbol",
width="100%",
)
controls = ax.Theme(
{
"heading": ax.Stack(
{
"title": ax.Heading("Orders", level=3),
"badge": ax.Badge("Live", variant="success"),
},
direction="horizontal",
gap=2,
align="center",
),
"symbol": ax.Typeahead(
["AAPL", "MSFT", "NVDA"],
value="AAPL",
label="Symbol",
),
"fields": ax.Tokenizer(
["Bid", "Ask", "Last", "Size"],
value=["Bid", "Ask"],
label="Fields",
),
"refresh": ax.Button(
"Refresh",
variant="primary",
callbacks=[lambda _button: setattr(status, "text", "Refreshed")],
),
"orders": orders,
},
brand=brand,
mode="system",
gap=3,
)
al.SplitPanel(
{"controls": controls, "status": status},
orientation="vertical",
sizes=[0.8, 0.2],
height=560,
)
What it provides
- Layout widgets: public
LayoutWidgetbase plusTabPanel,BoxPanel,HBox,VBox,ScrollBox,SplitPanel,DockPanel,AccordionPanel,StackedPanel,GridPanel, andResponsivePanel. - Astryx layout panels with the same keyed child API and Astryx styling:
TabPanel,StackedPanel,AccordionPanel,ScrollBox,SplitPanel, andResponsivePanelinanylumino.astryx. - Astryx component wrappers for notebook UI: buttons, text, headings, badges, cards, stacks, grids, forms, fields, tabs, segmented controls, selectors, checkboxes, radio lists, sliders, date/time inputs, banners, status dots, progress bars, code, Markdown, tooltips, popovers, inline dialogs, static search inputs, command palettes, and tables.
- Astryx table helpers: normalized rows and columns, optional checkbox selection, sorting, row append/prepend/update/remove helpers, and Python callbacks for selection and sort changes.
- Reusable Astryx theming with
BrandandTheme, so notebooks can define shared colors, radii, and component treatment once. - Action widgets:
Toolbar,DropdownMenu,MoreMenu, andCommandPalettefor notebook-safe commands.
All layout containers inherit from LayoutWidget, which provides keyed child
composition, owner access, dynamic mutation, and method forwarding.
This turns layout into an application structure, not only visual placement.
Children remain addressable by key, callbacks can interact with sibling
widgets, and larger components such as OrderDashboard, FigurePanel, or
SampleReview can be reused across notebooks without relying on global
variables.
Workbench
anylumino.workbench.Workbench builds a notebook UI from a chat with a local
claude (Claude Code) or codex (Codex CLI). Pass the data the UI should use,
then describe the UI:
import pandas as pd
from anylumino.workbench import Workbench
sales = pd.DataFrame(
{"region": ["EMEA", "APAC", "AMER"], "revenue": [1.5, 2.0, 3.2]}
)
Workbench(agent="claude", session="sales", namespace={"sales": sales})
The agent writes an OpenUI program and an App class with the callbacks. The
workbench shows the live UI next to the program, the Python generated from it,
and the callbacks. The agent sees the names, types, and data frame columns of
the namespace, not its rows, and its turns run read-only. Charts use Plotly.
Export writes the UI to one Python file, which
anylumino.workbench.load("exports/sales.py", sales=sales) shows in any
notebook without the agent. Import reads an edited file back into the
workbench. The workbench needs node on PATH and a signed-in agent CLI. See
the workbench guide.
Layout composition
Layout widgets are resizable by drag and drop by default. Pass
resizable=False to hide the bottom-right resize handle. SplitPanel also
syncs Lumino split-handle changes back to its sizes trait.
Layout children can be declared with semantic keys. If titles is omitted for
a mapping, the titles default to those keys.
tabs = al.TabPanel({"orders": orders, "logs": ax.Text("No events")})
tabs.select_key("orders")
tabs.selected_key
tabs.get_widget("logs")
If a child is a Python wrapper with a .widget anywidget view, anylumino renders
the view and keeps the wrapper as the keyed owner.
tabs = al.TabPanel({"trend": trend_panel})
tabs.get_widget("trend") # trend_panel.widget
tabs["trend"].refresh()
tabs.call_owner("trend", "refresh")
To add methods to a composed UI object, subclass a layout widget. The subclass is still an anywidget and can be nested inside another layout:
class OrderDashboard(al.VBox):
def __init__(self, rows):
self.table = ax.Table(
rows,
row_key="order_id",
selects="multiple",
sortable=True,
)
super().__init__({"table": self.table})
def replace_orders(self, rows):
self.table.set_rows(rows)
tabs = al.TabPanel({"orders": OrderDashboard(order_rows)})
tabs["orders"].replace_orders(next_order_rows)
Astryx widgets
Astryx widgets are the UI family for notebook-facing controls. They use the
anylumino.astryx namespace so notebook component code stays explicit. Useful
appearance and behavior options are keyword-only constructor arguments with
Python snake-case names. Unmodeled JSON-safe upstream options can still be
passed through **props. See the Astryx component documentation
for the upstream component reference.
form = ax.FormLayout(
{
"symbol": ax.TextInput(value="AAPL", label="Symbol"),
"quantity": ax.NumberInput(value=100, label="Quantity", min=1),
"side": ax.SegmentedControl(
["Buy", "Sell"],
value="Buy",
label="Side",
),
"routing": ax.Selector(
["Smart", "Primary", "Dark"],
value="Smart",
label="Routing",
),
"urgent": ax.Checkbox(value=False, label="Urgent"),
}
)
Read an input widget's current value with .value. Use traitlets observers
when another widget should react to edits:
symbol = ax.TextInput(value="AAPL", label="Symbol")
quantity = ax.NumberInput(value=100, label="Quantity")
symbol.value
quantity.value
symbol.observe(lambda change: print(change["new"]), names="value")
Text inputs, number inputs, and sliders send every keystroke and drag step to
Python. Pass continuous_update=False when a value should arrive only once the
edit ends, which for inputs means blur or Enter and for sliders means the end of
the drag.
notes = ax.TextArea(value="", label="Notes", continuous_update=False)
risk = ax.Slider(value=42, label="Risk budget", continuous_update=False)
Astryx Slider steps linearly, so two wrappers adapt it for scales it does not
model directly. LogSlider drives it in exponent space and reports
base ** exponent; SelectionSlider drives it over option indices and reports
the option, taking a two-item value for a range.
rate = ax.LogSlider(1e-3, label="Learning rate", base=10, min_exponent=-4, max_exponent=0)
size = ax.SelectionSlider(["XS", "S", "M", "L"], value="M", label="Size", marks=True)
window = ax.SelectionSlider(["Mon", "Tue", "Wed", "Thu", "Fri"], value=["Tue", "Thu"], label="Window")
To mirror one widget's state onto another, prefer traitlets.link over a
callback:
import traitlets
mirror = ax.Text("AAPL")
traitlets.link((symbol, "value"), (mirror, "text"))
Every anylumino widget registers activations the same way: on_click(callback)
runs for each activation and receives the widget, while on_action(callback)
runs only for activations that carry a value and receives the widget and that
value. Pass remove=True to unregister the same callable. The callbacks and
action_callbacks constructor arguments register the same two lists.
What counts as an activation depends on the widget kind:
- Event widgets such as buttons, cards, menus, toolbars, and command palettes activate on a click, because a click is not a persistent value.
- Value widgets such as checkboxes, switches, inputs, sliders, and the native
date and time controls activate on every
valuechange.on_actionreceives the new value. They also fire when Python assignsvalue, so useobservewhen a handler needs the old value or must ignore programmatic updates. - Tables and tab panels keep selection, sort, and tab state in synced traits.
Observe those traits, or use
on_selectionandon_sorton tables.
status = ax.Text("Ready")
button = ax.Button(
"Submit",
variant="primary",
callbacks=[lambda _button: setattr(status, "text", "Submitted")],
)
live = ax.Checkbox(False, label="Live")
live.on_action(lambda _checkbox, checked: setattr(status, "text", f"Live: {checked}"))
menu = ax.DropdownMenu(
[{"label": "Export", "value": "export"}],
label="Actions",
)
menu.on_action(lambda _menu, action: setattr(status, "text", f"Chose {action}"))
Choosing a menu item is both an activation and a value, so it runs on_click
and on_action. Toolbar, MenuBar, and CommandPalette also keep their
id-keyed callbacks mapping, which stays mutable after construction.
Astryx tables
Table renders structured row data with optional checkbox selection and
sorting. Pass mappings, sequences, or scalar rows; use row_key when mappings
contain a stable row id such as an order id or symbol.
orders = ax.Table(
rows=[
{"order_id": "O-1", "symbol": "AAPL", "qty": 10, "status": "NEW"},
{"order_id": "O-2", "symbol": "MSFT", "qty": 5, "status": "PARTIAL"},
],
columns={"order_id": "Order", "symbol": "Symbol", "qty": "Qty", "status": "Status"},
row_key="order_id",
selects="multiple",
sortable=True,
sort_key="symbol",
)
orders.prepend_row({"order_id": "O-0", "symbol": "AMD", "qty": 2, "status": "NEW"})
orders.append_row({"order_id": "O-3", "symbol": "NVDA", "qty": 1, "status": "NEW"})
orders.update_row("O-2", {"qty": 7, "status": "FILLED"})
orders.remove_row("O-1")
orders.set_rows(next_order_snapshot)
Each row helper assigns a new rows list, so displayed notebooks receive the
update through traitlets. prepend_row inserts before existing rows and
append_row inserts after existing rows. Leave selects="" for no checkbox
column, use selects="single" for one selected row, or use
selects="multiple" for multi-row selection.
Theming
Use Brand and Theme for small reusable notebook theme
overrides:
brand = ax.Brand(
"desk",
**{
"color-accent": ("#0057b8", "#79b8ff"),
"color-background-card": ("#ffffff", "#111827"),
"radius-container": "8px",
},
)
panel = ax.Theme(
{"button": ax.Button("Run"), "status": ax.Badge("Ready")},
brand=brand,
mode="system",
gap=2,
)
mode accepts "light", "dark", "system" to follow the OS
prefers-color-scheme, and "jupyterlab" to follow the JupyterLab theme.
Astryx syncs the active mode to the page's html[data-theme]; anylumino restores
what the page started with once the last Astryx widget unmounts, so clearing one
output does not leave the notebook with no mode.
Theme propagates the brand to nested Astryx children because every
anywidget child is mounted in its own frontend root. See
docs/astryx_brand_theme.md for runtime defineTheme() themes,
precompiled BuiltTheme CSS themes, and the workflow for defining new
Astryx themes. That document also describes the frontend style injection,
deduplication, and JupyterLab testing workflow for theme changes.
Assets
Frontend assets are bundled into the Python package. Runtime notebooks do not need CDN access for Lumino, Astryx, or the bundled workflow icons.
Saving a notebook with widget state stores those assets once per widget model,
which makes such files large. Rerun the examples with a kernel instead, or
execute them with --ExecutePreprocessor.store_widget_state=False.
Development
uv sync --group dev
npm ci
npm run build
uv run pytest
uv run jupyter lab
Build and browse the documentation site:
make docs-preview
This generates the Python API reference from source and NumPy docstrings, then opens
http://127.0.0.1:4200/docs with the Fumadocs development server. Use make docs for
a static export into out/, make docs-serve to serve that export locally, and
make docs-check to run API generation, link tests, linting, type checks, and the
production build. The Griffe-backed fumadocs-python generator owns
content/docs/api; update public signatures and docstrings in src/anylumino
instead of editing generated API pages.
The Docs workflow publishes the same export to GitHub Pages at
https://faysou.github.io/anylumino/docs/ on every push to main.
Open notebooks/astryx_components_smoke.py for the broad Astryx component
smoke test. Open notebooks/astryx_layout_panels.py for the Astryx layout
panels driven from Python. Open notebooks/astryx_table.py for focused table
selection, sorting, and row-helper examples. Open notebooks/plotly_tabs.py and
notebooks/plotly_dashboard.py for Lumino layout patterns with Plotly figures.
Open notebooks/itables_tabs.py to see an itables.widget.ITable inside a tab
with controls that append rows to a pandas DataFrame and refresh the displayed
table.
License
anylumino is MIT licensed and requires Python 3.14 or later. The frontend
bundles compile Lumino, Astryx, StyleX, and React into the wheel;
THIRD_PARTY_LICENSES.md lists those packages with their notices, and
npm run licenses regenerates it from the bundle inputs.
Metadata
Release files for anylumino 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| anylumino-0.3.0.tar.gz | 2.8 MB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| anylumino-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 3.7 MB
Release files / anylumino-0.3.0.tar.gz
| Download URL | anylumino-0.3.0.tar.gz |
|---|---|
| Size | 2.8 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
57cee725dd7bb1910bba9c2364376c7f216c8dd91c7e42fc23bffdcd1b1b12fd
|
|
BLAKE2b-256 checksum How to use checksums |
b9d0b175ecb5410f601b273ee052e11d01651cdbb2180b90f379d5cf73830bbf
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.3 {"installer":{"name":"uv","version":"0.13.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / anylumino-0.3.0-py3-none-any.whl
| Download URL | anylumino-0.3.0-py3-none-any.whl |
|---|---|
| Size | 876.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
fe745557123f169c5dae29232efe38a11074bcfe4541da4622a223c4511d2578
|
|
BLAKE2b-256 checksum How to use checksums |
fc1de77365b42f8b1f7f80c232b4d916264a2a2f4df70a335e5d874bdda64bc4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.3 {"installer":{"name":"uv","version":"0.13.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for anylumino 0.3.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.