Skip to content

[WIP] Add tethys run — zero-config single-file app runner (express mode POC)#1287

Draft
gagelarsen wants to merge 2 commits into
mainfrom
tethys-run-express
Draft

[WIP] Add tethys run — zero-config single-file app runner (express mode POC)#1287
gagelarsen wants to merge 2 commits into
mainfrom
tethys-run-express

Conversation

@gagelarsen

Copy link
Copy Markdown
Contributor

⚠️ Read this first

This is an AI-generated proof of concept. The design and implementation were produced with an AI assistant (Claude) working interactively with @gagelarsen, then verified end-to-end locally. It is a draft for discussion with the Tethys team — not a request to merge as-is. All suggestions, redesigns, and pushback are welcome.

Implements #1286.

What this does

tethys run app.py [-p PORT] [--host HOST] [--no-browser] [--no-reload] [--clean]

Runs a bare, single-file component app — never pip-installed, no pyproject.toml, no portal config, no database setup, no login — with the ergonomics of shiny run / streamlit run:

# app.py — complete and runnable
from tethys_sdk.components import ComponentBase

class App(ComponentBase):
    name = "My Dashboard"

@App.page
def home(lib):
    return lib.tethys.Display(lib.tethys.Map())

First run takes ~4s (silent SQLite migration); subsequent runs ~3s. Hot reload is on by default (Django's stock autoreloader — the app file is an imported module). tethys run with no argument looks for app.py in the cwd.

How it works (design)

"Portal-in-a-box": the existing portal runtime runs unmodified, configured down via an ephemeral generated environment — chosen over (a) a purpose-built minimal runtime (dual-runtime drift risk) and (b) materializing the file into a generated package (codegen indirection). Because everything downstream is stock machinery, an express app is guaranteed to behave identically when later installed in a real portal — the same file drops verbatim into a scaffolded component app's app.py (a future tethys scaffold --from app.py could automate graduation).

  1. tethys_cli/run_commands.py (new): validates the file (AST check for a ComponentBase subclass), creates ~/.tethys/express/<pkg>_<hash-of-abs-path>/ as an isolated TETHYS_HOME, writes a portal_config.yml into it (MULTIPLE_APP_MODE: False, STANDALONE_APP: <pkg>, ENABLE_OPEN_PORTAL: True, persisted SECRET_KEY), runs manage.py migrate --no-input (idempotent, every start), then execs manage.py runserver. Configuration flows through env vars (TETHYS_HOME, TETHYS_EXPRESS_APP) so it survives autoreloader restarts. No changes to settings.py were needed.
  2. tethys_apps/base/express.py (new): when TETHYS_EXPRESS_APP is set, loads the file as tethysapp.<pkg>.app and pre-registers it (plus a synthetic parent package) in sys.modules, so the untouched harvester/register_controllers machinery — including its importlib.reload() — resolves it like an installed app. Missing metadata is synthesized: package/root_url from the filename (parent dir for generic app.py), name title-cased, index = first @App.page function.
  3. tethys_apps/harvester.py (~10 lines): include the express app in the harvest dict.
  4. tethys_apps/base/component_base.py: __init_subclass__ hook calls the metadata synthesis at class-definition time — required because @App.page reads app.package at decoration time (ComponentLibrary keying).

Drive-by bug fixes (pre-existing, single-app mode)

Both were hit during testing and affect non-express portals too — happy to split them into a separate PR:

  • tethys_apps/utilities.py: get_configured_standalone_app() caught Postgres's ProgrammingError but not SQLite's OperationalError, so a fresh single-app portal with reactpy_django installed crashed during migrate (reactpy's app-ready hook imports the URLconf, which queries TethysApp before tables exist).
  • tethys_apps/base/component_base.py: auto nav links hard-coded /apps/<root_url>/, which 404s when MULTIPLE_APP_MODE=False (apps are served at root). Now settings-aware.

Verification performed

  • 28 new unit tests (test_express.py, test_run_commands.py) — all passing; regression run of touched-module test files shows failure sets identical to main (3 pre-existing, unrelated).
  • End-to-end in a real browser (Playwright): page renders, button click round-trips state over the ReactPy websocket, multi-page nav works, hot reload verified by editing the file mid-serve.
  • Built a real dashboard app on it (Utah lakes map + click-for-fish-species from public APIs) as a dogfooding exercise.
  • flake8 and black --check clean.

Known limitations / open questions

  • Component apps only (no classic template apps) — intentional for v1.
  • Hard-wired anonymous (ENABLE_OPEN_PORTAL); no auth flag yet.
  • Multi-file express apps work incidentally (sibling controllers.py etc. are importable via the synthetic package path) but are untested/undocumented.
  • The generated state dir accumulates under ~/.tethys/express/ (per-app, keyed by file path; --clean wipes one app's state). No global GC.
  • requests/network access to esm.sh (React CDN) is required at page load, as with all component apps.
  • Naming (tethys run vs tethys express), the state-dir lifecycle, and whether portal-in-a-box is the right long-term architecture are all up for debate in Feature proposal: tethys run app.py — zero-config single-file app runner ("Tethys Express") #1286.

🤖 Generated with Claude Code

gagelarsen and others added 2 commits July 14, 2026 15:08
Proof-of-concept, Shiny-inspired runner: "tethys run app.py" serves a
single-file component app with no portal configuration, no database
setup, no pip install, and no login. It generates an isolated
TETHYS_HOME (~/.tethys/express/<pkg>_<hash>/) containing a portal
config (single-app mode + open portal) and a throwaway SQLite database,
grafts the app file into the tethysapp namespace via sys.modules, and
launches the standard development server.

- tethys_cli/run_commands.py: new "run" subcommand (-p/--port, --host,
  --no-browser, --no-reload, --clean)
- tethys_apps/base/express.py: express loader + metadata synthesis
  (package/name/root_url/index derived from the file when omitted)
- tethys_apps/harvester.py: include the express app during harvest
- tethys_apps/base/component_base.py: __init_subclass__ hook for
  express metadata; fix auto nav links in single-app mode (/apps/<url>/
  404s when MULTIPLE_APP_MODE=False)
- tethys_apps/utilities.py: catch sqlite OperationalError in
  get_configured_standalone_app (fresh single-app portals crashed
  during migrate when reactpy_django imports the URLconf)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- docs/tethys_cli/run.rst: full reference page (quick start, how it
  works, auto-generated arguments via sphinx-argparse, examples)
- docs/tethys_cli.rst: add run to the CLI toctree
- docs/whats_new.rst: release note entry for express mode
- docs/tethys_sdk/components.rst: tip cross-referencing tethys run
  from the component app.py docs

Docs build verified locally with sphinx (no warnings from these files).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@gagelarsen

Copy link
Copy Markdown
Contributor Author

Added documentation in 35617c4:

  • New CLI reference page (docs/tethys_cli/run.rst): quick start, how-it-works explanation, auto-generated argument docs (sphinx-argparse), and usage examples
  • What's New entry for the release notes
  • Cross-reference tip in the Component Apps SDK docs (app.py section) pointing to tethys run
  • Added run to the CLI docs toctree

Sphinx build verified locally — no warnings from the changed files.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant