← Docs index · ← Project README
- Overview: one distribution, three import surfaces
- Install (users)
- Develop (from source)
- Using the SDK
- Build & publish
- The node library
PICOTTY ships as a single distribution — picotty (version single-sourced from
hub/pyproject.toml, license GPL-3.0-or-later) built with the uv_build backend
from the src/ layout under hub/. One wheel, three things you can import:
| Import surface | What it is | Needs |
|---|---|---|
picotty.protocol |
The wire protocol — PROTOCOL_VERSION, validate_send, encode_frame, read_frame, ProtocolError |
base install |
picotty.client |
The SDK — HubClient (async REST) + HubEvents (WebSocket async-iterator) |
base install |
picotty.hub |
The server (FastAPI app, picotty-hub) |
[hub] extra |
The base install is deliberately lean — only httpx and websockets. Everything
that only a client needs (the SDK, the Telegram sidecar, a cron health check, CI)
runs on that alone. The server's heavier stack lives behind extras so it is never
dragged in by accident:
[hub]—fastapi,uvicorn[standard],aiosqlite,pydantic,pyyaml.[telegram]—python-telegram-bot[rate-limiter],pyotp.
The point of the split is size and blast radius: a Pi Zero 2 W running only the
Telegram sidecar installs picotty[telegram] and never pulls FastAPI or uvicorn, and
a machine that just talks to a remote hub installs bare picotty. Dev tooling
(pytest) lives in a uv dependency group (dev), not an extra, so it never leaks
into a user install.
Install the tool in its own isolated environment with uv:
uv tool install picotty # base: SDK + protocol + both CLIs
uv tool install 'picotty[hub]' # add the server stack (to run the hub)That puts two console entry points on PATH:
| Command | Entry point | Does |
|---|---|---|
picotty-hub |
picotty.hub.main:main |
Runs the hub server (needs [hub]). |
picotty-sim |
picotty.sim:main |
The node simulator — a fake node for demos/tests. |
picotty-hub # serve the dashboard + REST/WS
picotty-sim --id demo --token <TOK> # dial a fake node into itUpgrade in place with uv tool upgrade picotty.
Runtime state lives out of the install tree. The hub's SQLite database defaults to
~/.local/share/picotty/hub.db (honoring XDG_DATA_HOME); a systemd unit that sets
StateDirectory=picotty gets /var/lib/picotty. The dashboard's static assets ship
inside the wheel and are loaded via importlib.resources. Two env overrides:
| Variable | Overrides | Default |
|---|---|---|
HUB_DB_PATH |
SQLite database path | $XDG_DATA_HOME/picotty/hub.db → ~/.local/share/picotty/hub.db |
HUB_STATIC_DIR |
Dashboard static-asset directory | the assets bundled in the wheel |
git clone https://github.com/morpheuslord/PICOTTY
cd PICOTTY/hub
uv sync --extra hub # creates hub/.venv with the server stack + dev groupRun the CLIs and the tests through uv run (no manual venv activation):
uv run --extra hub picotty-hub
uv run picotty-sim --id demo --token <TOK>
uv run python tests/test_db.py
uv run python tests/test_integration.pyFetch the vendored terminal libraries first for anything that serves the dashboard. xterm and the asciinema player are vendored (gitignored) and pulled by a script; run it once before serving or building so the assets exist:
bash src/picotty/static/vendor/fetch-vendor.shpicotty.client is the lean way for a program to drive a running hub — REST plus the
same /ws event feed the dashboard uses. It needs only the base install (no
[hub], no FastAPI):
import asyncio
from picotty.client import HubClient
async def main():
async with HubClient("http://hub:8080") as hub:
print(await hub.health())
print(await hub.nodes())
# HubEvents: async-iterate the live event stream, per-node subscribe
async with hub.events_stream() as stream:
await stream.subscribe("node-01")
async for ev in stream:
print(ev["event"], ev)
asyncio.run(main())The Telegram sidecar (telegram-bot/) is built exactly this way: it depends on
picotty[telegram] and imports picotty.client — never the server.
The wheel bundles the dashboard assets, so fetch the vendored libraries before you build or the wheel ships without a working terminal:
cd hub
bash src/picotty/static/vendor/fetch-vendor.sh # MUST run before uv build
uv build # -> dist/*.whl + dist/*.tar.gzInspect what landed before publishing:
ls -l dist/
python -m zipfile -l dist/picotty-1.0.0-*.whl | grep -E 'static/vendor' # vendored assets present?Publish with uv:
uv publish # dist/*.whl + *.tar.gz -> PyPIThe version is single-sourced in hub/pyproject.toml (__init__.py reads it back
from installed metadata) — bump it in one place, tag, and CI does the rest.
Publishing is wired to GitHub Releases with PyPI Trusted Publishing (OIDC,
no long-lived token) — .github/workflows/publish.yml.
When you publish a release, the workflow builds the distributions with uv (fetching
the vendored assets first), attaches the wheel + sdist to the GitHub Release,
and uploads them to PyPI with pypa/gh-action-pypi-publish, so the GitHub Release
and the PyPI project stay linked. (A Python package doesn't appear under the repo's
Packages menu — that registry has no PyPI backend — so the Release assets are
the GitHub-side artifact.)
One-time setup (owner morpheus_lord):
- PyPI → project
picotty→ Publishing → add a trusted publisher (a pending publisher if the project doesn't exist yet): owner/repomorpheuslord/PICOTTY, workflowpublish.yml, environmentpypi. - GitHub → Settings → Environments → create an environment named
pypi(add an approval protection rule if you want a manual gate before upload).
Then each release ships itself:
GitHub → Releases → Draft a new release
tag: v1.0.0 (create it)
title: v1.0.0
notes: (the release notes)
Publish → publish.yml builds + uploads to PyPI
Manual fallback (from a laptop, or if OIDC isn't set up yet): build and push with an API token —
cd hub && bash src/picotty/static/vendor/fetch-vendor.sh && uv build
UV_PUBLISH_TOKEN=pypi-... uv publishNever commit a token; the CI workflow embeds none.
The CircuitPython node library is a documented follow-up — the on-device firmware
packaged as installable .mpy bundles is deferred. See
node/README.md for the current status.