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.
Files changed (200) hide show
  1. supython/__init__.py +24 -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 +162 -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/backups/__init__.py +24 -0
  100. supython/backups/_backup_job.py +170 -0
  101. supython/backups/schemas.py +18 -0
  102. supython/backups/service.py +217 -0
  103. supython/body_size.py +184 -0
  104. supython/cli.py +1663 -0
  105. supython/client/__init__.py +67 -0
  106. supython/client/_auth.py +249 -0
  107. supython/client/_client.py +145 -0
  108. supython/client/_config.py +92 -0
  109. supython/client/_functions.py +69 -0
  110. supython/client/_storage.py +255 -0
  111. supython/client/py.typed +0 -0
  112. supython/db.py +151 -0
  113. supython/db_admin.py +8 -0
  114. supython/extensions.py +36 -0
  115. supython/functions/__init__.py +19 -0
  116. supython/functions/context.py +262 -0
  117. supython/functions/loader.py +307 -0
  118. supython/functions/router.py +228 -0
  119. supython/functions/schemas.py +50 -0
  120. supython/gen/__init__.py +5 -0
  121. supython/gen/_introspect.py +137 -0
  122. supython/gen/types_py.py +270 -0
  123. supython/gen/types_ts.py +365 -0
  124. supython/health.py +229 -0
  125. supython/hooks.py +117 -0
  126. supython/jobs/__init__.py +31 -0
  127. supython/jobs/backends.py +97 -0
  128. supython/jobs/context.py +58 -0
  129. supython/jobs/cron.py +152 -0
  130. supython/jobs/cron_inproc.py +119 -0
  131. supython/jobs/decorators.py +76 -0
  132. supython/jobs/registry.py +79 -0
  133. supython/jobs/router.py +136 -0
  134. supython/jobs/schemas.py +92 -0
  135. supython/jobs/service.py +311 -0
  136. supython/jobs/worker.py +219 -0
  137. supython/jwks.py +257 -0
  138. supython/keyset.py +279 -0
  139. supython/logging_config.py +291 -0
  140. supython/mail.py +33 -0
  141. supython/mailer.py +65 -0
  142. supython/migrate.py +81 -0
  143. supython/migrations/0001_extensions_and_roles.sql +46 -0
  144. supython/migrations/0002_auth_schema.sql +66 -0
  145. supython/migrations/0003_demo_todos.sql +42 -0
  146. supython/migrations/0004_auth_v0_2.sql +47 -0
  147. supython/migrations/0005_storage_schema.sql +117 -0
  148. supython/migrations/0006_realtime_schema.sql +206 -0
  149. supython/migrations/0007_jobs_schema.sql +254 -0
  150. supython/migrations/0008_jobs_last_error.sql +56 -0
  151. supython/migrations/0009_auth_rate_limits.sql +33 -0
  152. supython/migrations/0010_worker_heartbeat.sql +14 -0
  153. supython/migrations/0011_admin_schema.sql +45 -0
  154. supython/migrations/0012_auth_banned_until.sql +10 -0
  155. supython/migrations/0013_email_templates.sql +19 -0
  156. supython/migrations/0014_realtime_payload_warning.sql +96 -0
  157. supython/migrations/0015_backups_schema.sql +14 -0
  158. supython/passwords.py +15 -0
  159. supython/realtime/__init__.py +6 -0
  160. supython/realtime/broker.py +814 -0
  161. supython/realtime/protocol.py +234 -0
  162. supython/realtime/router.py +184 -0
  163. supython/realtime/schemas.py +207 -0
  164. supython/realtime/service.py +261 -0
  165. supython/realtime/topics.py +175 -0
  166. supython/realtime/websocket.py +586 -0
  167. supython/scaffold/__init__.py +5 -0
  168. supython/scaffold/init_project.py +144 -0
  169. supython/scaffold/templates/Caddyfile.tmpl +4 -0
  170. supython/scaffold/templates/README.md.tmpl +22 -0
  171. supython/scaffold/templates/apps_hooks.py.tmpl +11 -0
  172. supython/scaffold/templates/apps_jobs.py.tmpl +8 -0
  173. supython/scaffold/templates/asgi.py.tmpl +14 -0
  174. supython/scaffold/templates/docker-compose.prod.yml.tmpl +84 -0
  175. supython/scaffold/templates/docker-compose.yml.tmpl +45 -0
  176. supython/scaffold/templates/docker_postgres_Dockerfile.tmpl +9 -0
  177. supython/scaffold/templates/docker_postgres_postgresql.conf.tmpl +3 -0
  178. supython/scaffold/templates/env.example.tmpl +168 -0
  179. supython/scaffold/templates/functions_README.md.tmpl +21 -0
  180. supython/scaffold/templates/gitignore.tmpl +14 -0
  181. supython/scaffold/templates/manage.py.tmpl +11 -0
  182. supython/scaffold/templates/migrations/.gitkeep +0 -0
  183. supython/scaffold/templates/package_init.py.tmpl +1 -0
  184. supython/scaffold/templates/settings.py.tmpl +31 -0
  185. supython/secretset.py +347 -0
  186. supython/security_headers.py +78 -0
  187. supython/settings.py +244 -0
  188. supython/settings_module.py +117 -0
  189. supython/storage/__init__.py +5 -0
  190. supython/storage/backends.py +392 -0
  191. supython/storage/router.py +341 -0
  192. supython/storage/schemas.py +50 -0
  193. supython/storage/service.py +445 -0
  194. supython/storage/signing.py +119 -0
  195. supython/tokens.py +85 -0
  196. supython-0.1.0.dist-info/METADATA +756 -0
  197. supython-0.1.0.dist-info/RECORD +200 -0
  198. supython-0.1.0.dist-info/WHEEL +4 -0
  199. supython-0.1.0.dist-info/entry_points.txt +2 -0
  200. 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,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,11 @@
1
+ """Auth hooks for {name}."""
2
+ from supython.hooks import on
3
+
4
+
5
+ @on("signup")
6
+ async def welcome(user, ctx) -> None:
7
+ await ctx.send_email(
8
+ to=user.email,
9
+ subject="Welcome to {name}",
10
+ text="Thanks for signing up.",
11
+ )
@@ -0,0 +1,8 @@
1
+ """Background jobs for {name}."""
2
+ from supython.jobs.context import JobCtx
3
+ from supython.jobs.decorators import job
4
+
5
+
6
+ @job("{name}_example_job")
7
+ async def example_job(ctx: JobCtx) -> None:
8
+ ctx.logger.info("example_job ran with payload=%s", ctx.payload)
@@ -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,3 @@
1
+ shared_preload_libraries = 'pg_cron'
2
+ cron.database_name = '{name}'
3
+ listen_addresses = '*'
@@ -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] = []