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 @@
1
+ .filter-bar[data-v-6ba6e82b]{flex-wrap:wrap;align-items:flex-end;gap:8px;margin-bottom:12px;display:flex}.filter-bar__search[data-v-6ba6e82b]{border:1px solid var(--n-border-color,#ffffff1f);min-width:200px;color:inherit;box-sizing:border-box;background:#ffffff0d;border-radius:4px;outline:none;flex:220px;height:34px;padding:6px 10px;font-size:13px}.filter-bar__field[data-v-6ba6e82b]{flex-direction:column;gap:2px;display:flex}.filter-bar__label[data-v-6ba6e82b]{color:var(--n-text-color-3,#ffffff8c);font-size:11px}
@@ -0,0 +1,2 @@
1
+ import{En as e}from"./Space-n5-XcguU.js";function t(){return e(`discrete`).message}export{t};
2
+ //# sourceMappingURL=useToast-DsZKx0IX.js.map
@@ -0,0 +1,2 @@
1
+ import{Cn as e}from"./Space-n5-XcguU.js";var t=[],n=new WeakMap;function r(){t.forEach(e=>e(...n.get(e))),t=[]}function i(e,...i){n.set(e,i),!t.includes(e)&&t.push(e)===1&&requestAnimationFrame(r)}function a(){return e()!==null}var o=typeof window<`u`;export{o as n,i as r,a as t};
2
+ //# sourceMappingURL=utils-sbXoq7Ir.js.map
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="48" height="46" fill="none" viewBox="0 0 48 46"><path fill="#863bff" d="M25.946 44.938c-.664.845-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.287c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.497 0-3.578-1.842-3.578H1.237c-.92 0-1.456-1.04-.92-1.788L10.013.474c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.579 1.842 3.579h11.377c.943 0 1.473 1.088.89 1.83L25.947 44.94z" style="fill:#863bff;fill:color(display-p3 .5252 .23 1);fill-opacity:1"/><mask id="a" width="48" height="46" x="0" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M25.842 44.938c-.664.844-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.183c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.498 0-3.579-1.842-3.579H1.133c-.92 0-1.456-1.04-.92-1.787L9.91.473c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.578 1.842 3.578h11.377c.943 0 1.473 1.088.89 1.832L25.843 44.94z" style="fill:#000;fill-opacity:1"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#ede6ff" rx="5.508" ry="14.704" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -4.47 31.516)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#ede6ff" rx="10.399" ry="29.851" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -39.328 7.883)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#7e14ff" rx="5.508" ry="30.487" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -25.913 -14.639)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -32.644 -3.334)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -34.34 30.47)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#ede6ff" rx="14.072" ry="22.078" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="rotate(93.35 24.506 48.493)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx=".387" cy="8.972" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(39.51 .387 8.972)"/></g><g filter="url(#k)"><ellipse cx="47.523" cy="-6.092" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 47.523 -6.092)"/></g><g filter="url(#l)"><ellipse cx="41.412" cy="6.333" fill="#47bfff" rx="5.971" ry="9.665" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 41.412 6.333)"/></g><g filter="url(#m)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#n)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#o)"><ellipse cx="35.651" cy="29.907" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 35.651 29.907)"/></g><g filter="url(#p)"><ellipse cx="38.418" cy="32.4" fill="#47bfff" rx="5.971" ry="15.297" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 38.418 32.4)"/></g></g><defs><filter id="b" width="60.045" height="41.654" x="-19.77" y="16.149" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-54.613" y="-7.533" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-49.64" y="2.03" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-45.045" y="20.029" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-43.513" y="21.178" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="g" width="74.749" height="58.852" x="15.756" y="-17.901" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="h" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="i" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="j" width="56.045" height="63.649" x="-27.636" y="-22.853" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="k" width="54.814" height="64.646" x="20.116" y="-38.415" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="l" width="33.541" height="35.313" x="24.641" y="-11.323" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="m" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="n" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="o" width="54.814" height="64.646" x="8.244" y="-2.416" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="p" width="39.409" height="43.623" x="18.713" y="10.588" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter></defs></svg>
@@ -0,0 +1,24 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg">
2
+ <symbol id="bluesky-icon" viewBox="0 0 16 17">
3
+ <g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
4
+ <defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
5
+ </symbol>
6
+ <symbol id="discord-icon" viewBox="0 0 20 19">
7
+ <path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
8
+ </symbol>
9
+ <symbol id="documentation-icon" viewBox="0 0 21 20">
10
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
11
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
12
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
13
+ </symbol>
14
+ <symbol id="github-icon" viewBox="0 0 19 19">
15
+ <path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
16
+ </symbol>
17
+ <symbol id="social-icon" viewBox="0 0 20 20">
18
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
19
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
20
+ </symbol>
21
+ <symbol id="x-icon" viewBox="0 0 19 19">
22
+ <path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
23
+ </symbol>
24
+ </svg>
@@ -0,0 +1,24 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>supython admin</title>
7
+ <script type="module" crossorigin src="/admin/assets/index-CeE6v959.js"></script>
8
+ <link rel="modulepreload" crossorigin href="/admin/assets/Space-n5-XcguU.js">
9
+ <link rel="modulepreload" crossorigin href="/admin/assets/use-locale-mtgM0a3a.js">
10
+ <link rel="modulepreload" crossorigin href="/admin/assets/get-Ca6unauB.js">
11
+ <link rel="modulepreload" crossorigin href="/admin/assets/use-merged-state-BvhkaHNX.js">
12
+ <link rel="modulepreload" crossorigin href="/admin/assets/Input-DppYTq9C.js">
13
+ <link rel="modulepreload" crossorigin href="/admin/assets/Empty-cr2r7e2u.js">
14
+ <link rel="modulepreload" crossorigin href="/admin/assets/format-length-CGCY1rMh.js">
15
+ <link rel="modulepreload" crossorigin href="/admin/assets/Tag-D1fOKpTH.js">
16
+ <link rel="modulepreload" crossorigin href="/admin/assets/utils-sbXoq7Ir.js">
17
+ <link rel="modulepreload" crossorigin href="/admin/assets/Select-DIzZyRZb.js">
18
+ <link rel="modulepreload" crossorigin href="/admin/assets/ChevronRight-CtQH1EQ1.js">
19
+ <link rel="modulepreload" crossorigin href="/admin/assets/pinia-COXwfrOX.js">
20
+ </head>
21
+ <body>
22
+ <div id="app"></div>
23
+ </body>
24
+ </html>
supython/app.py ADDED
@@ -0,0 +1,162 @@
1
+ import asyncio
2
+ import contextlib
3
+ import logging
4
+ from collections.abc import AsyncIterator, Callable, Coroutine
5
+ from contextlib import asynccontextmanager
6
+ from typing import Any
7
+
8
+ from fastapi import FastAPI
9
+ from fastapi.middleware.cors import CORSMiddleware
10
+
11
+ from . import __version__, db, jwks
12
+ from .admin import router as admin_api_router
13
+ from .admin import spa as admin_spa
14
+ from .auth.router import router as auth_router
15
+ from .extensions import load_extensions
16
+ from .settings_module import UserSettings, load_user_settings
17
+ from .functions.loader import get_registry as get_function_registry
18
+ from .functions.router import router as functions_router
19
+ from .health import router as health_router
20
+ from .hooks import on as hook_on
21
+ from .logging_config import (
22
+ RequestIdMiddleware,
23
+ RequestLoggingMiddleware,
24
+ configure_logging,
25
+ )
26
+ from .body_size import BodySizeLimitMiddleware
27
+ from .security_headers import SecurityHeadersMiddleware
28
+ from .realtime import get_broker
29
+ from .realtime.router import router as realtime_router
30
+ from .settings import get_settings
31
+ from .storage.router import router as storage_router
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+
36
+ @asynccontextmanager
37
+ async def _lifespan(app: FastAPI) -> AsyncIterator[None]:
38
+ async with db.lifespan(app):
39
+ registry = get_function_registry()
40
+ try:
41
+ registry.discover()
42
+ except Exception:
43
+ logger.warning("functions: discover() failed at startup", exc_info=True)
44
+
45
+ settings = get_settings()
46
+ jwks_doc = jwks.write_current_jwks(settings.jwt_jwks_path)
47
+ kids = ", ".join(key["kid"] for key in jwks_doc["keys"])
48
+ logger.info("jwt: wrote JWKS path=%s kids=[%s]", settings.jwt_jwks_path, kids)
49
+
50
+ broker = get_broker() if settings.realtime_enabled else None
51
+ if broker is not None:
52
+ try:
53
+ await broker.start()
54
+ except Exception:
55
+ logger.exception("realtime: broker failed to start; continuing without realtime")
56
+ broker = None
57
+ app.state.broker = broker
58
+
59
+ worker = None
60
+ worker_task = None
61
+ if settings.jobs_enabled and settings.jobs_cron_backend == "pg_cron":
62
+ try:
63
+ from .jobs.cron import sync_pg_cron
64
+
65
+ async with db.as_service_role() as conn:
66
+ await sync_pg_cron(conn)
67
+ except Exception:
68
+ logger.warning("jobs: pg_cron sync failed at startup", exc_info=True)
69
+
70
+ if settings.jobs_enabled and settings.jobs_dev_inprocess:
71
+ from .jobs.worker import Worker
72
+
73
+ worker = Worker(settings)
74
+ try:
75
+ worker_task = asyncio.create_task(worker.start())
76
+ except Exception:
77
+ logger.warning("jobs: in-process worker failed to start", exc_info=True)
78
+
79
+ try:
80
+ yield
81
+ finally:
82
+ if worker is not None:
83
+ try:
84
+ await worker.stop()
85
+ except Exception:
86
+ logger.exception("jobs: worker failed to stop cleanly")
87
+ if worker_task is not None:
88
+ worker_task.cancel()
89
+ with contextlib.suppress(asyncio.CancelledError):
90
+ await worker_task
91
+ if broker is not None:
92
+ try:
93
+ await broker.stop()
94
+ except Exception:
95
+ logger.exception("realtime: broker failed to stop cleanly")
96
+
97
+
98
+ def create_app() -> FastAPI:
99
+ settings = get_settings()
100
+ configure_logging(settings.log_level, json_format=settings.log_json)
101
+
102
+ user = (
103
+ load_user_settings(settings.settings_module)
104
+ if settings.settings_module
105
+ else UserSettings()
106
+ )
107
+ load_extensions([*settings.extensions, *user.extensions])
108
+
109
+ app = FastAPI(
110
+ title="supython",
111
+ description="Lightweight Postgres-first BaaS for Python.",
112
+ version=__version__,
113
+ lifespan=_lifespan,
114
+ )
115
+
116
+ app.add_middleware(RequestLoggingMiddleware)
117
+ app.add_middleware(RequestIdMiddleware)
118
+
119
+ app.add_middleware(
120
+ CORSMiddleware,
121
+ allow_origins=[o.strip() for o in settings.cors_origins.split(",") if o.strip()],
122
+ allow_credentials=True,
123
+ allow_methods=["*"],
124
+ allow_headers=["*"],
125
+ )
126
+ app.add_middleware(BodySizeLimitMiddleware)
127
+ app.add_middleware(SecurityHeadersMiddleware)
128
+
129
+ for mw in user.extra_middleware:
130
+ app.add_middleware(mw)
131
+
132
+ app.include_router(auth_router)
133
+ app.include_router(storage_router)
134
+ app.include_router(functions_router)
135
+ if settings.realtime_enabled:
136
+ app.include_router(realtime_router)
137
+ if settings.jobs_enabled:
138
+ from .jobs.router import router as jobs_router
139
+
140
+ app.include_router(jobs_router)
141
+
142
+ app.include_router(health_router)
143
+ app.include_router(admin_api_router)
144
+ admin_spa.mount(app)
145
+
146
+ for rt in user.extra_routers:
147
+ app.include_router(rt)
148
+
149
+ def _make_hook_decorator(event: str) -> Callable[..., Any]:
150
+ def decorator(fn: Callable[..., Coroutine[Any, Any, None]] | None = None) -> Any:
151
+ return hook_on(event, fn)
152
+
153
+ return decorator
154
+
155
+ app.on_signup = _make_hook_decorator("signup")
156
+ app.on_login = _make_hook_decorator("login")
157
+ app.on_logout = _make_hook_decorator("logout")
158
+
159
+ return app
160
+
161
+
162
+ app = create_app()
@@ -0,0 +1,3 @@
1
+ from . import _email_job, router
2
+
3
+ __all__ = ["router"]
@@ -0,0 +1,11 @@
1
+ from ..jobs.decorators import job
2
+
3
+
4
+ @job("send_auth_email", max_attempts=5, backoff="exponential", backoff_base_s=10.0)
5
+ async def send_auth_email(ctx, payload):
6
+ await ctx.send_email(
7
+ to=payload["to"],
8
+ subject=payload["subject"],
9
+ text=payload["text"],
10
+ html=payload.get("html"),
11
+ )
@@ -0,0 +1,34 @@
1
+ """OAuth provider abstractions."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass, field
5
+ from typing import Any
6
+
7
+
8
+ @dataclass
9
+ class ProviderProfile:
10
+ """Normalised identity returned by any OAuth provider."""
11
+
12
+ provider_user_id: str
13
+ email: str
14
+ raw: dict[str, Any] = field(default_factory=dict)
15
+
16
+
17
+ class Provider(ABC):
18
+ """Abstract base that each concrete OAuth provider must implement."""
19
+
20
+ name: str
21
+
22
+ @abstractmethod
23
+ async def authorize_url(
24
+ self, state: str, redirect_uri: str, code_verifier: str | None = None
25
+ ) -> str:
26
+ """Return the full authorization URL to redirect the user to."""
27
+ ...
28
+
29
+ @abstractmethod
30
+ async def exchange(
31
+ self, code: str, redirect_uri: str, code_verifier: str | None = None
32
+ ) -> ProviderProfile:
33
+ """Exchange an authorization code for a normalised ProviderProfile."""
34
+ ...
@@ -0,0 +1,22 @@
1
+ """GitHub OAuth2 provider."""
2
+
3
+ from . import ProviderProfile
4
+ from .oauth import OAuthProvider
5
+
6
+
7
+ class GitHubProvider(OAuthProvider):
8
+ name = "github"
9
+ AUTHORIZE_URL = "https://github.com/login/oauth/authorize"
10
+ # GitHub requires Accept: application/json to return JSON (not form-encoded).
11
+ TOKEN_URL = "https://github.com/login/oauth/access_token"
12
+ USERINFO_URL = "https://api.github.com/user"
13
+ DEFAULT_SCOPE = "read:user user:email"
14
+
15
+ def _profile_from_userinfo(self, data: dict) -> ProviderProfile:
16
+ # GitHub users may have a null public email; the service layer will
17
+ # reject the profile if the email is empty.
18
+ return ProviderProfile(
19
+ provider_user_id=str(data["id"]),
20
+ email=data.get("email") or "",
21
+ raw=data,
22
+ )
@@ -0,0 +1,19 @@
1
+ """Google OAuth2 provider."""
2
+
3
+ from . import ProviderProfile
4
+ from .oauth import OAuthProvider
5
+
6
+
7
+ class GoogleProvider(OAuthProvider):
8
+ name = "google"
9
+ AUTHORIZE_URL = "https://accounts.google.com/o/oauth2/v2/auth"
10
+ TOKEN_URL = "https://oauth2.googleapis.com/token"
11
+ USERINFO_URL = "https://www.googleapis.com/oauth2/v3/userinfo"
12
+ DEFAULT_SCOPE = "openid email profile"
13
+
14
+ def _profile_from_userinfo(self, data: dict) -> ProviderProfile:
15
+ return ProviderProfile(
16
+ provider_user_id=data["sub"],
17
+ email=data.get("email", ""),
18
+ raw=data,
19
+ )
@@ -0,0 +1,56 @@
1
+ """Shared authlib-based OAuth2 provider base."""
2
+
3
+ from authlib.integrations.httpx_client import AsyncOAuth2Client
4
+
5
+ from . import Provider, ProviderProfile
6
+
7
+
8
+ class OAuthProvider(Provider):
9
+ """Concrete base for standard Authorization Code + PKCE providers."""
10
+
11
+ AUTHORIZE_URL: str
12
+ TOKEN_URL: str
13
+ USERINFO_URL: str
14
+ DEFAULT_SCOPE: str = "openid email profile"
15
+
16
+ def __init__(self, client_id: str, client_secret: str) -> None:
17
+ self._client_id = client_id
18
+ self._client_secret = client_secret
19
+
20
+ async def authorize_url(
21
+ self, state: str, redirect_uri: str, code_verifier: str | None = None
22
+ ) -> str:
23
+ async with AsyncOAuth2Client(
24
+ client_id=self._client_id,
25
+ client_secret=self._client_secret,
26
+ scope=self.DEFAULT_SCOPE,
27
+ code_challenge_method="S256",
28
+ ) as client:
29
+ url, _ = client.create_authorization_url(
30
+ self.AUTHORIZE_URL,
31
+ state=state,
32
+ redirect_uri=redirect_uri,
33
+ code_verifier=code_verifier,
34
+ )
35
+ return url
36
+
37
+ async def exchange(
38
+ self, code: str, redirect_uri: str, code_verifier: str | None = None
39
+ ) -> ProviderProfile:
40
+ async with AsyncOAuth2Client(
41
+ client_id=self._client_id,
42
+ client_secret=self._client_secret,
43
+ ) as client:
44
+ await client.fetch_token(
45
+ self.TOKEN_URL,
46
+ code=code,
47
+ redirect_uri=redirect_uri,
48
+ code_verifier=code_verifier,
49
+ )
50
+ resp = await client.get(self.USERINFO_URL)
51
+ resp.raise_for_status()
52
+ data: dict = resp.json()
53
+ return self._profile_from_userinfo(data)
54
+
55
+ def _profile_from_userinfo(self, data: dict) -> ProviderProfile:
56
+ raise NotImplementedError
@@ -0,0 +1,16 @@
1
+ """Provider registry: maps provider name → configured Provider instance."""
2
+
3
+ from ...settings import get_settings
4
+ from . import Provider
5
+ from .github import GitHubProvider
6
+ from .google import GoogleProvider
7
+
8
+
9
+ def get_provider(name: str) -> Provider:
10
+ """Return a configured provider by name, or raise KeyError if unavailable."""
11
+ s = get_settings()
12
+ if name == "google" and s.google_client_id and s.google_client_secret:
13
+ return GoogleProvider(s.google_client_id, s.google_client_secret)
14
+ if name == "github" and s.github_client_id and s.github_client_secret:
15
+ return GitHubProvider(s.github_client_id, s.github_client_secret)
16
+ raise KeyError(f"Unknown or unconfigured OAuth provider: {name!r}")
@@ -0,0 +1,39 @@
1
+ """Postgres-backed fixed-window rate limiter shared by auth endpoints."""
2
+
3
+ import logging
4
+ from dataclasses import dataclass
5
+ from datetime import UTC, datetime
6
+
7
+ import asyncpg
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class RateLimit:
14
+ limit: int
15
+ window_seconds: int
16
+
17
+
18
+ def _window_start(window_seconds: int, *, now: datetime | None = None) -> datetime:
19
+ now = now or datetime.now(UTC)
20
+ epoch = int(now.timestamp())
21
+ return datetime.fromtimestamp(epoch - (epoch % window_seconds), tz=UTC)
22
+
23
+
24
+ async def hit(
25
+ conn: asyncpg.Connection, *, bucket: str, rule: RateLimit
26
+ ) -> tuple[bool, int]:
27
+ """Increment the counter for ``bucket``. Returns ``(blocked, count)``."""
28
+ count = await conn.fetchval(
29
+ """
30
+ insert into auth.rate_limit_buckets as b (bucket, window_start, count)
31
+ values ($1, $2, 1)
32
+ on conflict (bucket, window_start) do update
33
+ set count = b.count + 1
34
+ returning b.count
35
+ """,
36
+ bucket,
37
+ _window_start(rule.window_seconds),
38
+ )
39
+ return count > rule.limit, count