Skip to content

Getting Started

oldmangan edited this page Oct 11, 2026 · 3 revisions

wapyt apps run in pyTincture service mode: a Python backend serves the page, the browser runs your UI module in Pyodide, and backend-for-frontend (BFF) classes carry data between them. Targets pyTincture 1.0 release candidates and wapyt 0.2 (0.2.0 is on PyPI); the server needs Python 3.13 or 3.14.

1. Lay out the folder

my_app/
├── service.py            # ASGI app; never sent to the browser
├── dashboard.py          # browser entrypoint (APP_ENTRYPOINT + MainWindow subclass)
├── dashboard_data.py     # @backend_for_frontend class(es)
├── widget.py             # literal widgetset metadata
└── wapyt-99.99.99-py3-none-any.whl   # the browser wheel (step 3)

/dashboard serves dashboard.py: the URL is the module filename, not the class name.

2. Tell pyTincture which widgetset to load

pyTincture reads the entrypoint's imports with ast (it never imports them) looking for literal metadata. A local widget.py is the dependable way:

# widget.py
__widgetset__ = "wapyt"
__version__ = "0.2.0"   # the wapyt version you build from

pyTincture turns this into a pin (wapyt==0.2.0) and the browser installs the matching wheel from the app folder, or the 99.99.99 development wheel from step 3, which it accepts whatever the pin says. Keep it equal to the version of the wapyt checkout or release you use. From PyPI, development and release-candidate versions need pip install --pre wapyt.

# dashboard.py
import widget  # noqa: F401

Importing wapyt directly also works when wapyt is installed in the server's Python, but only as a regular install: with pyTincture ≤ 1.0.0rc12 an editable (pip install -e) install is not discovered and the app boots with no widgets.

3. Ship the browser wheel

The browser installs a wheel, not your checkout, and wapyt is neither on PyPI nor in pyTincture's built-in wheel locks. Build it into the app folder:

/path/to/wa_pytincture_widgetset/scripts/dev_wheel.sh /path/to/my_app

It builds at the development version 99.99.99 (pyTincture's PYTINCTURE_DEV_WHEEL_VERSION), regenerates the hash manifest and copies the wheel in. Re-run it after any wapyt change and restart the service.

4. Declare the entrypoint

pyTincture's automatic MainWindow detection only recognises dhxpyt's class. Either name your class after the module (class dashboard(MainWindow) in dashboard.py) or declare it:

APP_ENTRYPOINT = "Dashboard"

Without one of the two, the page answers HTTP 422.

5. Build the UI in load_ui()

class Dashboard(MainWindow):
    def load_ui(self):      # never define __init__ to call this
        ...

MainWindow's metaclass calls load_ui() after construction. Calling it from your own __init__ as well builds everything twice.

6. Write service.py

from pathlib import Path
from pytincture import PytinctureConfig, create_app

HERE = Path(__file__).resolve().parent

def policy_hook(user, policy, class_name, function_name, **kwargs):
    return True     # extra checks beyond the claims pyTincture enforces itself

app = create_app(PytinctureConfig(
    modules_path=str(HERE),
    default_application="dashboard",
    # Only needed when an export uses @bff_policy. Register by dotted path:
    # set_bff_policy_hook() does not reach an app built by create_app().
    environment={"BFF_POLICY_HOOK_PATH": "service.policy_hook"},
))
python -m uvicorn service:app --port 8070    # http://127.0.0.1:8070/dashboard

7. Load data from a BFF

# dashboard_data.py
from pytincture.dataclass import backend_for_frontend

@backend_for_frontend
class DashboardData:
    def rows(self) -> list[dict]:
        return [{"id": "1", "name": "From the server"}]
# dashboard.py
import asyncio
from dashboard_data import DashboardData

    def load_ui(self):
        self.table = self.add_datatable("main", DataTableConfig(...))
        asyncio.ensure_future(self.refresh())

    async def refresh(self):
        self.table.set_rows(await DashboardData().rows_async())

Call the generated name_async() companion; the plain name() is a blocking request. pyTincture re-executes a BFF module for every call, so keep state in storage, never in module globals. See Events and Async.

Standalone pages

pyTincture can also run browser-only pages that install the widgetset from an index. wapyt is not published to one, so wapyt apps use service mode.

Clone this wiki locally