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,144 @@
|
|
|
1
|
+
"""Scaffold a new supython project directory."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import secrets
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from supython import jwks as _jwks
|
|
9
|
+
|
|
10
|
+
_TEMPLATES_DIR = Path(__file__).parent / "templates"
|
|
11
|
+
_JWT_KEYS_DIRNAME = ".supython"
|
|
12
|
+
_PRIVATE_KEY_FILENAME = "jwt_private.pem"
|
|
13
|
+
_JWKS_FILENAME = "jwks.json"
|
|
14
|
+
|
|
15
|
+
_TEMPLATE_MAP: list[tuple[str, str]] = [
|
|
16
|
+
("docker-compose.yml.tmpl", "docker-compose.yml"),
|
|
17
|
+
("docker-compose.prod.yml.tmpl", "docker-compose.prod.yml"),
|
|
18
|
+
("env.example.tmpl", ".env.example"),
|
|
19
|
+
("gitignore.tmpl", ".gitignore"),
|
|
20
|
+
("README.md.tmpl", "README.md"),
|
|
21
|
+
("functions_README.md.tmpl", "functions/README.md"),
|
|
22
|
+
("Caddyfile.tmpl", "Caddyfile"),
|
|
23
|
+
("docker_postgres_Dockerfile.tmpl", "docker/postgres/Dockerfile"),
|
|
24
|
+
("docker_postgres_postgresql.conf.tmpl", "docker/postgres/postgresql.conf"),
|
|
25
|
+
("settings.py.tmpl", "{name}/settings.py"),
|
|
26
|
+
("asgi.py.tmpl", "{name}/asgi.py"),
|
|
27
|
+
("manage.py.tmpl", "manage.py"),
|
|
28
|
+
("package_init.py.tmpl", "{name}/__init__.py"),
|
|
29
|
+
("apps_jobs.py.tmpl", "{name}/jobs.py"),
|
|
30
|
+
("apps_hooks.py.tmpl", "{name}/hooks.py"),
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def scaffold(name: str, target: Path, *, force: bool = False) -> list[Path]:
|
|
35
|
+
"""Write a minimal supython project into *target*.
|
|
36
|
+
|
|
37
|
+
Returns the list of paths written, in order.
|
|
38
|
+
Raises ``FileExistsError`` if *target* is non-empty and *force`` is False.
|
|
39
|
+
"""
|
|
40
|
+
if target.exists() and any(target.iterdir()) and not force:
|
|
41
|
+
raise FileExistsError(f"{target} is not empty. Use --force to overwrite.")
|
|
42
|
+
|
|
43
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
(target / "migrations").mkdir(exist_ok=True)
|
|
45
|
+
(target / "functions").mkdir(exist_ok=True)
|
|
46
|
+
|
|
47
|
+
vars: dict[str, str] = {
|
|
48
|
+
"name": name,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
written: list[Path] = []
|
|
52
|
+
|
|
53
|
+
for tmpl_name, dest_rel in _TEMPLATE_MAP:
|
|
54
|
+
tmpl_path = _TEMPLATES_DIR / tmpl_name
|
|
55
|
+
content = tmpl_path.read_text().format(**vars)
|
|
56
|
+
dest = target / dest_rel.format(**vars)
|
|
57
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
if dest.exists() and not force:
|
|
59
|
+
continue
|
|
60
|
+
dest.write_text(content)
|
|
61
|
+
written.append(dest)
|
|
62
|
+
|
|
63
|
+
# manage.py should be executable.
|
|
64
|
+
manage_py = target / "manage.py"
|
|
65
|
+
if manage_py.exists() and os.name == "posix":
|
|
66
|
+
manage_py.chmod(manage_py.stat().st_mode | 0o111)
|
|
67
|
+
|
|
68
|
+
gitkeep = target / "migrations" / ".gitkeep"
|
|
69
|
+
if not gitkeep.exists() or force:
|
|
70
|
+
gitkeep.write_text("")
|
|
71
|
+
written.append(gitkeep)
|
|
72
|
+
|
|
73
|
+
written.extend(_write_scaffold_keypair(target, force=force))
|
|
74
|
+
written.extend(_write_scaffold_secrets(target, force=force))
|
|
75
|
+
return written
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _write_scaffold_keypair(target: Path, *, force: bool) -> list[Path]:
|
|
79
|
+
key_dir = target / _JWT_KEYS_DIRNAME
|
|
80
|
+
key_dir.mkdir(parents=True, exist_ok=True)
|
|
81
|
+
pem_path = key_dir / _PRIVATE_KEY_FILENAME
|
|
82
|
+
jwks_path = key_dir / _JWKS_FILENAME
|
|
83
|
+
|
|
84
|
+
if not force and pem_path.exists() and jwks_path.exists():
|
|
85
|
+
return []
|
|
86
|
+
|
|
87
|
+
key = _jwks.generate_private_key("RS256")
|
|
88
|
+
signer = _jwks.signing_key_from_private_key(key, "RS256")
|
|
89
|
+
_jwks.write_private_key_pem(pem_path, _jwks.private_key_to_pem(key), force=force)
|
|
90
|
+
_jwks.write_jwks_file(jwks_path, _jwks.jwks_for_signing_key(signer))
|
|
91
|
+
return [pem_path, jwks_path]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _write_scaffold_secrets(target: Path, *, force: bool) -> list[Path]:
|
|
95
|
+
secrets_dir = target / ".supython" / "secrets"
|
|
96
|
+
secrets_dir.mkdir(parents=True, exist_ok=True)
|
|
97
|
+
manifest_path = target / ".supython" / "secrets.json"
|
|
98
|
+
|
|
99
|
+
if not force and manifest_path.exists():
|
|
100
|
+
return []
|
|
101
|
+
|
|
102
|
+
oauth_secret = secrets.token_urlsafe(48)
|
|
103
|
+
storage_secret = secrets.token_urlsafe(48)
|
|
104
|
+
|
|
105
|
+
oauth_kid = "v1"
|
|
106
|
+
storage_kid = "v1"
|
|
107
|
+
|
|
108
|
+
(secrets_dir / f"oauth_state.{oauth_kid}.secret").write_text(oauth_secret)
|
|
109
|
+
(secrets_dir / f"storage_signed_url.{storage_kid}.secret").write_text(storage_secret)
|
|
110
|
+
if os.name == "posix":
|
|
111
|
+
(secrets_dir / f"oauth_state.{oauth_kid}.secret").chmod(0o600)
|
|
112
|
+
(secrets_dir / f"storage_signed_url.{storage_kid}.secret").chmod(0o600)
|
|
113
|
+
|
|
114
|
+
now_iso = "2026-01-01T00:00:00Z" # static; tests read back the actual values
|
|
115
|
+
manifest = {
|
|
116
|
+
"oauth_state": {
|
|
117
|
+
"active": oauth_kid,
|
|
118
|
+
"keys": [
|
|
119
|
+
{
|
|
120
|
+
"kid": oauth_kid,
|
|
121
|
+
"status": "active",
|
|
122
|
+
"created_at": now_iso,
|
|
123
|
+
"retired_at": None,
|
|
124
|
+
}
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
"storage_signed_url": {
|
|
128
|
+
"active": storage_kid,
|
|
129
|
+
"keys": [
|
|
130
|
+
{
|
|
131
|
+
"kid": storage_kid,
|
|
132
|
+
"status": "active",
|
|
133
|
+
"created_at": now_iso,
|
|
134
|
+
"retired_at": None,
|
|
135
|
+
}
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
}
|
|
139
|
+
manifest_path.write_text(json.dumps(manifest, sort_keys=True, indent=2) + "\n")
|
|
140
|
+
return [
|
|
141
|
+
secrets_dir / f"oauth_state.{oauth_kid}.secret",
|
|
142
|
+
secrets_dir / f"storage_signed_url.{storage_kid}.secret",
|
|
143
|
+
manifest_path,
|
|
144
|
+
]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# {name}
|
|
2
|
+
|
|
3
|
+
A [supython](https://github.com/your-org/supython) project.
|
|
4
|
+
|
|
5
|
+
## Quick start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cp .env.example .env
|
|
9
|
+
pip install supython
|
|
10
|
+
supython up # start Postgres + PostgREST and apply migrations
|
|
11
|
+
supython dev # start the auth/API service on http://localhost:8000
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Layout
|
|
15
|
+
|
|
16
|
+
- `migrations/` — SQL migration files (applied in filename order by `supython migrate`)
|
|
17
|
+
- `functions/` — edge functions (one `.py` file per route, no restart required)
|
|
18
|
+
- `.supython/` — generated JWT keypair and JWKS (private key is gitignored)
|
|
19
|
+
|
|
20
|
+
## Docs
|
|
21
|
+
|
|
22
|
+
See the [supython documentation](https://github.com/your-org/supython) for full reference.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""ASGI entrypoint.
|
|
2
|
+
|
|
3
|
+
For most workflows you do not need this file — ``./manage.py dev`` and
|
|
4
|
+
``supython dev`` serve ``supython.app:app`` directly. Use this when you
|
|
5
|
+
want to run the app under your own uvicorn / gunicorn invocation:
|
|
6
|
+
|
|
7
|
+
uvicorn {name}.asgi:app
|
|
8
|
+
|
|
9
|
+
``build_app`` is a public alias of ``supython.app.create_app``.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from supython import build_app
|
|
13
|
+
|
|
14
|
+
app = build_app()
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
name: {name}
|
|
2
|
+
|
|
3
|
+
services:
|
|
4
|
+
db:
|
|
5
|
+
build: ./docker/postgres
|
|
6
|
+
container_name: {name}-db
|
|
7
|
+
environment:
|
|
8
|
+
POSTGRES_USER: {name}
|
|
9
|
+
POSTGRES_PASSWORD: ${{POSTGRES_PASSWORD:-{name}}}
|
|
10
|
+
POSTGRES_DB: {name}
|
|
11
|
+
ports:
|
|
12
|
+
- "127.0.0.1:54322:5432"
|
|
13
|
+
volumes:
|
|
14
|
+
- {name}-db-data:/var/lib/postgresql/data
|
|
15
|
+
healthcheck:
|
|
16
|
+
test: ["CMD-SHELL", "pg_isready -U {name} -d {name}"]
|
|
17
|
+
interval: 2s
|
|
18
|
+
timeout: 5s
|
|
19
|
+
retries: 30
|
|
20
|
+
|
|
21
|
+
postgrest:
|
|
22
|
+
image: postgrest/postgrest:v12.2.3
|
|
23
|
+
container_name: {name}-postgrest
|
|
24
|
+
depends_on:
|
|
25
|
+
db:
|
|
26
|
+
condition: service_healthy
|
|
27
|
+
environment:
|
|
28
|
+
PGRST_DB_URI: postgres://authenticator:${{AUTHENTICATOR_PASSWORD:-authenticator}}@db:5432/{name}
|
|
29
|
+
PGRST_DB_SCHEMAS: public
|
|
30
|
+
PGRST_DB_ANON_ROLE: anon
|
|
31
|
+
PGRST_JWT_SECRET: "@/etc/postgrest/jwks.json"
|
|
32
|
+
PGRST_JWT_AUD: authenticated
|
|
33
|
+
PGRST_OPENAPI_MODE: ignore-privileges
|
|
34
|
+
PGRST_DB_USE_LEGACY_GUCS: "false"
|
|
35
|
+
volumes:
|
|
36
|
+
- ./.supython/jwks.json:/etc/postgrest/jwks.json:ro
|
|
37
|
+
|
|
38
|
+
supython:
|
|
39
|
+
# Pin to a release tag, or replace with `build: .` if you are building from source.
|
|
40
|
+
image: ghcr.io/<owner>/supython:<version>
|
|
41
|
+
container_name: {name}-api
|
|
42
|
+
depends_on:
|
|
43
|
+
postgrest:
|
|
44
|
+
condition: service_started
|
|
45
|
+
env_file:
|
|
46
|
+
- .env
|
|
47
|
+
volumes:
|
|
48
|
+
- ./.supython/jwt_private.pem:/run/secrets/jwt_private.pem:ro
|
|
49
|
+
- ./.supython/jwks.json:/run/secrets/jwks.json:ro
|
|
50
|
+
- {name}-storage:/var/lib/supython/storage
|
|
51
|
+
ports:
|
|
52
|
+
- "${{SUPYTHON_PUBLISH_PORT:-8000}}:8000"
|
|
53
|
+
|
|
54
|
+
worker:
|
|
55
|
+
image: ghcr.io/<owner>/supython:<version>
|
|
56
|
+
profiles: ["worker"]
|
|
57
|
+
container_name: {name}-worker
|
|
58
|
+
depends_on:
|
|
59
|
+
- db
|
|
60
|
+
env_file:
|
|
61
|
+
- .env
|
|
62
|
+
volumes:
|
|
63
|
+
- ./.supython/jwt_private.pem:/run/secrets/jwt_private.pem:ro
|
|
64
|
+
command: ["supython", "worker", "run", "--queue", "default"]
|
|
65
|
+
|
|
66
|
+
caddy:
|
|
67
|
+
image: caddy:2-alpine
|
|
68
|
+
profiles: ["tls"]
|
|
69
|
+
container_name: {name}-caddy
|
|
70
|
+
ports:
|
|
71
|
+
- "80:80"
|
|
72
|
+
- "443:443"
|
|
73
|
+
volumes:
|
|
74
|
+
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
|
75
|
+
- {name}-caddy-data:/data
|
|
76
|
+
- {name}-caddy-config:/config
|
|
77
|
+
depends_on:
|
|
78
|
+
- supython
|
|
79
|
+
|
|
80
|
+
volumes:
|
|
81
|
+
{name}-db-data:
|
|
82
|
+
{name}-storage:
|
|
83
|
+
{name}-caddy-data:
|
|
84
|
+
{name}-caddy-config:
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
name: {name}
|
|
2
|
+
|
|
3
|
+
services:
|
|
4
|
+
db:
|
|
5
|
+
# Build a Postgres image with `pg_cron` baked in (see docker/postgres/Dockerfile).
|
|
6
|
+
# `pg_cron` is required by migration 0001_extensions_and_roles.sql, so the
|
|
7
|
+
# stock `postgres:*-alpine` image is not sufficient. The first `supython up`
|
|
8
|
+
# builds this image once; later runs reuse the cached layer.
|
|
9
|
+
build: ./docker/postgres
|
|
10
|
+
container_name: {name}-db
|
|
11
|
+
environment:
|
|
12
|
+
POSTGRES_USER: {name}
|
|
13
|
+
POSTGRES_PASSWORD: {name}
|
|
14
|
+
POSTGRES_DB: {name}
|
|
15
|
+
ports:
|
|
16
|
+
- "54322:5432"
|
|
17
|
+
volumes:
|
|
18
|
+
- {name}-db-data:/var/lib/postgresql/data
|
|
19
|
+
healthcheck:
|
|
20
|
+
test: ["CMD-SHELL", "pg_isready -U {name} -d {name}"]
|
|
21
|
+
interval: 2s
|
|
22
|
+
timeout: 5s
|
|
23
|
+
retries: 30
|
|
24
|
+
|
|
25
|
+
postgrest:
|
|
26
|
+
image: postgrest/postgrest:v12.2.3
|
|
27
|
+
container_name: {name}-postgrest
|
|
28
|
+
depends_on:
|
|
29
|
+
db:
|
|
30
|
+
condition: service_healthy
|
|
31
|
+
environment:
|
|
32
|
+
PGRST_DB_URI: postgres://authenticator:${{AUTHENTICATOR_PASSWORD:-authenticator}}@db:5432/{name}
|
|
33
|
+
PGRST_DB_SCHEMAS: public
|
|
34
|
+
PGRST_DB_ANON_ROLE: anon
|
|
35
|
+
PGRST_JWT_SECRET: "@/etc/postgrest/jwks.json"
|
|
36
|
+
PGRST_JWT_AUD: authenticated
|
|
37
|
+
PGRST_OPENAPI_MODE: ignore-privileges
|
|
38
|
+
PGRST_DB_USE_LEGACY_GUCS: "false"
|
|
39
|
+
ports:
|
|
40
|
+
- "54321:3000"
|
|
41
|
+
volumes:
|
|
42
|
+
- ./.supython/jwks.json:/etc/postgrest/jwks.json:ro
|
|
43
|
+
|
|
44
|
+
volumes:
|
|
45
|
+
{name}-db-data:
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
FROM postgres:16-bookworm
|
|
2
|
+
|
|
3
|
+
RUN apt-get update \
|
|
4
|
+
&& apt-get install -y --no-install-recommends postgresql-16-cron \
|
|
5
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
6
|
+
|
|
7
|
+
COPY postgresql.conf /etc/postgresql/postgresql.conf
|
|
8
|
+
|
|
9
|
+
CMD ["postgres", "-c", "config_file=/etc/postgresql/postgresql.conf"]
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# Copy to .env and adjust as needed.
|
|
2
|
+
|
|
3
|
+
DATABASE_URL=postgresql://{name}:{name}@localhost:54322/{name}
|
|
4
|
+
# 0 disables; otherwise per-connection ceiling for any one query.
|
|
5
|
+
DB_STATEMENT_TIMEOUT_MS=30000
|
|
6
|
+
DB_POOL_MIN_SIZE=1
|
|
7
|
+
DB_POOL_MAX_SIZE=10
|
|
8
|
+
DB_ALLOWED_ROLES=anon,authenticated
|
|
9
|
+
|
|
10
|
+
# Asymmetric JWT (RS256). `supython init` generated the keypair under
|
|
11
|
+
# ./.supython/; the private key is .gitignored, the JWKS (public) is safe
|
|
12
|
+
# to commit if you want reproducible PostgREST behavior across clones.
|
|
13
|
+
# To rotate, run: supython keygen --force
|
|
14
|
+
JWT_ALG=RS256
|
|
15
|
+
JWT_PRIVATE_KEY_PATH=./.supython/jwt_private.pem
|
|
16
|
+
# Inline PEM alternative to JWT_PRIVATE_KEY_PATH (use for container secrets).
|
|
17
|
+
# JWT_PRIVATE_KEY=
|
|
18
|
+
JWT_JWKS_PATH=./.supython/jwks.json
|
|
19
|
+
JWT_AUD=authenticated
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
ACCESS_TOKEN_TTL=3600
|
|
23
|
+
REFRESH_TOKEN_TTL=2592000
|
|
24
|
+
|
|
25
|
+
# One-time token lifetimes (seconds): password recover, magic link, email OTP.
|
|
26
|
+
RECOVER_TOKEN_TTL=3600
|
|
27
|
+
MAGIC_LINK_TOKEN_TTL=900
|
|
28
|
+
OTP_TOKEN_TTL=600
|
|
29
|
+
AUTH_RATE_LIMIT_ENABLED=true
|
|
30
|
+
AUTH_RATE_LIMIT_WINDOW_SECONDS=60
|
|
31
|
+
AUTH_RATE_LIMIT_TOKEN_PER_WINDOW=10
|
|
32
|
+
AUTH_RATE_LIMIT_SIGNUP_PER_WINDOW=5
|
|
33
|
+
AUTH_RATE_LIMIT_RECOVER_PER_WINDOW=3
|
|
34
|
+
AUTH_RATE_LIMIT_OTP_PER_WINDOW=5
|
|
35
|
+
AUTH_RATE_LIMIT_MAGICLINK_PER_WINDOW=5
|
|
36
|
+
|
|
37
|
+
# Postgres superuser password. Used by docker-compose.prod.yml; dev compose hardcodes it.
|
|
38
|
+
POSTGRES_PASSWORD={name}
|
|
39
|
+
|
|
40
|
+
# PostgREST connects as `authenticator`; default matches migrations/0001_extensions_and_roles.sql.
|
|
41
|
+
# If you change this, update docker-compose PGRST_DB_URI (or AUTHENTICATOR_PASSWORD) to match.
|
|
42
|
+
AUTHENTICATOR_PASSWORD=authenticator
|
|
43
|
+
|
|
44
|
+
# Email: console (log only) or smtp.
|
|
45
|
+
EMAIL_BACKEND=console
|
|
46
|
+
EMAIL_FROM={name}@localhost
|
|
47
|
+
SMTP_HOST=localhost
|
|
48
|
+
SMTP_PORT=587
|
|
49
|
+
SMTP_USERNAME=
|
|
50
|
+
SMTP_PASSWORD=
|
|
51
|
+
SMTP_STARTTLS=true
|
|
52
|
+
|
|
53
|
+
# OAuth (optional). Leave unset to disable a provider.
|
|
54
|
+
GOOGLE_CLIENT_ID=
|
|
55
|
+
GOOGLE_CLIENT_SECRET=
|
|
56
|
+
GITHUB_CLIENT_ID=
|
|
57
|
+
GITHUB_CLIENT_SECRET=
|
|
58
|
+
|
|
59
|
+
# OAuth state signing key is generated fresh by `supython init` and stored
|
|
60
|
+
# in .supython/secrets/ (manifest: .supython/secrets.json).
|
|
61
|
+
# Rotate via: supython secret rotate oauth
|
|
62
|
+
OAUTH_STATE_MAX_AGE=600
|
|
63
|
+
|
|
64
|
+
POSTGREST_URL=http://localhost:54321
|
|
65
|
+
SITE_URL=http://localhost:8000
|
|
66
|
+
|
|
67
|
+
# Comma-separated list of allowed browser origins (scheme + host + port). Empty = no CORS
|
|
68
|
+
# wildcard; cross-origin browser calls with credentials will not be allowed until you set this.
|
|
69
|
+
# Example: CORS_ORIGINS=http://localhost:3000,https://app.example.com
|
|
70
|
+
CORS_ORIGINS=
|
|
71
|
+
|
|
72
|
+
# Storage: local (default) or s3 (requires pip install supython[s3]).
|
|
73
|
+
STORAGE_BACKEND=local
|
|
74
|
+
STORAGE_LOCAL_ROOT=./storage
|
|
75
|
+
|
|
76
|
+
# S3 / MinIO / R2 (only when STORAGE_BACKEND=s3). Set endpoint for non-AWS.
|
|
77
|
+
STORAGE_S3_ENDPOINT=
|
|
78
|
+
STORAGE_S3_REGION=us-east-1
|
|
79
|
+
STORAGE_S3_BUCKET=
|
|
80
|
+
STORAGE_S3_ACCESS_KEY_ID=
|
|
81
|
+
STORAGE_S3_SECRET_ACCESS_KEY=
|
|
82
|
+
|
|
83
|
+
# Storage signed-URL signing key is generated fresh by `supython init` and
|
|
84
|
+
# stored in .supython/secrets/ (manifest: .supython/secrets.json).
|
|
85
|
+
# Rotate via: supython secret rotate storage
|
|
86
|
+
STORAGE_SIGNED_URL_DEFAULT_TTL=3600
|
|
87
|
+
# Hard cap above any per-bucket file_size_limit (bytes).
|
|
88
|
+
STORAGE_MAX_UPLOAD_BYTES=52428800
|
|
89
|
+
|
|
90
|
+
# Functions: directory holding edge functions (one .py per route).
|
|
91
|
+
FUNCTIONS_DIR=./functions
|
|
92
|
+
# Reload changed function modules on each request. Disable in production.
|
|
93
|
+
FUNCTIONS_HOT_RELOAD=true
|
|
94
|
+
# Max eager request body the dispatcher will buffer before invoking a handler.
|
|
95
|
+
FUNCTIONS_MAX_BODY_BYTES=5242880
|
|
96
|
+
# Max handler execution time (seconds) before cancellation.
|
|
97
|
+
FUNCTIONS_MAX_HANDLER_SECONDS=30.0
|
|
98
|
+
|
|
99
|
+
# Realtime (v0.4)
|
|
100
|
+
REALTIME_ENABLED=true
|
|
101
|
+
REALTIME_NOTIFY_CHANNEL=realtime:changes
|
|
102
|
+
REALTIME_MAX_CONNECTIONS=1000
|
|
103
|
+
REALTIME_MAX_SUBS_PER_CONN=100
|
|
104
|
+
# Server-side idle timeout (seconds); client sends heartbeats every ~25s.
|
|
105
|
+
REALTIME_HEARTBEAT_TIMEOUT_SECONDS=30
|
|
106
|
+
REALTIME_BROKER_QUEUE_SIZE=1000
|
|
107
|
+
REALTIME_RLS_CHECK_TIMEOUT_S=1.0
|
|
108
|
+
REALTIME_BROADCAST_SELF_DEFAULT=false
|
|
109
|
+
|
|
110
|
+
# Jobs & cron (v0.5). Enable the job queue worker.
|
|
111
|
+
JOBS_ENABLED=true
|
|
112
|
+
JOBS_BACKEND=pg
|
|
113
|
+
JOBS_CRON_BACKEND=pg_cron
|
|
114
|
+
JOBS_QUEUE_DEFAULT=default
|
|
115
|
+
JOBS_POLL_INTERVAL_S=1.0
|
|
116
|
+
JOBS_CONCURRENCY=5
|
|
117
|
+
JOBS_DEFAULT_MAX_ATTEMPTS=3
|
|
118
|
+
JOBS_BACKOFF_BASE_S=5.0
|
|
119
|
+
JOBS_BACKOFF_MAX_S=300.0
|
|
120
|
+
JOBS_VISIBILITY_TIMEOUT_S=300.0
|
|
121
|
+
JOBS_VISIBILITY_RECLAIM_BATCH=10
|
|
122
|
+
JOBS_DRAIN_TIMEOUT_S=30.0
|
|
123
|
+
JOBS_DEV_INPROCESS=false
|
|
124
|
+
|
|
125
|
+
# External queue backends (optional — only needed if you switch jobs_backend).
|
|
126
|
+
ARQ_REDIS_URL=redis://localhost:6379
|
|
127
|
+
DRAMATIQ_BROKER_URL=redis://localhost:6379
|
|
128
|
+
|
|
129
|
+
# Backups (v0.5+). pg_dump-based; runs as a job on the worker.
|
|
130
|
+
BACKUPS_DIR=./backups
|
|
131
|
+
BACKUP_TIMEOUT_S=1800
|
|
132
|
+
# host — invoke pg_dump from the worker's PATH (requires postgresql-client).
|
|
133
|
+
# docker — exec pg_dump inside the running postgres container (matches dev compose).
|
|
134
|
+
BACKUP_VIA=docker
|
|
135
|
+
# Required when BACKUP_VIA=docker. Must match the postgres container name in
|
|
136
|
+
# docker-compose.yml; the scaffold names it `{name}-db`.
|
|
137
|
+
BACKUP_DOCKER_CONTAINER={name}-db
|
|
138
|
+
|
|
139
|
+
# Security headers (v0.7)
|
|
140
|
+
SECURITY_HEADERS_ENABLED=true
|
|
141
|
+
# HSTS: None = auto (on iff SITE_URL starts with https://). True/False forces.
|
|
142
|
+
SECURITY_HSTS_ENABLED=
|
|
143
|
+
SECURITY_HSTS_MAX_AGE=31536000
|
|
144
|
+
SECURITY_HSTS_INCLUDE_SUBDOMAINS=true
|
|
145
|
+
SECURITY_HSTS_PRELOAD=false
|
|
146
|
+
SECURITY_FRAME_OPTIONS=DENY
|
|
147
|
+
SECURITY_REFERRER_POLICY=strict-origin-when-cross-origin
|
|
148
|
+
SECURITY_CSP="default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'"
|
|
149
|
+
SECURITY_CSP_EXEMPT_PATHS=/docs,/redoc,/openapi.json,/admin
|
|
150
|
+
# Global request body cap (bytes); exempt paths use their own per-feature caps.
|
|
151
|
+
SECURITY_MAX_BODY_BYTES=1048576
|
|
152
|
+
SECURITY_BODY_LIMIT_EXEMPT_PATHS=/storage/v1/object,/functions
|
|
153
|
+
|
|
154
|
+
# Port published by the prod compose supython service (dev uses hardcoded 8000).
|
|
155
|
+
SUPYTHON_PUBLISH_PORT=8000
|
|
156
|
+
|
|
157
|
+
# Extensions: comma-separated dotted module paths to import at boot.
|
|
158
|
+
# Set automatically by manage.py via SUPYTHON_SETTINGS_MODULE; only set
|
|
159
|
+
# directly if you don't use a settings.py.
|
|
160
|
+
# EXTENSIONS={name}.jobs,{name}.hooks
|
|
161
|
+
|
|
162
|
+
# Settings module: Django-style Python config that declares EXTENSIONS,
|
|
163
|
+
# EXTRA_ROUTERS, EXTRA_MIDDLEWARE. manage.py sets this for you.
|
|
164
|
+
SUPYTHON_SETTINGS_MODULE={name}.settings
|
|
165
|
+
|
|
166
|
+
# Observability
|
|
167
|
+
LOG_LEVEL=INFO
|
|
168
|
+
LOG_JSON=true
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# functions/
|
|
2
|
+
|
|
3
|
+
Drop a `.py` file here and supython will serve it under `POST /functions/<name>` — no server restart required.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
# functions/hello.py
|
|
7
|
+
auth = "anon" # "anon" | "authenticated" (default)
|
|
8
|
+
methods = ["GET", "POST"]
|
|
9
|
+
|
|
10
|
+
async def handler(req, ctx):
|
|
11
|
+
name = ctx.user.email if ctx.user else "world"
|
|
12
|
+
return {{"msg": f"hello, {{name}}"}}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`ctx` gives you:
|
|
16
|
+
- `ctx.db` — a role-scoped `asyncpg.Connection` (RLS already applied)
|
|
17
|
+
- `ctx.user` — caller identity (`id`, `email`, `role`, `claims`)
|
|
18
|
+
- `ctx.storage` — `upload / download / sign` over the storage subsystem
|
|
19
|
+
- `ctx.postgrest` — `httpx.AsyncClient` pre-authed with the caller's JWT
|
|
20
|
+
- `ctx.send_email(to=..., subject=..., text=...)` — send transactional email
|
|
21
|
+
- `ctx.request` — the raw FastAPI `Request` (escape hatch)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
.env
|
|
2
|
+
.venv/
|
|
3
|
+
__pycache__/
|
|
4
|
+
.pytest_cache/
|
|
5
|
+
storage/
|
|
6
|
+
|
|
7
|
+
# Asymmetric JWT private key (public JWKS stays tracked).
|
|
8
|
+
.supython/jwt_private.pem
|
|
9
|
+
.supython/keys/
|
|
10
|
+
.supython/keyset.json
|
|
11
|
+
|
|
12
|
+
# Symmetric secrets
|
|
13
|
+
.supython/secrets/
|
|
14
|
+
.supython/secrets.json
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Project entrypoint. Sets SUPYTHON_SETTINGS_MODULE then delegates to supython CLI."""
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
os.environ.setdefault("SUPYTHON_SETTINGS_MODULE", "{name}.settings")
|
|
7
|
+
|
|
8
|
+
if __name__ == "__main__":
|
|
9
|
+
from supython.cli import app as cli_app
|
|
10
|
+
|
|
11
|
+
cli_app()
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""{name} package."""
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Project settings.
|
|
2
|
+
|
|
3
|
+
Read by the supython CLI when ``SUPYTHON_SETTINGS_MODULE`` is set
|
|
4
|
+
(``manage.py`` does this for you). Pure Python — declare what you want
|
|
5
|
+
mounted on top of the framework.
|
|
6
|
+
|
|
7
|
+
Conventional uppercase attributes:
|
|
8
|
+
|
|
9
|
+
- ``EXTENSIONS`` list[str] dotted module paths imported at boot,
|
|
10
|
+
so ``@job`` / ``@cron`` / ``@on``
|
|
11
|
+
decorators register before the
|
|
12
|
+
FastAPI app builds and before the
|
|
13
|
+
worker starts.
|
|
14
|
+
- ``EXTRA_ROUTERS`` list[str] ``"module.path:router_symbol"``
|
|
15
|
+
strings, mounted via
|
|
16
|
+
``app.include_router``.
|
|
17
|
+
- ``EXTRA_MIDDLEWARE`` list[str] ``"module.path:ClassName"`` strings,
|
|
18
|
+
added via ``app.add_middleware``.
|
|
19
|
+
|
|
20
|
+
Secrets and infrastructure (DATABASE_URL, JWT keys, OAuth secrets) stay
|
|
21
|
+
in env / .env. They are typed by ``supython.settings.Settings`` and
|
|
22
|
+
should never live in this file.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
EXTENSIONS = [
|
|
26
|
+
"{name}.jobs",
|
|
27
|
+
"{name}.hooks",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
EXTRA_ROUTERS: list[str] = []
|
|
31
|
+
EXTRA_MIDDLEWARE: list[str] = []
|