`workspace/` is runtime state (per-user folders, no-auth dev's `code/`) and shouldn't be in git. The same files were previously committed under both `workspace/code/` and `src/static/bundled-demos/`, which forced a Docker `diff -q` sync check and leaked user-scoped paths into version control. - /workspace/ added to .gitignore; all previously tracked files removed via `git rm --cached`. - src/static/bundled-demos/ becomes the single source of truth: panel16 demos, led_tutorial, led_patterns, neopixel demos, and main.py move here alongside the existing canonical demos. - New BUNDLED_DEMOS_DIR config; user_workspace seeders read from it. - main.py lifespan seeds WORKSPACE_ROOT/code/ on startup so a fresh clone running `pipenv run dev` still gets the full sample set (existing files never overwritten — user edits survive restarts). - Dockerfile drops `COPY workspace` and the diff sanity check. - README/LED_TUTORIAL repointed at the new canonical paths. - test_led_patterns loads led_patterns.py from bundled-demos. - test_api uses mkdir(exist_ok=True) for `code/` (startup pre-creates). Co-authored-by: Cursor <cursoragent@cursor.com>
159 lines
9.1 KiB
Markdown
159 lines
9.1 KiB
Markdown
# 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=<token>`.
|
||
|
||
When auth is enabled, file APIs use a per-user workspace under `WORKSPACE_ROOT/users/<username-id>/` 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=<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/<file>.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.
|