supython 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- supython/__init__.py +24 -0
- supython/admin/__init__.py +3 -0
- supython/admin/api/__init__.py +24 -0
- supython/admin/api/auth.py +118 -0
- supython/admin/api/auth_templates.py +67 -0
- supython/admin/api/auth_users.py +225 -0
- supython/admin/api/db.py +174 -0
- supython/admin/api/functions.py +92 -0
- supython/admin/api/jobs.py +192 -0
- supython/admin/api/ops.py +224 -0
- supython/admin/api/realtime.py +281 -0
- supython/admin/api/service_auth.py +49 -0
- supython/admin/api/service_auth_templates.py +83 -0
- supython/admin/api/service_auth_users.py +346 -0
- supython/admin/api/service_db.py +214 -0
- supython/admin/api/service_functions.py +287 -0
- supython/admin/api/service_jobs.py +282 -0
- supython/admin/api/service_ops.py +213 -0
- supython/admin/api/service_realtime.py +30 -0
- supython/admin/api/service_storage.py +220 -0
- supython/admin/api/storage.py +117 -0
- supython/admin/api/system.py +37 -0
- supython/admin/audit.py +29 -0
- supython/admin/deps.py +22 -0
- supython/admin/errors.py +16 -0
- supython/admin/schemas.py +310 -0
- supython/admin/session.py +52 -0
- supython/admin/spa.py +38 -0
- supython/admin/static/assets/Alert-dluGVkos.js +49 -0
- supython/admin/static/assets/Audit-Njung3HI.js +2 -0
- supython/admin/static/assets/Backups-DzPlFgrm.js +2 -0
- supython/admin/static/assets/Buckets-ByacGkU1.js +2 -0
- supython/admin/static/assets/Channels-BoIuTtam.js +353 -0
- supython/admin/static/assets/ChevronRight-CtQH1EQ1.js +2 -0
- supython/admin/static/assets/CodeViewer-Bqy7-wvH.js +2 -0
- supython/admin/static/assets/Crons-B67vc39F.js +2 -0
- supython/admin/static/assets/DashboardView-CUTFVL6k.js +2 -0
- supython/admin/static/assets/DataTable-COAAWEft.js +747 -0
- supython/admin/static/assets/DescriptionsItem-P8JUDaBs.js +75 -0
- supython/admin/static/assets/DrawerContent-TpYTFgF1.js +139 -0
- supython/admin/static/assets/Empty-cr2r7e2u.js +25 -0
- supython/admin/static/assets/EmptyState-DeDck-OL.js +2 -0
- supython/admin/static/assets/Grid-hFkp9F4P.js +2 -0
- supython/admin/static/assets/Input-DppYTq9C.js +259 -0
- supython/admin/static/assets/Invoke-DW3Nveeh.js +2 -0
- supython/admin/static/assets/JsonField-DibyJgun.js +2 -0
- supython/admin/static/assets/LoginView-BjLyE3Ds.css +1 -0
- supython/admin/static/assets/LoginView-CoOjECT_.js +111 -0
- supython/admin/static/assets/Logs-D9WYrnIT.js +2 -0
- supython/admin/static/assets/Logs-DS1XPa0h.css +1 -0
- supython/admin/static/assets/Migrations-DOSC2ddQ.js +2 -0
- supython/admin/static/assets/ObjectBrowser-_5w8vOX8.js +2 -0
- supython/admin/static/assets/Queue-CywZs6vI.js +2 -0
- supython/admin/static/assets/RefreshTokens-Ccjr53jg.js +2 -0
- supython/admin/static/assets/RlsEditor-BSlH9vSc.js +2 -0
- supython/admin/static/assets/Routes-BiLXE49D.js +2 -0
- supython/admin/static/assets/Routes-C-ianIGD.css +1 -0
- supython/admin/static/assets/SchemaBrowser-DKy2_KQi.css +1 -0
- supython/admin/static/assets/SchemaBrowser-XFvFbtDB.js +2 -0
- supython/admin/static/assets/Select-DIzZyRZb.js +434 -0
- supython/admin/static/assets/Space-n5-XcguU.js +400 -0
- supython/admin/static/assets/SqlEditor-b8pTsILY.js +3 -0
- supython/admin/static/assets/SqlWorkspace-BUS7IntH.js +104 -0
- supython/admin/static/assets/TableData-CQIagLKn.js +2 -0
- supython/admin/static/assets/Tag-D1fOKpTH.js +72 -0
- supython/admin/static/assets/Templates-BS-ugkdq.js +2 -0
- supython/admin/static/assets/Thing-CEAniuMg.js +107 -0
- supython/admin/static/assets/Users-wzwajhlh.js +2 -0
- supython/admin/static/assets/_plugin-vue_export-helper-DGA9ry_j.js +1 -0
- supython/admin/static/assets/dist-VXIJLCYq.js +13 -0
- supython/admin/static/assets/format-length-CGCY1rMh.js +2 -0
- supython/admin/static/assets/get-Ca6unauB.js +2 -0
- supython/admin/static/assets/index-CeE6v959.js +951 -0
- supython/admin/static/assets/pinia-COXwfrOX.js +2 -0
- supython/admin/static/assets/resources-Bt6thQCD.js +44 -0
- supython/admin/static/assets/use-locale-mtgM0a3a.js +2 -0
- supython/admin/static/assets/use-merged-state-BvhkaHNX.js +2 -0
- supython/admin/static/assets/useConfirm-tMjvBFXR.js +2 -0
- supython/admin/static/assets/useResource-C_rJCY8C.js +2 -0
- supython/admin/static/assets/useTable-CnZc5zhi.js +363 -0
- supython/admin/static/assets/useTable-Dg0XlRlq.css +1 -0
- supython/admin/static/assets/useToast-DsZKx0IX.js +2 -0
- supython/admin/static/assets/utils-sbXoq7Ir.js +2 -0
- supython/admin/static/favicon.svg +1 -0
- supython/admin/static/icons.svg +24 -0
- supython/admin/static/index.html +24 -0
- supython/app.py +162 -0
- supython/auth/__init__.py +3 -0
- supython/auth/_email_job.py +11 -0
- supython/auth/providers/__init__.py +34 -0
- supython/auth/providers/github.py +22 -0
- supython/auth/providers/google.py +19 -0
- supython/auth/providers/oauth.py +56 -0
- supython/auth/providers/registry.py +16 -0
- supython/auth/ratelimit.py +39 -0
- supython/auth/router.py +282 -0
- supython/auth/schemas.py +79 -0
- supython/auth/service.py +587 -0
- supython/backups/__init__.py +24 -0
- supython/backups/_backup_job.py +170 -0
- supython/backups/schemas.py +18 -0
- supython/backups/service.py +217 -0
- supython/body_size.py +184 -0
- supython/cli.py +1663 -0
- supython/client/__init__.py +67 -0
- supython/client/_auth.py +249 -0
- supython/client/_client.py +145 -0
- supython/client/_config.py +92 -0
- supython/client/_functions.py +69 -0
- supython/client/_storage.py +255 -0
- supython/client/py.typed +0 -0
- supython/db.py +151 -0
- supython/db_admin.py +8 -0
- supython/extensions.py +36 -0
- supython/functions/__init__.py +19 -0
- supython/functions/context.py +262 -0
- supython/functions/loader.py +307 -0
- supython/functions/router.py +228 -0
- supython/functions/schemas.py +50 -0
- supython/gen/__init__.py +5 -0
- supython/gen/_introspect.py +137 -0
- supython/gen/types_py.py +270 -0
- supython/gen/types_ts.py +365 -0
- supython/health.py +229 -0
- supython/hooks.py +117 -0
- supython/jobs/__init__.py +31 -0
- supython/jobs/backends.py +97 -0
- supython/jobs/context.py +58 -0
- supython/jobs/cron.py +152 -0
- supython/jobs/cron_inproc.py +119 -0
- supython/jobs/decorators.py +76 -0
- supython/jobs/registry.py +79 -0
- supython/jobs/router.py +136 -0
- supython/jobs/schemas.py +92 -0
- supython/jobs/service.py +311 -0
- supython/jobs/worker.py +219 -0
- supython/jwks.py +257 -0
- supython/keyset.py +279 -0
- supython/logging_config.py +291 -0
- supython/mail.py +33 -0
- supython/mailer.py +65 -0
- supython/migrate.py +81 -0
- supython/migrations/0001_extensions_and_roles.sql +46 -0
- supython/migrations/0002_auth_schema.sql +66 -0
- supython/migrations/0003_demo_todos.sql +42 -0
- supython/migrations/0004_auth_v0_2.sql +47 -0
- supython/migrations/0005_storage_schema.sql +117 -0
- supython/migrations/0006_realtime_schema.sql +206 -0
- supython/migrations/0007_jobs_schema.sql +254 -0
- supython/migrations/0008_jobs_last_error.sql +56 -0
- supython/migrations/0009_auth_rate_limits.sql +33 -0
- supython/migrations/0010_worker_heartbeat.sql +14 -0
- supython/migrations/0011_admin_schema.sql +45 -0
- supython/migrations/0012_auth_banned_until.sql +10 -0
- supython/migrations/0013_email_templates.sql +19 -0
- supython/migrations/0014_realtime_payload_warning.sql +96 -0
- supython/migrations/0015_backups_schema.sql +14 -0
- supython/passwords.py +15 -0
- supython/realtime/__init__.py +6 -0
- supython/realtime/broker.py +814 -0
- supython/realtime/protocol.py +234 -0
- supython/realtime/router.py +184 -0
- supython/realtime/schemas.py +207 -0
- supython/realtime/service.py +261 -0
- supython/realtime/topics.py +175 -0
- supython/realtime/websocket.py +586 -0
- supython/scaffold/__init__.py +5 -0
- supython/scaffold/init_project.py +144 -0
- supython/scaffold/templates/Caddyfile.tmpl +4 -0
- supython/scaffold/templates/README.md.tmpl +22 -0
- supython/scaffold/templates/apps_hooks.py.tmpl +11 -0
- supython/scaffold/templates/apps_jobs.py.tmpl +8 -0
- supython/scaffold/templates/asgi.py.tmpl +14 -0
- supython/scaffold/templates/docker-compose.prod.yml.tmpl +84 -0
- supython/scaffold/templates/docker-compose.yml.tmpl +45 -0
- supython/scaffold/templates/docker_postgres_Dockerfile.tmpl +9 -0
- supython/scaffold/templates/docker_postgres_postgresql.conf.tmpl +3 -0
- supython/scaffold/templates/env.example.tmpl +168 -0
- supython/scaffold/templates/functions_README.md.tmpl +21 -0
- supython/scaffold/templates/gitignore.tmpl +14 -0
- supython/scaffold/templates/manage.py.tmpl +11 -0
- supython/scaffold/templates/migrations/.gitkeep +0 -0
- supython/scaffold/templates/package_init.py.tmpl +1 -0
- supython/scaffold/templates/settings.py.tmpl +31 -0
- supython/secretset.py +347 -0
- supython/security_headers.py +78 -0
- supython/settings.py +244 -0
- supython/settings_module.py +117 -0
- supython/storage/__init__.py +5 -0
- supython/storage/backends.py +392 -0
- supython/storage/router.py +341 -0
- supython/storage/schemas.py +50 -0
- supython/storage/service.py +445 -0
- supython/storage/signing.py +119 -0
- supython/tokens.py +85 -0
- supython-0.1.0.dist-info/METADATA +756 -0
- supython-0.1.0.dist-info/RECORD +200 -0
- supython-0.1.0.dist-info/WHEEL +4 -0
- supython-0.1.0.dist-info/entry_points.txt +2 -0
- supython-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: supython
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A lightweight Postgres-first BaaS framework for Python
|
|
5
|
+
Project-URL: Homepage, https://github.com/Tkeby/supython
|
|
6
|
+
Project-URL: Repository, https://github.com/Tkeby/supython
|
|
7
|
+
Project-URL: Issues, https://github.com/Tkeby/supython/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/Tkeby/supython/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: Documentation, https://github.com/Tkeby/supython/blob/main/docs/PROJECT.md
|
|
10
|
+
Author-email: Tkeby <tsegaw.dev@gmail.com>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: auth,baas,fastapi,postgres,postgrest
|
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
|
15
|
+
Classifier: Framework :: FastAPI
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Database
|
|
23
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.11
|
|
27
|
+
Requires-Dist: aiosmtplib>=3.0
|
|
28
|
+
Requires-Dist: argon2-cffi>=23.1
|
|
29
|
+
Requires-Dist: asyncpg>=0.30
|
|
30
|
+
Requires-Dist: authlib>=1.3
|
|
31
|
+
Requires-Dist: email-validator>=2.2
|
|
32
|
+
Requires-Dist: fastapi>=0.115
|
|
33
|
+
Requires-Dist: httpx>=0.28
|
|
34
|
+
Requires-Dist: itsdangerous>=2.2
|
|
35
|
+
Requires-Dist: pydantic-settings>=2.6
|
|
36
|
+
Requires-Dist: pydantic>=2.9
|
|
37
|
+
Requires-Dist: pyjwt[crypto]>=2.10
|
|
38
|
+
Requires-Dist: python-multipart>=0.0.20
|
|
39
|
+
Requires-Dist: typer>=0.15
|
|
40
|
+
Requires-Dist: uvicorn[standard]>=0.32
|
|
41
|
+
Provides-Extra: arq
|
|
42
|
+
Requires-Dist: arq>=0.26; extra == 'arq'
|
|
43
|
+
Provides-Extra: client
|
|
44
|
+
Requires-Dist: httpx>=0.28; extra == 'client'
|
|
45
|
+
Requires-Dist: postgrest-py>=0.16.0; extra == 'client'
|
|
46
|
+
Requires-Dist: realtime>=2.0.0; extra == 'client'
|
|
47
|
+
Provides-Extra: cron-inproc
|
|
48
|
+
Requires-Dist: croniter>=2.0; extra == 'cron-inproc'
|
|
49
|
+
Provides-Extra: dev
|
|
50
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
51
|
+
Requires-Dist: pytest>=8.3; extra == 'dev'
|
|
52
|
+
Requires-Dist: ruff>=0.8; extra == 'dev'
|
|
53
|
+
Provides-Extra: dramatiq
|
|
54
|
+
Requires-Dist: dramatiq>=1.17; extra == 'dramatiq'
|
|
55
|
+
Provides-Extra: s3
|
|
56
|
+
Requires-Dist: aioboto3>=13.0; extra == 's3'
|
|
57
|
+
Description-Content-Type: text/markdown
|
|
58
|
+
|
|
59
|
+
# supython
|
|
60
|
+
|
|
61
|
+
> A lightweight, Postgres-first BaaS framework for Python. **v0.1.0 release**
|
|
62
|
+
|
|
63
|
+
supython is the inverse of Django: **the database owns the schema, Python owns
|
|
64
|
+
the things SQL is bad at**. It leans on [PostgREST](https://postgrest.org)
|
|
65
|
+
for auto-generated REST APIs and on Postgres' own RLS for authorization,
|
|
66
|
+
while a small FastAPI service in Python handles auth, JWT issuance, realtime,
|
|
67
|
+
storage, functions, workers, and an optional admin control plane.
|
|
68
|
+
|
|
69
|
+
supython is for a specific person with a specific problem:
|
|
70
|
+
> A developer who wants to build a CRUD-heavy web app (most apps are), who thinks in SQL, who wants Postgres to own authorization, and who wants auth + storage + custom logic without assembling the integration themselves.
|
|
71
|
+
|
|
72
|
+
Shipped [v0.1.0]:
|
|
73
|
+
|
|
74
|
+
**Core platform**
|
|
75
|
+
- **Email/password auth** — signup, login, refresh-token rotation with **reuse detection**
|
|
76
|
+
- **OAuth** (Google + GitHub) via `authlib` with PKCE
|
|
77
|
+
- **Password reset**, **magic link**, and **email OTP** (pluggable email backend)
|
|
78
|
+
- **RS256 JWT** — asymmetric signing; PostgREST verifies via shared JWKS; zero-downtime key rotation
|
|
79
|
+
- **Rate limiting** on auth endpoints (per-IP fixed-window counters)
|
|
80
|
+
- **Row-Level Security** — `auth.uid()` helper, `request.jwt.claims` GUC, role-scoped DB access via `db.as_role()`
|
|
81
|
+
- **`supython init`** — scaffold a new project in one command
|
|
82
|
+
- **`supython gen types --lang py`** — emit typed dataclasses + TypedDicts from your Postgres schema
|
|
83
|
+
- **zero Python CRUD code** — every read, write, filter, sort, and pagination is served by PostgREST under RLS
|
|
84
|
+
|
|
85
|
+
**Storage & functions**
|
|
86
|
+
- **S3/local storage** with RLS-on-metadata, signed URLs, multipart upload, and range download
|
|
87
|
+
- **Edge functions** from a `functions/` directory with hot reload and role-scoped DB access
|
|
88
|
+
|
|
89
|
+
**Realtime**
|
|
90
|
+
- **WebSocket Realtime** — `postgres_changes`, `broadcast`, `presence` with per-subscriber RLS filtering
|
|
91
|
+
- **Phoenix Channels wire format** — unmodified `supabase-js` / `supabase-py` SDKs connect
|
|
92
|
+
- **Generic trigger** — `realtime.enable('public.todos')` opts any table in
|
|
93
|
+
- **Two-browser chat demo** — `examples/chat.html` (zero build step)
|
|
94
|
+
|
|
95
|
+
**Jobs & cron**
|
|
96
|
+
- **Job queue** — Postgres-backed (`SELECT FOR UPDATE SKIP LOCKED`), idempotent enqueue, retry with backoff
|
|
97
|
+
- **Cron scheduling** — `pg_cron` (primary) or in-process `croniter` fallback
|
|
98
|
+
- **Generic hooks** — `@app.on_signup` / `@app.on_login` lifecycle hooks
|
|
99
|
+
- **`supython worker run`** — long-running worker with graceful SIGTERM drain
|
|
100
|
+
|
|
101
|
+
**Operations & security**
|
|
102
|
+
- **Structured JSON logs** — request-id propagation, redacted auth headers, tracebacks on 5xx
|
|
103
|
+
- **`/livez` / `/readyz` / `/health`** — liveness, dependency readiness (DB, PostgREST, broker, worker), full detail
|
|
104
|
+
- **Security headers** — HSTS, CSP, X-Content-Type-Options, X-Frame-Options, Referrer-Policy
|
|
105
|
+
- **`supython doctor`** — diagnoses roles, extensions, grants, JWKS, migration drift, symmetric secrets
|
|
106
|
+
- **Secret rotation** — JWT keys, symmetric secrets, Postgres passwords; all with zero-downtime runbooks
|
|
107
|
+
- **Multi-arch Docker image** — `linux/amd64` + `linux/arm64`, non-root user, `tini` PID 1, ~64 MB
|
|
108
|
+
|
|
109
|
+
**Admin control plane** (shipped in v0.1.0)
|
|
110
|
+
- **Vue 3 + Vite SPA** at `/admin` — no runtime Node deps; pre-built static bundle in the wheel
|
|
111
|
+
- **Database surface** — schema browser, table data with role switcher, SQL workspace (read-only default + write toggle), RLS policy editor with dry-run, migrations panel
|
|
112
|
+
- **Auth surface** — user search, ban/unban/force-logout, refresh-token inspector, audit log, email template editing
|
|
113
|
+
- **Module screens** — storage buckets/objects, function routes/invoke, realtime tables/channels, job queue/crons
|
|
114
|
+
- **Ops** — backup management, live log tail via SSE with level/request-id filters
|
|
115
|
+
- **`supython admin create-user`** — bootstrap the first admin (no chicken-and-egg)
|
|
116
|
+
|
|
117
|
+
## Architecture
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
┌──────────────────────┐
|
|
121
|
+
client ─────► │ supython (FastAPI) │ /auth/v1/*, /storage/v1/*,
|
|
122
|
+
│ port 8000 │ /functions/v1/*, /jobs/v1/*,
|
|
123
|
+
│ │ /admin, /livez, /readyz ──┐
|
|
124
|
+
└──────────────────────┘ │
|
|
125
|
+
│ │
|
|
126
|
+
┌──────────────────────┐ │
|
|
127
|
+
client ─────► │ PostgREST │ /<table> ─┤
|
|
128
|
+
│ port 54321 │ │
|
|
129
|
+
└──────────────────────┘ │
|
|
130
|
+
│ │
|
|
131
|
+
▼ │
|
|
132
|
+
┌──────────────────────┐ ◄───────────────────────────┘
|
|
133
|
+
│ Postgres │
|
|
134
|
+
│ port 54322 │
|
|
135
|
+
│ roles: anon / │
|
|
136
|
+
│ authenticated / │
|
|
137
|
+
│ service_role │
|
|
138
|
+
└──────────────────────┘
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The unifying contract is the **JWT** + **Postgres role system**. supython
|
|
142
|
+
mints the JWT; PostgREST verifies the signature via shared JWKS and runs
|
|
143
|
+
every request under the role + claims it carries. RLS does the rest.
|
|
144
|
+
|
|
145
|
+
## Quick start
|
|
146
|
+
|
|
147
|
+
Requires Python 3.11+, Docker 24+ with the `compose` plugin.
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
# 1. install the wheel
|
|
151
|
+
python -m venv .venv && source .venv/bin/activate
|
|
152
|
+
pip install supython
|
|
153
|
+
|
|
154
|
+
# 2. scaffold a new project
|
|
155
|
+
supython init myapp
|
|
156
|
+
cd myapp
|
|
157
|
+
cp .env.example .env
|
|
158
|
+
# Review .env — at minimum confirm AUTHENTICATOR_PASSWORD matches
|
|
159
|
+
# the value PostgREST will use (docker-compose.yml injects it via env).
|
|
160
|
+
#
|
|
161
|
+
# The scaffold creates:
|
|
162
|
+
# manage.py — Django-style CLI entrypoint (sets SUPYTHON_SETTINGS_MODULE)
|
|
163
|
+
# myapp/settings.py — declare EXTENSIONS, EXTRA_ROUTERS, EXTRA_MIDDLEWARE
|
|
164
|
+
# myapp/jobs.py — example @job seed (register your background jobs here)
|
|
165
|
+
# myapp/hooks.py — example @on("signup") seed (lifecycle hooks)
|
|
166
|
+
# myapp/asgi.py — optional entrypoint for uvicorn/gunicorn
|
|
167
|
+
|
|
168
|
+
# 3. boot Postgres + PostgREST and run migrations (one command)
|
|
169
|
+
supython up
|
|
170
|
+
|
|
171
|
+
# 4. run the auth/API service (separate terminal)
|
|
172
|
+
supython dev
|
|
173
|
+
|
|
174
|
+
# 5. (optional) generate typed Python classes from your Postgres schema
|
|
175
|
+
supython gen types --lang py --out types.py
|
|
176
|
+
|
|
177
|
+
# 6. (optional) bootstrap the admin dashboard
|
|
178
|
+
supython admin create-user
|
|
179
|
+
# then open http://localhost:8000/admin
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
You should now have:
|
|
183
|
+
|
|
184
|
+
| service | url |
|
|
185
|
+
| ---------- | ---------------------------- |
|
|
186
|
+
| supython | http://localhost:8000 |
|
|
187
|
+
| Admin UI | http://localhost:8000/admin |
|
|
188
|
+
| PostgREST | http://localhost:54321 |
|
|
189
|
+
| Postgres | `postgres://supython:supython@localhost:54322/supython` |
|
|
190
|
+
|
|
191
|
+
## End-to-end smoke test
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
# sign up
|
|
195
|
+
curl -sS -X POST http://localhost:8000/auth/v1/signup \
|
|
196
|
+
-H 'content-type: application/json' \
|
|
197
|
+
-d '{"email":"alice@example.com","password":"password123"}'
|
|
198
|
+
|
|
199
|
+
# get a fresh token
|
|
200
|
+
TOKEN=$(curl -sS -X POST http://localhost:8000/auth/v1/token \
|
|
201
|
+
-H 'content-type: application/json' \
|
|
202
|
+
-d '{"email":"alice@example.com","password":"password123"}' \
|
|
203
|
+
| python -c 'import sys,json;print(json.load(sys.stdin)["access_token"])')
|
|
204
|
+
|
|
205
|
+
# create a todo via PostgREST — note: NO Python code involved
|
|
206
|
+
curl -sS -X POST http://localhost:54321/todos \
|
|
207
|
+
-H "authorization: Bearer $TOKEN" \
|
|
208
|
+
-H 'content-type: application/json' \
|
|
209
|
+
-H 'prefer: return=representation' \
|
|
210
|
+
-d '{"title":"buy milk"}'
|
|
211
|
+
|
|
212
|
+
# list todos — RLS hides everyone else's rows
|
|
213
|
+
curl -sS http://localhost:54321/todos -H "authorization: Bearer $TOKEN"
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
See [`examples/todos.http`](examples/todos.http) for the full set of calls
|
|
217
|
+
including filtering, sorting, refresh, and isolation between users.
|
|
218
|
+
|
|
219
|
+
## Realtime quickstart
|
|
220
|
+
|
|
221
|
+
supython ships a WebSocket engine that speaks the **Phoenix Channels 5-tuple
|
|
222
|
+
protocol** used by every official Supabase SDK. Unmodified `supabase-js` and
|
|
223
|
+
`supabase-py` clients connect without any shim.
|
|
224
|
+
|
|
225
|
+
### 1. Opt a table into realtime
|
|
226
|
+
|
|
227
|
+
```sql
|
|
228
|
+
-- run once (or add to a migration)
|
|
229
|
+
SELECT realtime.enable('public.messages');
|
|
230
|
+
-- with a custom owner column for DELETE visibility:
|
|
231
|
+
-- SELECT realtime.enable('public.messages', 'author_id');
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Or via CLI (requires a running server):
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
supython realtime enable public.messages
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### 2. Open the demo
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
# start the stack
|
|
244
|
+
supython up
|
|
245
|
+
supython dev # in a second terminal
|
|
246
|
+
|
|
247
|
+
# open the chat demo in two browser tabs
|
|
248
|
+
python -m http.server --directory examples 8080
|
|
249
|
+
# → http://localhost:8080/chat.html
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Enter your JWT (or leave blank for `anon` role), pick a room name, and open
|
|
253
|
+
the same page in a second tab. Messages broadcast instantly; Postgres row
|
|
254
|
+
changes appear as structured cards.
|
|
255
|
+
|
|
256
|
+
### 3. Subscribe from JavaScript (no SDK required)
|
|
257
|
+
|
|
258
|
+
```js
|
|
259
|
+
// Phoenix Channels 5-tuple: [join_ref, ref, topic, event, payload]
|
|
260
|
+
const ws = new WebSocket(
|
|
261
|
+
"ws://localhost:8000/realtime/v1/websocket?apikey=<JWT>&vsn=1.0.0"
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
let ref = 0;
|
|
265
|
+
ws.onopen = () => {
|
|
266
|
+
ws.send(JSON.stringify(["1", String(++ref), "realtime:room-42", "phx_join", {
|
|
267
|
+
config: {
|
|
268
|
+
postgres_changes: [{ event: "*", schema: "public", table: "messages" }],
|
|
269
|
+
broadcast: { self: false }
|
|
270
|
+
},
|
|
271
|
+
access_token: "<JWT>"
|
|
272
|
+
}]));
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
ws.onmessage = ({ data }) => {
|
|
276
|
+
const [, , topic, event, payload] = JSON.parse(data);
|
|
277
|
+
if (event === "postgres_changes") console.log(payload.data);
|
|
278
|
+
if (event === "broadcast") console.log(payload.payload);
|
|
279
|
+
};
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### 4. Subscribe from Python
|
|
283
|
+
|
|
284
|
+
```python
|
|
285
|
+
import asyncio, json
|
|
286
|
+
import websockets
|
|
287
|
+
|
|
288
|
+
TOKEN = "eyJ..." # or omit for anon
|
|
289
|
+
|
|
290
|
+
async def main():
|
|
291
|
+
url = f"ws://localhost:8000/realtime/v1/websocket?apikey={TOKEN}&vsn=1.0.0"
|
|
292
|
+
async with websockets.connect(url) as ws:
|
|
293
|
+
await ws.send(json.dumps([
|
|
294
|
+
"1", "1", "realtime:room-42", "phx_join",
|
|
295
|
+
{"config": {"broadcast": {"self": True}}, "access_token": TOKEN}
|
|
296
|
+
]))
|
|
297
|
+
async for raw in ws:
|
|
298
|
+
join_ref, ref, topic, event, payload = json.loads(raw)
|
|
299
|
+
print(event, payload)
|
|
300
|
+
|
|
301
|
+
asyncio.run(main())
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### 5. REST broadcast (server → clients)
|
|
305
|
+
|
|
306
|
+
```bash
|
|
307
|
+
curl -sS -X POST http://localhost:8000/realtime/v1/broadcast/room-42 \
|
|
308
|
+
-H "authorization: Bearer $SERVICE_ROLE_TOKEN" \
|
|
309
|
+
-H "content-type: application/json" \
|
|
310
|
+
-d '{"event": "announcement", "payload": {"text": "Hello from the server!"}}'
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Realtime settings (`.env`)
|
|
314
|
+
|
|
315
|
+
| Variable | Default | Purpose |
|
|
316
|
+
|---|---|---|
|
|
317
|
+
| `REALTIME_ENABLED` | `true` | Toggle the realtime module |
|
|
318
|
+
| `REALTIME_NOTIFY_CHANNEL` | `realtime:changes` | Postgres `LISTEN` channel |
|
|
319
|
+
| `REALTIME_MAX_CONNECTIONS` | `1000` | Max concurrent WS clients |
|
|
320
|
+
| `REALTIME_MAX_SUBS_PER_CONN` | `100` | Max channel joins per connection |
|
|
321
|
+
| `REALTIME_HEARTBEAT_TIMEOUT_SECONDS` | `30` | Idle-close timeout (client sends every 25s) |
|
|
322
|
+
| `REALTIME_BROKER_QUEUE_SIZE` | `1000` | Per-subscriber outbound queue depth |
|
|
323
|
+
|
|
324
|
+
## Jobs & cron quickstart
|
|
325
|
+
|
|
326
|
+
### 1. Define a job
|
|
327
|
+
|
|
328
|
+
```python
|
|
329
|
+
# in your application code
|
|
330
|
+
from supython.jobs.decorators import job
|
|
331
|
+
|
|
332
|
+
@job("send_welcome_email", version=1, max_attempts=5)
|
|
333
|
+
async def send_welcome_email(ctx, payload):
|
|
334
|
+
await ctx.send_email(
|
|
335
|
+
to=payload["email"],
|
|
336
|
+
subject="Welcome!",
|
|
337
|
+
text="Thanks for signing up.",
|
|
338
|
+
)
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### 2. Enqueue from a function or hook
|
|
342
|
+
|
|
343
|
+
```python
|
|
344
|
+
from supython import hooks
|
|
345
|
+
from supython.jobs.service import enqueue
|
|
346
|
+
|
|
347
|
+
@hooks.on("signup")
|
|
348
|
+
async def on_signup(user, ctx):
|
|
349
|
+
await enqueue(
|
|
350
|
+
ctx.db,
|
|
351
|
+
name="send_welcome_email",
|
|
352
|
+
payload={"email": user.email},
|
|
353
|
+
idempotency_key=f"welcome:{user.id}",
|
|
354
|
+
)
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### 3. Schedule a cron
|
|
358
|
+
|
|
359
|
+
```python
|
|
360
|
+
from supython.jobs.decorators import cron
|
|
361
|
+
|
|
362
|
+
@cron("*/5 * * * *", name="cleanup", job_name="cleanup_job")
|
|
363
|
+
async def cleanup_job(ctx, payload):
|
|
364
|
+
ctx.logger.info("running periodic cleanup")
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### 4. Run the worker
|
|
368
|
+
|
|
369
|
+
```bash
|
|
370
|
+
supython worker run --queue default --concurrency 5
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
Or enable in-process mode for development:
|
|
374
|
+
|
|
375
|
+
```bash
|
|
376
|
+
# in .env
|
|
377
|
+
JOBS_DEV_INPROCESS=true
|
|
378
|
+
supython dev
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### Jobs settings (`.env`)
|
|
382
|
+
|
|
383
|
+
| Variable | Default | Purpose |
|
|
384
|
+
|---|---|---|
|
|
385
|
+
| `JOBS_ENABLED` | `true` | Toggle the jobs module |
|
|
386
|
+
| `JOBS_BACKEND` | `pg` | Queue backend (only `pg` for now) |
|
|
387
|
+
| `JOBS_CRON_BACKEND` | `pg_cron` | `pg_cron`, `inproc`, or `off` |
|
|
388
|
+
| `JOBS_POLL_INTERVAL_S` | `1.0` | Seconds between queue polls |
|
|
389
|
+
| `JOBS_CONCURRENCY` | `5` | Max concurrent jobs per worker |
|
|
390
|
+
| `JOBS_DEFAULT_MAX_ATTEMPTS` | `3` | Default retry limit |
|
|
391
|
+
| `JOBS_BACKOFF_BASE_S` | `5.0` | Base backoff delay (seconds) |
|
|
392
|
+
| `JOBS_BACKOFF_MAX_S` | `300.0` | Max backoff delay (seconds) |
|
|
393
|
+
| `JOBS_VISIBILITY_TIMEOUT_S` | `300.0` | Zombie reclaim timeout (seconds) |
|
|
394
|
+
| `JOBS_DRAIN_TIMEOUT_S` | `30.0` | Graceful shutdown drain (seconds) |
|
|
395
|
+
| `JOBS_DEV_INPROCESS` | `false` | Spawn worker in-process during `supython dev` |
|
|
396
|
+
|
|
397
|
+
## v0.2 auth endpoints
|
|
398
|
+
|
|
399
|
+
| Endpoint | Method | Purpose |
|
|
400
|
+
|---|---|---|
|
|
401
|
+
| `/auth/v1/signup` | POST | Create account, return token pair |
|
|
402
|
+
| `/auth/v1/token` | POST | Password login |
|
|
403
|
+
| `/auth/v1/refresh` | POST | Rotate refresh token (reuse detection built in) |
|
|
404
|
+
| `/auth/v1/logout` | POST | Revoke refresh token |
|
|
405
|
+
| `/auth/v1/user` | GET | Return the caller's user (JWT required) |
|
|
406
|
+
| `/auth/v1/recover` | POST | Request password-reset email |
|
|
407
|
+
| `/auth/v1/recover/verify` | POST | Verify reset token, set new password |
|
|
408
|
+
| `/auth/v1/magiclink` | POST | Request magic-link email |
|
|
409
|
+
| `/auth/v1/magiclink/verify` | GET | Verify magic-link token (`?token=…`) |
|
|
410
|
+
| `/auth/v1/otp` | POST | Request email OTP |
|
|
411
|
+
| `/auth/v1/otp/verify` | POST | Verify OTP code |
|
|
412
|
+
| `/auth/v1/authorize/{provider}` | GET | Start OAuth flow (redirect to provider) |
|
|
413
|
+
| `/auth/v1/callback/{provider}` | GET | Handle OAuth callback, redirect with tokens |
|
|
414
|
+
|
|
415
|
+
**Email backend** — set `EMAIL_BACKEND=console` (default, logs to stdout) or
|
|
416
|
+
`EMAIL_BACKEND=smtp` and configure `SMTP_HOST / SMTP_PORT / SMTP_USERNAME /
|
|
417
|
+
SMTP_PASSWORD`.
|
|
418
|
+
|
|
419
|
+
**OAuth** — add `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` (and/or GitHub
|
|
420
|
+
equivalents) to `.env`. Providers without credentials are silently disabled.
|
|
421
|
+
|
|
422
|
+
### Auth hardening settings (`.env`)
|
|
423
|
+
|
|
424
|
+
| Variable | Default | Purpose |
|
|
425
|
+
|---|---|---|
|
|
426
|
+
| `DB_STATEMENT_TIMEOUT_MS` | `30000` | Per-connection query timeout for the asyncpg pool (`0` disables) |
|
|
427
|
+
| `DB_POOL_MIN_SIZE` | `1` | Minimum asyncpg pool size |
|
|
428
|
+
| `DB_POOL_MAX_SIZE` | `10` | Maximum asyncpg pool size |
|
|
429
|
+
| `AUTH_RATE_LIMIT_ENABLED` | `true` | Toggle auth endpoint rate limiting |
|
|
430
|
+
| `AUTH_RATE_LIMIT_WINDOW_SECONDS` | `60` | Fixed-window size for auth endpoint counters |
|
|
431
|
+
| `AUTH_RATE_LIMIT_TOKEN_PER_WINDOW` | `10` | `/auth/v1/token` attempts per IP/window |
|
|
432
|
+
| `AUTH_RATE_LIMIT_SIGNUP_PER_WINDOW` | `5` | `/auth/v1/signup` attempts per IP/window |
|
|
433
|
+
| `AUTH_RATE_LIMIT_RECOVER_PER_WINDOW` | `3` | `/auth/v1/recover` attempts per IP/window |
|
|
434
|
+
| `AUTH_RATE_LIMIT_OTP_PER_WINDOW` | `5` | `/auth/v1/otp` attempts per IP/window |
|
|
435
|
+
| `AUTH_RATE_LIMIT_MAGICLINK_PER_WINDOW` | `5` | `/auth/v1/magiclink` attempts per IP/window |
|
|
436
|
+
|
|
437
|
+
**`AUTHENTICATOR_PASSWORD`** — the password used for the `authenticator` Postgres
|
|
438
|
+
role that PostgREST connects as. Defaults to `authenticator` (matches the
|
|
439
|
+
migration), but you should change it in production. `supython up` automatically
|
|
440
|
+
runs `ALTER ROLE authenticator WITH PASSWORD …` after migrations so you never
|
|
441
|
+
need to edit SQL.
|
|
442
|
+
|
|
443
|
+
## What's in the box
|
|
444
|
+
|
|
445
|
+
```
|
|
446
|
+
supython/
|
|
447
|
+
├── manage.py # Django-style entrypoint (sets SUPYTHON_SETTINGS_MODULE)
|
|
448
|
+
├── docker-compose.yml # Postgres + PostgREST (dev stack)
|
|
449
|
+
├── docker-compose.prod.yml # hardened single-host production stack
|
|
450
|
+
├── docker-compose.test.yml # dedicated test Postgres on port 54323
|
|
451
|
+
├── Caddyfile # reverse-proxy TLS for prod
|
|
452
|
+
├── <name>/ # your Python package
|
|
453
|
+
│ ├── __init__.py
|
|
454
|
+
│ ├── settings.py # project settings (EXTENSIONS, EXTRA_ROUTERS, …)
|
|
455
|
+
│ ├── asgi.py # optional ASGI entrypoint (uvicorn <name>.asgi:app)
|
|
456
|
+
│ ├── jobs.py # example @job seed
|
|
457
|
+
│ └── hooks.py # example @on("signup") seed
|
|
458
|
+
├── migrations/
|
|
459
|
+
│ ├── 0001_extensions_and_roles.sql # anon/authenticated/service_role/authenticator
|
|
460
|
+
│ ├── 0002_auth_schema.sql # auth.users, auth.refresh_tokens, auth.uid()
|
|
461
|
+
│ ├── 0003_demo_todos.sql # the demo table + RLS policies
|
|
462
|
+
│ ├── 0004_auth_v0_2.sql # identities, one_time_tokens, audit_log
|
|
463
|
+
│ ├── 0005_storage_schema.sql # storage.buckets + storage.objects
|
|
464
|
+
│ ├── 0006_realtime_schema.sql # realtime schema, trigger, enable() helper
|
|
465
|
+
│ ├── 0007_jobs_schema.sql # jobs queue, cron_schedules, enqueue/claim_next
|
|
466
|
+
│ ├── 0008_jobs_last_error.sql # last_error column on jobs.jobs
|
|
467
|
+
│ ├── 0009_auth_rate_limits.sql # auth fixed-window rate-limit counters
|
|
468
|
+
│ ├── 0010_worker_heartbeat.sql # worker heartbeat for /readyz
|
|
469
|
+
│ ├── 0011_admin_schema.sql # admin.admin_users, sessions, audit
|
|
470
|
+
│ ├── 0012_auth_banned_until.sql # banned_until on auth.users
|
|
471
|
+
│ ├── 0013_email_templates.sql # auth.email_templates
|
|
472
|
+
│ ├── 0014_realtime_payload_warning.sql # >8KB payload warning counter
|
|
473
|
+
│ └── 0015_backups_schema.sql # backups metadata
|
|
474
|
+
├── examples/
|
|
475
|
+
│ ├── todos.http # HTTP smoke tests (auth + PostgREST)
|
|
476
|
+
│ ├── storage.http # storage upload / signed URL examples
|
|
477
|
+
│ ├── functions.http # edge-function call examples
|
|
478
|
+
│ └── chat.html # two-browser realtime demo (vanilla JS, zero deps)
|
|
479
|
+
├── docs/
|
|
480
|
+
│ ├── PROJECT.md # architecture + roadmap (single source of truth)
|
|
481
|
+
│ ├── Installation.md # full install guide (dev, prod, managed Postgres)
|
|
482
|
+
│ └── admin-ui/
|
|
483
|
+
│ ├── admin-surface-plan.md # admin implementation plan + phase status
|
|
484
|
+
│ └── admin-surface.md # admin architecture + contracts
|
|
485
|
+
├── tests/
|
|
486
|
+
│ ├── conftest.py # cross-tree fixtures (keys, capturing mailer)
|
|
487
|
+
│ ├── _keys.py # JWT-forging helpers
|
|
488
|
+
│ ├── fixtures/ # test function modules, etc.
|
|
489
|
+
│ ├── unit/ # pure-Python tests (~6s, no Docker)
|
|
490
|
+
│ │ ├── test_admin_session.py
|
|
491
|
+
│ │ ├── test_admin_service_db.py
|
|
492
|
+
│ │ └── ...
|
|
493
|
+
│ └── integration/ # full ASGI + Postgres on port 54323
|
|
494
|
+
│ ├── conftest.py # pool, app, client, autouse DB cleaners
|
|
495
|
+
│ ├── test_auth_signup_login.py
|
|
496
|
+
│ ├── test_admin_auth.py
|
|
497
|
+
│ ├── test_admin_auth_users.py
|
|
498
|
+
│ ├── test_admin_db_rows.py
|
|
499
|
+
│ ├── test_admin_jobs.py
|
|
500
|
+
│ ├── test_admin_ops_backups.py
|
|
501
|
+
│ ├── test_admin_storage.py
|
|
502
|
+
│ ├── test_postgrest_rls.py
|
|
503
|
+
│ ├── test_realtime_ws.py
|
|
504
|
+
│ └── ...
|
|
505
|
+
├── admin-ui/ # Vue 3 + Vite SPA (built → src/supython/admin/static/)
|
|
506
|
+
│ └── src/
|
|
507
|
+
│ ├── api/ # single fetch seam
|
|
508
|
+
│ ├── components/ # shell, data, editors, feedback
|
|
509
|
+
│ ├── composables/ # useResource, useTable, useConfirm, useImpersonate, …
|
|
510
|
+
│ ├── stores/ # auth, ui
|
|
511
|
+
│ ├── views/ # Dashboard, db/, auth/, storage/, functions/, …
|
|
512
|
+
│ └── router/
|
|
513
|
+
└── src/supython/
|
|
514
|
+
├── __init__.py # single version string
|
|
515
|
+
├── settings.py # pydantic-settings, .env-driven
|
|
516
|
+
├── db.py # asyncpg pool + lifespan + as_role() / as_service_role()
|
|
517
|
+
├── mailer.py # ConsoleBackend / SmtpBackend
|
|
518
|
+
├── tokens.py # RS256/ES256 JWT + JWKS
|
|
519
|
+
├── passwords.py # argon2id
|
|
520
|
+
├── migrate.py # ~50-line SQL migration runner
|
|
521
|
+
├── app.py # FastAPI factory
|
|
522
|
+
├── cli.py # typer: up, dev, keygen, admin, worker, test, …
|
|
523
|
+
├── extensions.py # eager-import dotted module paths at boot
|
|
524
|
+
├── settings_module.py # Django-style user settings (EXTENSIONS, EXTRA_ROUTERS, …)
|
|
525
|
+
├── health.py # /livez, /readyz, /health endpoints
|
|
526
|
+
├── logging_config.py # structured JSON log setup
|
|
527
|
+
├── security_headers.py # HSTS, CSP, etc.
|
|
528
|
+
├── body_size.py # request body size guards
|
|
529
|
+
├── jwks.py # JWKS generation + rotation helpers
|
|
530
|
+
├── keyset.py # asymmetric key rotation manifest
|
|
531
|
+
├── secretset.py # symmetric secret rotation manifest
|
|
532
|
+
├── hooks.py # generic hook system: on() / fire()
|
|
533
|
+
├── mail.py # email send with job-retry fallback
|
|
534
|
+
├── auth/
|
|
535
|
+
│ ├── schemas.py
|
|
536
|
+
│ ├── service.py # full auth layer: signup / OAuth / OTP / recover …
|
|
537
|
+
│ ├── router.py # all /auth/v1/* routes
|
|
538
|
+
│ └── providers/ # Google, GitHub, OAuth2 helpers
|
|
539
|
+
├── storage/
|
|
540
|
+
│ ├── backends.py # LocalBackend, S3Backend
|
|
541
|
+
│ ├── service.py
|
|
542
|
+
│ └── router.py # /storage/v1/*
|
|
543
|
+
├── functions/
|
|
544
|
+
│ ├── loader.py # filesystem discovery + hot reload
|
|
545
|
+
│ └── router.py # /functions/v1/*
|
|
546
|
+
├── realtime/
|
|
547
|
+
│ ├── protocol.py # Phoenix Channels encode/decode
|
|
548
|
+
│ ├── broker.py # fan-out engine with RLS filtering
|
|
549
|
+
│ ├── websocket.py # WS route with JWT auth
|
|
550
|
+
│ └── router.py # /realtime/v1/*
|
|
551
|
+
├── jobs/
|
|
552
|
+
│ ├── registry.py # @job / @cron decorator store
|
|
553
|
+
│ ├── service.py # enqueue, claim_next, mark_*
|
|
554
|
+
│ ├── worker.py # long-running poll/dispatch/drain loop
|
|
555
|
+
│ ├── cron.py # pg_cron sync + InProcScheduler
|
|
556
|
+
│ └── router.py # /jobs/v1/*
|
|
557
|
+
├── admin/
|
|
558
|
+
│ ├── session.py # admin cookie session (SHA-256 hashed, 8h TTL)
|
|
559
|
+
│ ├── deps.py # require_admin dependency
|
|
560
|
+
│ ├── spa.py # static SPA mount + index.html fallback
|
|
561
|
+
│ ├── schemas.py # shared Pydantic models
|
|
562
|
+
│ ├── audit.py # admin audit log writer
|
|
563
|
+
│ ├── static/ # pre-built Vue 3 SPA bundle (committed)
|
|
564
|
+
│ └── api/ # /admin/api/v1/* route handlers
|
|
565
|
+
├── backups/ # pg_dump wrapper + restore
|
|
566
|
+
├── gen/ # supython gen types --lang py|ts
|
|
567
|
+
├── scaffold/ # supython init templates
|
|
568
|
+
└── client/ # Python SDK (optional [client] extra)
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
## Plugins & extensions
|
|
572
|
+
|
|
573
|
+
supython uses a Django-style settings module to declare your app's extensions:
|
|
574
|
+
|
|
575
|
+
```python
|
|
576
|
+
# <name>/settings.py — scaffolded by `supython init`
|
|
577
|
+
|
|
578
|
+
EXTENSIONS = [
|
|
579
|
+
"myapp.jobs", # your @job / @cron decorators
|
|
580
|
+
"myapp.hooks", # your @on("signup") / @on("login") hooks
|
|
581
|
+
]
|
|
582
|
+
|
|
583
|
+
EXTRA_ROUTERS: list[str] = [] # "module.path:router_symbol"
|
|
584
|
+
EXTRA_MIDDLEWARE: list[str] = [] # "module.path:ClassName"
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
The scaffolded `manage.py` sets `SUPYTHON_SETTINGS_MODULE` so the CLI and
|
|
588
|
+
worker automatically discover your extensions. You can also set
|
|
589
|
+
`EXTENSIONS=myapp.jobs,myapp.hooks` directly in `.env` if you prefer not
|
|
590
|
+
to use a settings module.
|
|
591
|
+
|
|
592
|
+
Extensions are plain Python modules imported eagerly at boot — their
|
|
593
|
+
`@job`, `@cron`, and `@on` decorators register before the FastAPI app or
|
|
594
|
+
worker starts. This is the same mechanism supython's own internals use
|
|
595
|
+
to stay composable without cross-module imports.
|
|
596
|
+
|
|
597
|
+
## CLI
|
|
598
|
+
|
|
599
|
+
```
|
|
600
|
+
supython up # docker compose up + migrate + start postgrest
|
|
601
|
+
supython up --prod # boot the production stack
|
|
602
|
+
supython up --prod --worker # boot prod stack + worker
|
|
603
|
+
supython dev # uvicorn the FastAPI service with reload
|
|
604
|
+
supython down # stop the stack (keeps data)
|
|
605
|
+
supython down --prod # stop the prod stack
|
|
606
|
+
supython reset # stop the stack and DELETE the volume (destructive)
|
|
607
|
+
supython reset --prod # stop prod stack and DELETE volumes
|
|
608
|
+
supython migrate # apply pending SQL migrations
|
|
609
|
+
supython info # print resolved settings
|
|
610
|
+
supython doctor # diagnose roles, extensions, JWKS, grants, migration drift
|
|
611
|
+
supython init <name> # scaffold a new supython project
|
|
612
|
+
supython gen types --lang py --out types.py # emit typed dataclasses + TypedDicts
|
|
613
|
+
supython gen types --lang ts --out types.ts # emit TypeScript Database interface
|
|
614
|
+
|
|
615
|
+
# Auth & key management
|
|
616
|
+
supython keygen init [--alg RS256|ES256] # generate signing keypair + JWKS
|
|
617
|
+
supython keygen rotate [--no-reload] # add new verifying kid (zero-downtime)
|
|
618
|
+
supython keygen activate <kid> [--no-reload] # promote kid to active signer
|
|
619
|
+
supython keygen prune [--force] [--no-reload] # drop retired kids past grace window
|
|
620
|
+
supython secret status # show symmetric secret manifest
|
|
621
|
+
supython secret rotate <storage|oauth> # add new verifying symmetric secret
|
|
622
|
+
supython secret activate <storage|oauth> <kid>
|
|
623
|
+
supython secret prune <storage|oauth> [--force]
|
|
624
|
+
supython password rotate <role> # rotate a Postgres role password
|
|
625
|
+
|
|
626
|
+
# Admin
|
|
627
|
+
supython admin create-user # bootstrap the first admin (interactive)
|
|
628
|
+
|
|
629
|
+
# Realtime
|
|
630
|
+
supython realtime enable public.messages # opt a table into realtime
|
|
631
|
+
supython realtime enable public.posts --owner-column author_id
|
|
632
|
+
|
|
633
|
+
# Jobs & worker
|
|
634
|
+
supython worker run --queue default # start the job worker (blocks)
|
|
635
|
+
supython jobs list # list queued/running/finished jobs
|
|
636
|
+
supython jobs show <uuid> # show job details
|
|
637
|
+
supython jobs cancel <uuid> # cancel a job
|
|
638
|
+
supython jobs retry <uuid> # re-queue a failed job
|
|
639
|
+
supython jobs enqueue send_welcome_email --payload '{"email":"a@b.com"}'
|
|
640
|
+
supython cron list # list registered crons
|
|
641
|
+
supython cron sync # sync crons with pg_cron
|
|
642
|
+
|
|
643
|
+
# Test suite
|
|
644
|
+
supython test up # start test Postgres on port 54323
|
|
645
|
+
supython test run [PYTEST_ARGS...] # bootstrap + run full suite
|
|
646
|
+
supython test run tests/unit # fast loop, no Docker
|
|
647
|
+
supython test run tests/integration -k auth_signup
|
|
648
|
+
supython test down # stop test DB (keeps volume)
|
|
649
|
+
supython test reset # stop test DB + delete volume
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
## How the auth ↔ PostgREST contract works
|
|
653
|
+
|
|
654
|
+
1. supython signs an RS256 JWT with its private key and the claims:
|
|
655
|
+
```json
|
|
656
|
+
{
|
|
657
|
+
"sub": "<user uuid>",
|
|
658
|
+
"email": "<user email>",
|
|
659
|
+
"role": "authenticated",
|
|
660
|
+
"aud": "authenticated",
|
|
661
|
+
"iat": ...,
|
|
662
|
+
"exp": ...
|
|
663
|
+
}
|
|
664
|
+
```
|
|
665
|
+
2. The client sends the token to **either** supython (for `/auth/v1/user`)
|
|
666
|
+
**or** PostgREST (for everything else) as `Authorization: Bearer <jwt>`.
|
|
667
|
+
3. PostgREST verifies the signature via the public JWKS, switches
|
|
668
|
+
the DB role to `authenticated`, and sets `request.jwt.claims` so
|
|
669
|
+
`auth.uid()` returns the user's id inside RLS policies.
|
|
670
|
+
4. RLS policies on `public.todos` use `auth.uid()` to scope every query.
|
|
671
|
+
|
|
672
|
+
This is exactly the model Supabase uses, in ~400 lines of Python.
|
|
673
|
+
|
|
674
|
+
## Docker image
|
|
675
|
+
|
|
676
|
+
The supython service ships as a multi-arch image (`linux/amd64`,
|
|
677
|
+
`linux/arm64`) on `python:3.11-slim`. The admin UI bundle is committed
|
|
678
|
+
into the wheel, so the image build is Node-free.
|
|
679
|
+
|
|
680
|
+
```bash
|
|
681
|
+
# build locally (host arch)
|
|
682
|
+
docker build -t supython:dev .
|
|
683
|
+
|
|
684
|
+
# build the multi-arch manifest
|
|
685
|
+
docker buildx build --platform linux/amd64,linux/arm64 -t supython:dev .
|
|
686
|
+
|
|
687
|
+
# run against an existing Postgres
|
|
688
|
+
docker run --rm -p 8000:8000 \
|
|
689
|
+
-e DATABASE_URL=postgres://supython:supython@host.docker.internal:54322/supython \
|
|
690
|
+
-e JWT_PRIVATE_KEY_PATH=/run/secrets/jwt.pem \
|
|
691
|
+
-v ./.supython:/run/secrets:ro \
|
|
692
|
+
supython:dev
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
The container runs as a non-root `supython` user (uid 1000), uses `tini`
|
|
696
|
+
as PID 1, exposes port 8000 (override with `SUPYTHON_PORT`), and ships a
|
|
697
|
+
`/livez` HEALTHCHECK. CI builds both arches on every PR via
|
|
698
|
+
`.github/workflows/docker.yml` and publishes the multi-arch manifest to
|
|
699
|
+
GHCR (`ghcr.io/<owner>/supython:<tag>`) on `v*` tags.
|
|
700
|
+
|
|
701
|
+
## Running the test suite
|
|
702
|
+
|
|
703
|
+
The test suite is split in two so unit feedback is fast and integration
|
|
704
|
+
runs are deterministic:
|
|
705
|
+
|
|
706
|
+
```
|
|
707
|
+
tests/unit/ # pure Python, no Docker (~6s) — run first, always
|
|
708
|
+
tests/integration/ # full ASGI + Postgres on port 54323
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
**One-time setup:**
|
|
712
|
+
|
|
713
|
+
```bash
|
|
714
|
+
supython test up # start dedicated test Postgres on port 54323, apply migrations
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
**Day-to-day:**
|
|
718
|
+
|
|
719
|
+
```bash
|
|
720
|
+
supython test run # bootstrap + run full suite
|
|
721
|
+
supython test run tests/unit # fast loop, no Docker required
|
|
722
|
+
supython test run tests/integration -k auth_signup
|
|
723
|
+
pytest tests/unit # unit-only without going through CLI
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
`supython test run` forwards every extra arg to `pytest`, sets
|
|
727
|
+
`DATABASE_URL` to the test container, and exits with pytest's status code.
|
|
728
|
+
Integration tests skip cleanly (not fail) when Postgres is unreachable, so
|
|
729
|
+
unit tests always run in isolation.
|
|
730
|
+
|
|
731
|
+
**CI:** runners with Docker run `supython test up && supython test run`;
|
|
732
|
+
runners without Docker run `pytest tests/unit` for a meaningful subset.
|
|
733
|
+
|
|
734
|
+
## Roadmap [shipped v0.1.0]
|
|
735
|
+
|
|
736
|
+
- ✅ Email/password auth, PostgREST contract, RLS demo
|
|
737
|
+
- ✅ OAuth, password reset, magic link, OTP, reuse detection, email backend, test suite
|
|
738
|
+
- ✅ Storage (S3/local) with RLS-on-metadata, edge-style functions from a `functions/` directory; `db.as_role(role, claims)` helper; `supython init` scaffold; `supython gen types --lang py`
|
|
739
|
+
- ✅ Realtime over `LISTEN/NOTIFY` with RLS-aware fan-out; Phoenix Channels wire format; broadcast + presence; `examples/chat.html` demo
|
|
740
|
+
- ✅ Job queue worker + `pg_cron` scheduling + hooks + CLI management commands
|
|
741
|
+
- ✅ Grooming + security foundation: unified versioning, CORS closed by default, RS256 JWT, rate limiting, `supython doctor`, pool sizing, statement timeout
|
|
742
|
+
- ✅ Production observable: structured JSON logs, `/livez`/`/readyz`/`/health`, security headers, input size guards, audit log completeness, OAuth PKCE, secret rotation runbooks
|
|
743
|
+
- ✅ (partial) Multi-arch Docker images, admin control plane (Vue 3 SPA — database, auth, storage, functions, realtime, jobs, backups, log tail), CI buildx workflow; benchmarks + security audit pass + dependency budget CI remaining
|
|
744
|
+
- *(deferred)* — Realtime v2 over logical replication
|
|
745
|
+
- v0.1.0 Release — final sweep, tag, publish wheel, production deployment with no patches
|
|
746
|
+
- ✅ **TypeScript SDK** — `@supython/sdk` wrapping `@supabase/postgrest-js` + `@supabase/realtime-js`
|
|
747
|
+
|
|
748
|
+
### Post v0.1.0
|
|
749
|
+
|
|
750
|
+
- **v1.1+** — Admin control plane polish (backend + frontend shipped in v0.1.0; tests + remaining DoD items deferred)
|
|
751
|
+
- **Realtime v2** — logical replication (demand-driven; swap when trigger overhead or >8KB payload data warrants it)
|
|
752
|
+
- **Prometheus `/metrics`** + **OpenTelemetry** — optional extras
|
|
753
|
+
|
|
754
|
+
## License
|
|
755
|
+
|
|
756
|
+
MIT
|