supython 0.5.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.
Files changed (188) hide show
  1. supython/__init__.py +8 -0
  2. supython/admin/__init__.py +3 -0
  3. supython/admin/api/__init__.py +24 -0
  4. supython/admin/api/auth.py +118 -0
  5. supython/admin/api/auth_templates.py +67 -0
  6. supython/admin/api/auth_users.py +225 -0
  7. supython/admin/api/db.py +174 -0
  8. supython/admin/api/functions.py +92 -0
  9. supython/admin/api/jobs.py +192 -0
  10. supython/admin/api/ops.py +224 -0
  11. supython/admin/api/realtime.py +281 -0
  12. supython/admin/api/service_auth.py +49 -0
  13. supython/admin/api/service_auth_templates.py +83 -0
  14. supython/admin/api/service_auth_users.py +346 -0
  15. supython/admin/api/service_db.py +214 -0
  16. supython/admin/api/service_functions.py +287 -0
  17. supython/admin/api/service_jobs.py +282 -0
  18. supython/admin/api/service_ops.py +213 -0
  19. supython/admin/api/service_realtime.py +30 -0
  20. supython/admin/api/service_storage.py +220 -0
  21. supython/admin/api/storage.py +117 -0
  22. supython/admin/api/system.py +37 -0
  23. supython/admin/audit.py +29 -0
  24. supython/admin/deps.py +22 -0
  25. supython/admin/errors.py +16 -0
  26. supython/admin/schemas.py +310 -0
  27. supython/admin/session.py +52 -0
  28. supython/admin/spa.py +38 -0
  29. supython/admin/static/assets/Alert-dluGVkos.js +49 -0
  30. supython/admin/static/assets/Audit-Njung3HI.js +2 -0
  31. supython/admin/static/assets/Backups-DzPlFgrm.js +2 -0
  32. supython/admin/static/assets/Buckets-ByacGkU1.js +2 -0
  33. supython/admin/static/assets/Channels-BoIuTtam.js +353 -0
  34. supython/admin/static/assets/ChevronRight-CtQH1EQ1.js +2 -0
  35. supython/admin/static/assets/CodeViewer-Bqy7-wvH.js +2 -0
  36. supython/admin/static/assets/Crons-B67vc39F.js +2 -0
  37. supython/admin/static/assets/DashboardView-CUTFVL6k.js +2 -0
  38. supython/admin/static/assets/DataTable-COAAWEft.js +747 -0
  39. supython/admin/static/assets/DescriptionsItem-P8JUDaBs.js +75 -0
  40. supython/admin/static/assets/DrawerContent-TpYTFgF1.js +139 -0
  41. supython/admin/static/assets/Empty-cr2r7e2u.js +25 -0
  42. supython/admin/static/assets/EmptyState-DeDck-OL.js +2 -0
  43. supython/admin/static/assets/Grid-hFkp9F4P.js +2 -0
  44. supython/admin/static/assets/Input-DppYTq9C.js +259 -0
  45. supython/admin/static/assets/Invoke-DW3Nveeh.js +2 -0
  46. supython/admin/static/assets/JsonField-DibyJgun.js +2 -0
  47. supython/admin/static/assets/LoginView-BjLyE3Ds.css +1 -0
  48. supython/admin/static/assets/LoginView-CoOjECT_.js +111 -0
  49. supython/admin/static/assets/Logs-D9WYrnIT.js +2 -0
  50. supython/admin/static/assets/Logs-DS1XPa0h.css +1 -0
  51. supython/admin/static/assets/Migrations-DOSC2ddQ.js +2 -0
  52. supython/admin/static/assets/ObjectBrowser-_5w8vOX8.js +2 -0
  53. supython/admin/static/assets/Queue-CywZs6vI.js +2 -0
  54. supython/admin/static/assets/RefreshTokens-Ccjr53jg.js +2 -0
  55. supython/admin/static/assets/RlsEditor-BSlH9vSc.js +2 -0
  56. supython/admin/static/assets/Routes-BiLXE49D.js +2 -0
  57. supython/admin/static/assets/Routes-C-ianIGD.css +1 -0
  58. supython/admin/static/assets/SchemaBrowser-DKy2_KQi.css +1 -0
  59. supython/admin/static/assets/SchemaBrowser-XFvFbtDB.js +2 -0
  60. supython/admin/static/assets/Select-DIzZyRZb.js +434 -0
  61. supython/admin/static/assets/Space-n5-XcguU.js +400 -0
  62. supython/admin/static/assets/SqlEditor-b8pTsILY.js +3 -0
  63. supython/admin/static/assets/SqlWorkspace-BUS7IntH.js +104 -0
  64. supython/admin/static/assets/TableData-CQIagLKn.js +2 -0
  65. supython/admin/static/assets/Tag-D1fOKpTH.js +72 -0
  66. supython/admin/static/assets/Templates-BS-ugkdq.js +2 -0
  67. supython/admin/static/assets/Thing-CEAniuMg.js +107 -0
  68. supython/admin/static/assets/Users-wzwajhlh.js +2 -0
  69. supython/admin/static/assets/_plugin-vue_export-helper-DGA9ry_j.js +1 -0
  70. supython/admin/static/assets/dist-VXIJLCYq.js +13 -0
  71. supython/admin/static/assets/format-length-CGCY1rMh.js +2 -0
  72. supython/admin/static/assets/get-Ca6unauB.js +2 -0
  73. supython/admin/static/assets/index-CeE6v959.js +951 -0
  74. supython/admin/static/assets/pinia-COXwfrOX.js +2 -0
  75. supython/admin/static/assets/resources-Bt6thQCD.js +44 -0
  76. supython/admin/static/assets/use-locale-mtgM0a3a.js +2 -0
  77. supython/admin/static/assets/use-merged-state-BvhkaHNX.js +2 -0
  78. supython/admin/static/assets/useConfirm-tMjvBFXR.js +2 -0
  79. supython/admin/static/assets/useResource-C_rJCY8C.js +2 -0
  80. supython/admin/static/assets/useTable-CnZc5zhi.js +363 -0
  81. supython/admin/static/assets/useTable-Dg0XlRlq.css +1 -0
  82. supython/admin/static/assets/useToast-DsZKx0IX.js +2 -0
  83. supython/admin/static/assets/utils-sbXoq7Ir.js +2 -0
  84. supython/admin/static/favicon.svg +1 -0
  85. supython/admin/static/icons.svg +24 -0
  86. supython/admin/static/index.html +24 -0
  87. supython/app.py +149 -0
  88. supython/auth/__init__.py +3 -0
  89. supython/auth/_email_job.py +11 -0
  90. supython/auth/providers/__init__.py +34 -0
  91. supython/auth/providers/github.py +22 -0
  92. supython/auth/providers/google.py +19 -0
  93. supython/auth/providers/oauth.py +56 -0
  94. supython/auth/providers/registry.py +16 -0
  95. supython/auth/ratelimit.py +39 -0
  96. supython/auth/router.py +282 -0
  97. supython/auth/schemas.py +79 -0
  98. supython/auth/service.py +587 -0
  99. supython/body_size.py +184 -0
  100. supython/cli.py +1653 -0
  101. supython/client/__init__.py +67 -0
  102. supython/client/_auth.py +249 -0
  103. supython/client/_client.py +145 -0
  104. supython/client/_config.py +92 -0
  105. supython/client/_functions.py +69 -0
  106. supython/client/_storage.py +255 -0
  107. supython/client/py.typed +0 -0
  108. supython/db.py +151 -0
  109. supython/db_admin.py +8 -0
  110. supython/functions/__init__.py +19 -0
  111. supython/functions/context.py +262 -0
  112. supython/functions/loader.py +307 -0
  113. supython/functions/router.py +228 -0
  114. supython/functions/schemas.py +50 -0
  115. supython/gen/__init__.py +5 -0
  116. supython/gen/_introspect.py +137 -0
  117. supython/gen/types_py.py +270 -0
  118. supython/gen/types_ts.py +365 -0
  119. supython/health.py +229 -0
  120. supython/hooks.py +117 -0
  121. supython/jobs/__init__.py +31 -0
  122. supython/jobs/backends.py +97 -0
  123. supython/jobs/context.py +58 -0
  124. supython/jobs/cron.py +152 -0
  125. supython/jobs/cron_inproc.py +118 -0
  126. supython/jobs/decorators.py +76 -0
  127. supython/jobs/registry.py +79 -0
  128. supython/jobs/router.py +136 -0
  129. supython/jobs/schemas.py +92 -0
  130. supython/jobs/service.py +311 -0
  131. supython/jobs/worker.py +219 -0
  132. supython/jwks.py +257 -0
  133. supython/keyset.py +279 -0
  134. supython/logging_config.py +291 -0
  135. supython/mail.py +33 -0
  136. supython/mailer.py +65 -0
  137. supython/migrate.py +81 -0
  138. supython/migrations/0001_extensions_and_roles.sql +46 -0
  139. supython/migrations/0002_auth_schema.sql +66 -0
  140. supython/migrations/0003_demo_todos.sql +42 -0
  141. supython/migrations/0004_auth_v0_2.sql +47 -0
  142. supython/migrations/0005_storage_schema.sql +117 -0
  143. supython/migrations/0006_realtime_schema.sql +206 -0
  144. supython/migrations/0007_jobs_schema.sql +254 -0
  145. supython/migrations/0008_jobs_last_error.sql +56 -0
  146. supython/migrations/0009_auth_rate_limits.sql +33 -0
  147. supython/migrations/0010_worker_heartbeat.sql +14 -0
  148. supython/migrations/0011_admin_schema.sql +45 -0
  149. supython/migrations/0012_auth_banned_until.sql +10 -0
  150. supython/migrations/0013_email_templates.sql +19 -0
  151. supython/migrations/0014_realtime_payload_warning.sql +96 -0
  152. supython/migrations/0015_backups_schema.sql +14 -0
  153. supython/passwords.py +15 -0
  154. supython/realtime/__init__.py +6 -0
  155. supython/realtime/broker.py +814 -0
  156. supython/realtime/protocol.py +234 -0
  157. supython/realtime/router.py +184 -0
  158. supython/realtime/schemas.py +207 -0
  159. supython/realtime/service.py +261 -0
  160. supython/realtime/topics.py +175 -0
  161. supython/realtime/websocket.py +586 -0
  162. supython/scaffold/__init__.py +5 -0
  163. supython/scaffold/init_project.py +133 -0
  164. supython/scaffold/templates/Caddyfile.tmpl +4 -0
  165. supython/scaffold/templates/README.md.tmpl +22 -0
  166. supython/scaffold/templates/docker-compose.prod.yml.tmpl +84 -0
  167. supython/scaffold/templates/docker-compose.yml.tmpl +41 -0
  168. supython/scaffold/templates/docker_postgres_Dockerfile.tmpl +9 -0
  169. supython/scaffold/templates/docker_postgres_postgresql.conf.tmpl +3 -0
  170. supython/scaffold/templates/env.example.tmpl +149 -0
  171. supython/scaffold/templates/functions_README.md.tmpl +21 -0
  172. supython/scaffold/templates/gitignore.tmpl +14 -0
  173. supython/scaffold/templates/migrations/.gitkeep +0 -0
  174. supython/secretset.py +347 -0
  175. supython/security_headers.py +78 -0
  176. supython/settings.py +198 -0
  177. supython/storage/__init__.py +5 -0
  178. supython/storage/backends.py +392 -0
  179. supython/storage/router.py +341 -0
  180. supython/storage/schemas.py +50 -0
  181. supython/storage/service.py +445 -0
  182. supython/storage/signing.py +119 -0
  183. supython/tokens.py +85 -0
  184. supython-0.5.0.dist-info/METADATA +714 -0
  185. supython-0.5.0.dist-info/RECORD +188 -0
  186. supython-0.5.0.dist-info/WHEEL +4 -0
  187. supython-0.5.0.dist-info/entry_points.txt +2 -0
  188. supython-0.5.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,133 @@
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
+ ]
26
+
27
+
28
+ def scaffold(name: str, target: Path, *, force: bool = False) -> list[Path]:
29
+ """Write a minimal supython project into *target*.
30
+
31
+ Returns the list of paths written, in order.
32
+ Raises ``FileExistsError`` if *target* is non-empty and *force`` is False.
33
+ """
34
+ if target.exists() and any(target.iterdir()) and not force:
35
+ raise FileExistsError(f"{target} is not empty. Use --force to overwrite.")
36
+
37
+ target.mkdir(parents=True, exist_ok=True)
38
+ (target / "migrations").mkdir(exist_ok=True)
39
+ (target / "functions").mkdir(exist_ok=True)
40
+
41
+ vars: dict[str, str] = {
42
+ "name": name,
43
+ }
44
+
45
+ written: list[Path] = []
46
+
47
+ for tmpl_name, dest_rel in _TEMPLATE_MAP:
48
+ tmpl_path = _TEMPLATES_DIR / tmpl_name
49
+ content = tmpl_path.read_text().format(**vars)
50
+ dest = target / dest_rel
51
+ dest.parent.mkdir(parents=True, exist_ok=True)
52
+ if dest.exists() and not force:
53
+ continue
54
+ dest.write_text(content)
55
+ written.append(dest)
56
+
57
+ gitkeep = target / "migrations" / ".gitkeep"
58
+ if not gitkeep.exists() or force:
59
+ gitkeep.write_text("")
60
+ written.append(gitkeep)
61
+
62
+ written.extend(_write_scaffold_keypair(target, force=force))
63
+ written.extend(_write_scaffold_secrets(target, force=force))
64
+ return written
65
+
66
+
67
+ def _write_scaffold_keypair(target: Path, *, force: bool) -> list[Path]:
68
+ key_dir = target / _JWT_KEYS_DIRNAME
69
+ key_dir.mkdir(parents=True, exist_ok=True)
70
+ pem_path = key_dir / _PRIVATE_KEY_FILENAME
71
+ jwks_path = key_dir / _JWKS_FILENAME
72
+
73
+ if not force and pem_path.exists() and jwks_path.exists():
74
+ return []
75
+
76
+ key = _jwks.generate_private_key("RS256")
77
+ signer = _jwks.signing_key_from_private_key(key, "RS256")
78
+ _jwks.write_private_key_pem(pem_path, _jwks.private_key_to_pem(key), force=force)
79
+ _jwks.write_jwks_file(jwks_path, _jwks.jwks_for_signing_key(signer))
80
+ return [pem_path, jwks_path]
81
+
82
+
83
+ def _write_scaffold_secrets(target: Path, *, force: bool) -> list[Path]:
84
+ secrets_dir = target / ".supython" / "secrets"
85
+ secrets_dir.mkdir(parents=True, exist_ok=True)
86
+ manifest_path = target / ".supython" / "secrets.json"
87
+
88
+ if not force and manifest_path.exists():
89
+ return []
90
+
91
+ oauth_secret = secrets.token_urlsafe(48)
92
+ storage_secret = secrets.token_urlsafe(48)
93
+
94
+ oauth_kid = "v1"
95
+ storage_kid = "v1"
96
+
97
+ (secrets_dir / f"oauth_state.{oauth_kid}.secret").write_text(oauth_secret)
98
+ (secrets_dir / f"storage_signed_url.{storage_kid}.secret").write_text(storage_secret)
99
+ if os.name == "posix":
100
+ (secrets_dir / f"oauth_state.{oauth_kid}.secret").chmod(0o600)
101
+ (secrets_dir / f"storage_signed_url.{storage_kid}.secret").chmod(0o600)
102
+
103
+ now_iso = "2026-01-01T00:00:00Z" # static; tests read back the actual values
104
+ manifest = {
105
+ "oauth_state": {
106
+ "active": oauth_kid,
107
+ "keys": [
108
+ {
109
+ "kid": oauth_kid,
110
+ "status": "active",
111
+ "created_at": now_iso,
112
+ "retired_at": None,
113
+ }
114
+ ],
115
+ },
116
+ "storage_signed_url": {
117
+ "active": storage_kid,
118
+ "keys": [
119
+ {
120
+ "kid": storage_kid,
121
+ "status": "active",
122
+ "created_at": now_iso,
123
+ "retired_at": None,
124
+ }
125
+ ],
126
+ },
127
+ }
128
+ manifest_path.write_text(json.dumps(manifest, sort_keys=True, indent=2) + "\n")
129
+ return [
130
+ secrets_dir / f"oauth_state.{oauth_kid}.secret",
131
+ secrets_dir / f"storage_signed_url.{storage_kid}.secret",
132
+ manifest_path,
133
+ ]
@@ -0,0 +1,4 @@
1
+ {{$SITE_URL}} {{
2
+ reverse_proxy /rest/v1/* postgrest:3000
3
+ reverse_proxy supython:8000
4
+ }}
@@ -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,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,41 @@
1
+ name: {name}
2
+
3
+ services:
4
+ db:
5
+ image: postgres:16-alpine
6
+ container_name: {name}-db
7
+ environment:
8
+ POSTGRES_USER: {name}
9
+ POSTGRES_PASSWORD: {name}
10
+ POSTGRES_DB: {name}
11
+ ports:
12
+ - "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
+ ports:
36
+ - "54321:3000"
37
+ volumes:
38
+ - ./.supython/jwks.json:/etc/postgrest/jwks.json:ro
39
+
40
+ volumes:
41
+ {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,3 @@
1
+ shared_preload_libraries = 'pg_cron'
2
+ cron.database_name = '{name}'
3
+ listen_addresses = '*'
@@ -0,0 +1,149 @@
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
+ # Security headers (v0.7)
130
+ SECURITY_HEADERS_ENABLED=true
131
+ # HSTS: None = auto (on iff SITE_URL starts with https://). True/False forces.
132
+ SECURITY_HSTS_ENABLED=
133
+ SECURITY_HSTS_MAX_AGE=31536000
134
+ SECURITY_HSTS_INCLUDE_SUBDOMAINS=true
135
+ SECURITY_HSTS_PRELOAD=false
136
+ SECURITY_FRAME_OPTIONS=DENY
137
+ SECURITY_REFERRER_POLICY=strict-origin-when-cross-origin
138
+ SECURITY_CSP="default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'"
139
+ SECURITY_CSP_EXEMPT_PATHS=/docs,/redoc,/openapi.json,/admin
140
+ # Global request body cap (bytes); exempt paths use their own per-feature caps.
141
+ SECURITY_MAX_BODY_BYTES=1048576
142
+ SECURITY_BODY_LIMIT_EXEMPT_PATHS=/storage/v1/object,/functions
143
+
144
+ # Port published by the prod compose supython service (dev uses hardcoded 8000).
145
+ SUPYTHON_PUBLISH_PORT=8000
146
+
147
+ # Observability
148
+ LOG_LEVEL=INFO
149
+ 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
File without changes