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
supython/body_size.py ADDED
@@ -0,0 +1,184 @@
1
+ """Reject oversized request bodies before they reach the app.
2
+
3
+ The cap is the first line of defense for routes that accept JSON/form
4
+ payloads — auth, jobs control plane, realtime control, etc. Anything that
5
+ genuinely streams (storage uploads, functions) is exempted via path
6
+ prefix and governed by its own per-feature setting.
7
+
8
+ The motivation is concrete: argon2 hashes the entire submitted password,
9
+ so a multi-megabyte password DoS-es a worker. Bound the body, the worry
10
+ goes away.
11
+ """
12
+
13
+ import logging
14
+ from collections.abc import Awaitable, Callable
15
+ from typing import Any
16
+
17
+ from .settings import Settings, get_settings
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ ERR_BODY_TOO_LARGE = "body_too_large"
22
+
23
+ # Methods that may carry a body. We don't gate GET/HEAD/OPTIONS — even if
24
+ # a curious client attaches one, FastAPI ignores it for those methods.
25
+ _BODY_METHODS = frozenset({"POST", "PUT", "PATCH"})
26
+
27
+
28
+ class _BodyTooLargeError(Exception):
29
+ """Raised by the wrapped ASGI receive() once the cap is exceeded.
30
+
31
+ Propagates out of FastAPI's body-parsing code (``request.body()`` /
32
+ ``request.json()``) and is caught by ``BodySizeLimitMiddleware``,
33
+ which converts it into a 413 response. Custom class so we don't
34
+ accidentally swallow legitimate exceptions from the inner app.
35
+ """
36
+
37
+
38
+ def _path_matches(path: str, prefixes: tuple[str, ...]) -> bool:
39
+ return any(path == p or path.startswith(p + "/") for p in prefixes)
40
+
41
+
42
+ class BodySizeLimitMiddleware:
43
+ """ASGI middleware that enforces ``security_max_body_bytes``.
44
+
45
+ Two-layer defense:
46
+
47
+ 1. *Cheap path*: reject up-front when ``Content-Length`` declares a
48
+ body larger than the cap. The inner app is never invoked.
49
+ 2. *Streaming path*: forward chunks to the inner app as they arrive
50
+ while counting bytes. As soon as the cap is exceeded, the wrapped
51
+ receive() raises :class:`_BodyTooLargeError`, which propagates out
52
+ of the route handler and is caught here. The middleware then sends
53
+ a 413 — provided the inner app hasn't already started a response.
54
+
55
+ Streaming (rather than buffering) keeps memory bounded and lets
56
+ routes that *do* legitimately stream (storage, functions, exempt by
57
+ path prefix) operate without a copy.
58
+ """
59
+
60
+ def __init__(self, app: Any, settings: Settings | None = None) -> None:
61
+ self.app = app
62
+ self._settings = settings or get_settings()
63
+ self._max_bytes = self._settings.security_max_body_bytes
64
+ self._exempt = tuple(
65
+ p.strip()
66
+ for p in self._settings.security_body_limit_exempt_paths.split(",")
67
+ if p.strip()
68
+ )
69
+
70
+ async def __call__(
71
+ self,
72
+ scope: dict[str, Any],
73
+ receive: Callable[[], Awaitable[dict[str, Any]]],
74
+ send: Callable[[dict[str, Any]], Awaitable[None]],
75
+ ) -> None:
76
+ if scope["type"] != "http" or self._max_bytes <= 0:
77
+ await self.app(scope, receive, send)
78
+ return
79
+
80
+ method = scope.get("method", "").upper()
81
+ if method not in _BODY_METHODS:
82
+ await self.app(scope, receive, send)
83
+ return
84
+
85
+ if _path_matches(scope.get("path", ""), self._exempt):
86
+ await self.app(scope, receive, send)
87
+ return
88
+
89
+ if not await self._content_length_ok(scope, send):
90
+ return
91
+
92
+ await self._enforce_streaming(scope, receive, send)
93
+
94
+ async def _content_length_ok(
95
+ self,
96
+ scope: dict[str, Any],
97
+ send: Callable[[dict[str, Any]], Awaitable[None]],
98
+ ) -> bool:
99
+ for name, value in scope.get("headers", []):
100
+ if name == b"content-length":
101
+ try:
102
+ declared = int(value)
103
+ except ValueError:
104
+ await self._send_413(send, "Malformed Content-Length")
105
+ return False
106
+ if declared > self._max_bytes:
107
+ await self._send_413(send)
108
+ return False
109
+ break
110
+ return True
111
+
112
+ async def _enforce_streaming(
113
+ self,
114
+ scope: dict[str, Any],
115
+ receive: Callable[[], Awaitable[dict[str, Any]]],
116
+ send: Callable[[dict[str, Any]], Awaitable[None]],
117
+ ) -> None:
118
+ received = 0
119
+ response_started = False
120
+
121
+ async def _bounded_receive() -> dict[str, Any]:
122
+ nonlocal received
123
+ msg = await receive()
124
+ if msg.get("type") == "http.request":
125
+ received += len(msg.get("body", b""))
126
+ if received > self._max_bytes:
127
+ raise _BodyTooLargeError()
128
+ return msg
129
+
130
+ async def _send_wrapper(msg: dict[str, Any]) -> None:
131
+ nonlocal response_started
132
+ if msg["type"] == "http.response.start":
133
+ response_started = True
134
+ await send(msg)
135
+
136
+ try:
137
+ await self.app(scope, _bounded_receive, _send_wrapper)
138
+ except _BodyTooLargeError:
139
+ if not response_started:
140
+ await self._send_413(send)
141
+ else:
142
+ # The app already committed to a response before we
143
+ # noticed the overflow. We can't change the status — the
144
+ # bytes have left the building — but we should make this
145
+ # visible: the request was incomplete from our side.
146
+ logger.warning(
147
+ "body-size: cap exceeded after response started "
148
+ "(path=%s); response delivered but body was truncated",
149
+ scope.get("path", ""),
150
+ )
151
+
152
+ async def _send_413(
153
+ self,
154
+ send: Callable[[dict[str, Any]], Awaitable[None]],
155
+ message: str | None = None,
156
+ ) -> None:
157
+ msg = message or (
158
+ f"Request body exceeds maximum size of {self._max_bytes} bytes"
159
+ )
160
+ body = (
161
+ b'{"detail":{"code":"'
162
+ + ERR_BODY_TOO_LARGE.encode("ascii")
163
+ + b'","message":"'
164
+ + msg.encode("utf-8").replace(b'"', b'\\"')
165
+ + b'"}}'
166
+ )
167
+ await send(
168
+ {
169
+ "type": "http.response.start",
170
+ "status": 413,
171
+ "headers": [
172
+ (b"content-type", b"application/json"),
173
+ (b"content-length", str(len(body)).encode("ascii")),
174
+ (b"connection", b"close"),
175
+ ],
176
+ }
177
+ )
178
+ await send(
179
+ {
180
+ "type": "http.response.body",
181
+ "body": body,
182
+ "more_body": False,
183
+ }
184
+ )