Pytincture + DhxPyt
Overview
Build Python-driven UIs with dhxpyt and run them either as full pytincture services or as static browser-only pages. Targets pytincture 1.0.0rc5 and dhxpyt 0.9.18 (the release pytincture 1.0 locks). Service mode needs Python 3.13 or 3.14.
Pick a mode
- Service mode — backend routes, BFF calls, auth, private server Python.
- Standalone mode — one static HTML page, no server. No BFF, no auth, and everything on the page is visible to the user.
Rules that apply to every task
- Implement
load_ui()only. Never override__init__to call it — dhxpyt'sLoadUICallermetaclass already callsload_ui()after construction, so doing both builds the entire UI twice. - Mount widgets with the
add_*helpers (layout.add_grid,tabbar.add_form,layout.add_cardpanel, …) rather than constructing widgets directly. Theidyou pass is an existing cell id, not a new name. BareCardPanel(config, root="#x")fails unless#xalready exists. - Alias same-named configs across modules.
SeparatorConfigexists intoolbar,sidebarandribbonand they are different classes; importing two unqualified silently shadows one.
Task 1: Service-mode app
- Write the browser entrypoint: a module whose top-level class subclasses
MainWindowand implementsload_ui(). - Add
widget.pywith literal__widgetset__/__version__, andimport widgetfrom the entrypoint — the backend discovers the widgetset by walking the entrypoint's imports. - Add BFF data classes with
@backend_for_frontend, plus@bff_policy,@bff_http_methodsor@bff_streamas needed. Call them from the browser through the awaitablename_async()companion — the plainname()form is a blocking XHR, deprecated through 1.x. - Create the ASGI app with
create_app(PytinctureConfig(...))in a separateservice.py, and run it with uvicorn. (launch_service()still works and is the compatibility path for existing code.) - Register a policy hook if any export uses
@bff_policy— without one the service fails closed at startup. Pytincture enforces declared claims itself before the hook;rolesrequires all of them and needs an identity source, so declaringroleson an export a no-login service must serve is an unconditional 403. - Open
/{application}, where{application}is the module filename (py_ui.py→/py_ui), not the class name.
Start from assets/examples/pytincture_app/.
Read references/pytincture.md for the BFF contract,
policy-hook return values, and configuration.
Task 2: dhxpyt UI
- Subclass
MainWindow; implementload_ui(). - Build cells with
add_layout, then mount widgets into those cells with theadd_*helpers. - Configure widgets with
*Configclasses or plain dicts. - Wire events with
.on_*methods (toolbar.on_click(handler)); toolbar and sidebar handlers receive(id, event).
Start from assets/examples/dhxpyt_ui/testui.py.
Read references/dhxpyt.md for patterns and gotchas, and
references/dhxpyt/index.md for the module index
and the full add_* helper table. Load a single module page such as
references/dhxpyt/form.md when you need exact
config parameters — do not load them all.
Task 3: Standalone browser page
- Copy
assets/standalone/index.html. - Export the verified runtime on the build machine:
python -m pytincture.assets ./frontend. - Put your Python in
<script type="text/python">and setentrypointinwindow.pytinctureAutoStartConfig. - Pin every
#micropip-libsentry asname==version(or a wheel URL with#sha256=). Bare names are rejected. - Serve over HTTP(S);
file://does not work.
Read references/pytincture-runtime.md for
configuration keys, explicit startup, and the CDN escape hatch.
Known gotchas
| Symptom | Cause and fix |
|---|---|
| UI renders twice; duplicate toolbars/grids | __init__ calls load_ui() and so does the metaclass. Delete the __init__. |
| ComboConfig.__init__() got an unexpected keyword argument 'options' | Combo items go in data=, not options=. |
| CardPanel: target container not found | Use layout.add_cardpanel(id=<existing cell>, ...), or create the node first with attach_html. |
| Tabbar.add_cardpanel() got an unexpected keyword argument 'panel_config' | The parameter is cardpanel_config=. |
| Grid renders empty despite data | GridConfig(data=...) wants List[Dict]; json.loads() a BFF method that returns JSON text. |
| DeprecationWarning from a BFF call | Use the name_async() companion and await it from a coroutine scheduled with asyncio.ensure_future(). |
| Wrong separator style in a toolbar | sidebar.SeparatorConfig shadowed toolbar.SeparatorConfig. Alias the imports. |
| 404 for dhxpyt-99.99.99-py3-none-any.whl | The runtime fell through to devWheelVersion. Pin widgetlib: "dhxpyt==0.9.18". |
| micropip install rejected | Every entry needs an exact name==version pin or url#sha256=. |
| Service refuses to start, mentions @bff_policy | Register set_bff_policy_hook() or set BFF_POLICY_HOOK_PATH. |
| Policy hook raises RuntimeError | Hooks must return True/False/None, nothing else. |
| Shell renders but every BFF call answers 403 | An export declares roles while no identity source supplies role claims. Drop the requirement, or configure login (AUTH_USER_CLAIMS, an authenticator, or an IdP). |
Resources
references/
pytincture.md— service mode, BFF contract, policy hooks, configpytincture-runtime.md— standalone runtime setup and configuration keysdhxpyt.md— dhxpyt patterns, entrypoint rule, gotchasdhxpyt/index.md— module index and theadd_*helper tabledhxpyt/<module>.md— generated per-module config and widget signaturesexamples.md— index of bundled example assets
assets/
examples/pytincture_app/— full service-mode appexamples/dhxpyt_ui/— minimal dhxpyt UIstandalone/index.html— browser-only page template