# python-editor Browser-based Python editing: **FastAPI** serves static assets, stores workspace files, and optional **API key auth**. **Pyodide** runs your scripts and **Jedi** (inside Pyodide) powers completions and syntax diagnostics — no server-side Python execution or LSP process. ## Run ```bash cp .env.example .env # optional: set WORKSPACE_ROOT, EDITOR_API_KEY, etc. pipenv install pipenv run dev ``` Configuration is read from **`.env`** at the repo root (see `.env.example`). Values there are applied when the app loads unless the variable is already set in your shell. [Pipenv](https://pipenv.pypa.io/) also loads `.env` for `pipenv run` commands. Tests (includes **pytest** and **selenium** in dev dependencies): ```bash pipenv run test pipenv run test-integration # Playwright; optional ``` ### Selenium Selenium talks to a **real browser** against a **running server** (not the in-process `TestClient`). 1. Install **Google Chrome** or Chromium on the machine (Selenium 4 uses [Selenium Manager](https://www.selenium.dev/documentation/selenium_manager/) to resolve a matching driver). 2. In one terminal, start the app (default `http://127.0.0.1:8080`): ```bash pipenv run dev ``` 3. In another terminal: ```bash pipenv run test-selenium ``` If the app listens elsewhere, set **`SELENIUM_BASE_URL`** (e.g. `http://127.0.0.1:9000`) before running. Or run only Selenium-marked tests: ```bash cd src && PYTHONPATH=. pipenv run pytest ../tests -m selenium -v ``` If nothing is listening, the smoke test **skips** with a short message instead of failing. Open [http://localhost:8080](http://localhost:8080). ### Editor runtime controls - `Run Python` runs the active open `.py` tab. - Enable `Run main.py` to always run `code/main.py` instead. - Pressing `Run Python` while a script is running will stop and restart with the selected target. - `LSP` badge in the header shows in-browser Jedi syntax status (`n/a`, `checking...`, `OK`, or issue count). ## Deploy with Docker Build and run with Docker Compose: ```bash cp .env.example .env mkdir -p data docker compose up --build ``` Then open [http://localhost:8080](http://localhost:8080). Notes: - `workspace/` is mounted to `/app/workspace` so your code persists locally. - `data/` is mounted to `/app/data` for the SQLite auth DB. - In container mode, `WORKSPACE_ROOT` and `AUTH_DATABASE_PATH` are set by `docker-compose.yml`. **User accounts** — Set `AUTH_ENABLED=true` in `.env` to require sign-in for workspace APIs. Users live in a SQLite file (`AUTH_DATABASE_PATH`, default `./data/editor.db`). Use `/register` with an invite link (unless you opt into open signup) or `BOOTSTRAP_ADMIN_USERNAME` / `BOOTSTRAP_ADMIN_PASSWORD` for the first superuser. Superusers can **GET `/api/users`**, **PATCH `/api/users/{id}`** (username, password reset, admin flag — renames workspace folder when the username changes), or **DELETE `/api/users/{id}`** to manage accounts. New accounts are added only through **invite links** (**`POST /api/users/invites`**) plus self-service registration (`/register?invite=…`). Email invite signup: - By default **`AUTH_INVITE_ONLY=true`**: registrations need a valid invite token. Set **`AUTH_INVITE_ONLY=false`** to allow open signup whenever **`AUTH_REGISTER_OPEN=true`**. - Superusers can create invites via `POST /api/users/invites` with `{ "email": "...", "expires_days": 7 }`. - Response includes `invite_url`; if SMTP is configured the invite email is sent automatically. - Registration page accepts invite links like `/register?invite=`. When auth is enabled, file APIs use a per-user workspace under `WORKSPACE_ROOT/users//` for **isolated `code/`**. The `lib/` tree is shared and read-only for all users. When auth is disabled, the shared workspace root is used for everything. Admins can open another user's workspace from the home page user management panel (links to `/editor?workspace_user_id=`). Only superusers may use this override. **API key** — If `EDITOR_API_KEY` is set, requests may use `Authorization: Bearer …` instead of a session (useful for automation). When `AUTH_ENABLED=true`, a valid session *or* API key is accepted. The home page can store the API key in `sessionStorage` when you are not using cookie login, or use `?api_key=` on `/editor`. **Local mode (no login)** — Click *Use locally* on the home page (or open `/editor?local=1`) to run the editor without any FastAPI auth. The boot-time auth probe is skipped when local mode is active, so this works even on a host that has `AUTH_ENABLED=true`. Files default to the browser's **IndexedDB**; inside the editor the workspace badge has a **Folder…** button that opens `window.showDirectoryPicker()` so you can save straight to any folder on disk (Chromium-only — Firefox/Safari stay on IndexedDB). The picked directory handle is persisted across reloads in IndexedDB; if browser permission lapses a *Reconnect* button reappears in the badge. Nothing is sent to the server for file reads/writes. The MicroPython stubs are loaded from **`/static/bundled-lib/*.py`** (files under `src/static/bundled-lib/` in the repo) so a plain static file server is enough; if those requests fail, the app falls back to `GET /api/public/lib-bundle` when FastAPI is available. For static-only hosting, run `python scripts/serve_static_editor.py` from the repo root — it serves `src/static/` with the same `/static/…` URLs the HTML expects (it strips the `/static` prefix when resolving files), rewrites `/editor` → `index.html`, and sends the same COOP/COEP headers as the full app so **ADC sliders, pin toggles, and serial I/O** keep using `SharedArrayBuffer` on mobile Safari and Chrome where supported. An *Exit* button in the editor's workspace badge clears the local-mode flag (your IndexedDB files stay until you wipe browser storage). ## Layout - `src/` — FastAPI app and static UI (`src/static/`) - `lib/` — bundled MicroPython stubs, served read-only as `lib/` in the editor and merged into Pyodide at run time (single source of truth) - `workspace/` — default `WORKSPACE_ROOT`: `code/` samples and per-user folders (editable); the editor surfaces the repo `lib/` alongside it without copying anything to disk ## ESP32 / NeoPixel mock The browser runtime ships MicroPython-style stubs in repo `lib/` (they appear as `lib/` in the editor and are read-only via the APIs): - `machine.Pin` — `value/on/off/toggle/high/low/init/__call__/irq` plus a live "Pins" panel: OUT pins show an indicator, IN pins expose a clickable toggle button (its value is what `Pin.value()` returns), `irq()` fires on rising / falling edges as you click - `machine.PWM` — `freq()` / `duty()` / `duty_u16()` / `duty_ns()` with a duty-cycle bar in the Pins panel - `machine.ADC` — backed by a live slider in the editor UI (one slider per pin, `read_u16()` returns 0..65535) - `machine.UART` — opens a Serial Monitor pane; `write()` text appears there, what you type is delivered via `read()` / `readline()` - `neopixel.NeoPixel` - `utime` — `ticks_ms`, `ticks_diff`, `ticks_add`, `sleep_ms`, `sleep_us`, `sleep` - `micropython.const` — no-op helper for ported constant declarations Use them from scripts in `code/` (your editor workspace, populated on first run from `src/static/bundled-demos/`) like typical ESP32 / MicroPython examples: ```python from machine import Pin import neopixel np = neopixel.NeoPixel(Pin(4), 8) np[0] = (255, 0, 0) np.write() ``` `write()` updates the NeoPixel simulator so you can verify behavior visually. Simulator modes: - Default: in-app LED strip/panel section under the editor. - `16x16 panel` checkbox: opens a dedicated popup with 16x16 serpentine mapping: - first LED at top-right - first row goes right -> left - rows zig-zag left/right. - The 16x16 popup closes automatically on **Stop** or when script execution finishes. Tutorial files (canonical source — committed under `src/static/bundled-demos/`; copies appear in your editor's `code/` folder on first run): - `LED_TUTORIAL.md` - step-by-step NeoPixel tutorial - `led_tutorial.py` - runnable guided LED example - `led_patterns.py` - shared pattern helpers (used by automated tests); each `pattern_*_demo.py` duplicates what it needs and uses only Python stdlib + `machine` / `neopixel` / `time` - `pattern_rainbow_demo.py` - rainbow animation (self-contained) - `pattern_chase_demo.py` - Knight Rider–style bouncing scanner (self-contained) - `pattern_twinkle_demo.py` - twinkle animation (self-contained) - `panel16_utils.py` - helpers for 16x16 serpentine mapping - `panel16_rainbow_wave.py` - 16x16 rainbow wave - `panel16_bounce.py` - 16x16 bouncing pixel with trail - `panel16_matrix_rain.py` - 16x16 matrix rain effect > `workspace/` is gitignored runtime state. To edit the **shipped** demo source, edit `src/static/bundled-demos/.py` and re-run "Reset demos" in the editor (or restart the dev server with an empty `workspace/code/`). ## Dev auto-reload hook Project hook files are included in `.cursor/`: - `.cursor/hooks.json` - `.cursor/hooks/dev-reload-touch.sh` When files are edited through Cursor tools, the hook updates `src/static/.reload-token`. The editor (on localhost) polls that token and auto-reloads the browser when it changes.