supython 0.1.12__tar.gz → 0.1.13__tar.gz

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 (257) hide show
  1. {supython-0.1.12 → supython-0.1.13}/CHANGELOG.md +28 -0
  2. {supython-0.1.12 → supython-0.1.13}/PKG-INFO +1 -1
  3. {supython-0.1.12 → supython-0.1.13}/pyproject.toml +1 -1
  4. {supython-0.1.12 → supython-0.1.13}/src/supython/auth/router.py +48 -8
  5. {supython-0.1.12 → supython-0.1.13}/src/supython/auth/schemas.py +13 -0
  6. {supython-0.1.12 → supython-0.1.13}/src/supython/auth/service.py +88 -10
  7. supython-0.1.13/src/supython/migrations/0016_magic_link_redirect.sql +15 -0
  8. {supython-0.1.12 → supython-0.1.13}/src/supython/settings.py +13 -0
  9. {supython-0.1.12 → supython-0.1.13}/.gitignore +0 -0
  10. {supython-0.1.12 → supython-0.1.13}/LICENSE +0 -0
  11. {supython-0.1.12 → supython-0.1.13}/README.md +0 -0
  12. {supython-0.1.12 → supython-0.1.13}/SECURITY.md +0 -0
  13. {supython-0.1.12 → supython-0.1.13}/src/supython/__init__.py +0 -0
  14. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/__init__.py +0 -0
  15. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/api/__init__.py +0 -0
  16. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/api/auth.py +0 -0
  17. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/api/auth_templates.py +0 -0
  18. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/api/auth_users.py +0 -0
  19. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/api/db.py +0 -0
  20. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/api/functions.py +0 -0
  21. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/api/jobs.py +0 -0
  22. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/api/ops.py +0 -0
  23. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/api/realtime.py +0 -0
  24. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/api/service_auth.py +0 -0
  25. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/api/service_auth_templates.py +0 -0
  26. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/api/service_auth_users.py +0 -0
  27. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/api/service_db.py +0 -0
  28. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/api/service_functions.py +0 -0
  29. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/api/service_jobs.py +0 -0
  30. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/api/service_ops.py +0 -0
  31. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/api/service_realtime.py +0 -0
  32. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/api/service_storage.py +0 -0
  33. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/api/storage.py +0 -0
  34. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/api/system.py +0 -0
  35. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/audit.py +0 -0
  36. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/deps.py +0 -0
  37. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/errors.py +0 -0
  38. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/schemas.py +0 -0
  39. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/session.py +0 -0
  40. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/spa.py +0 -0
  41. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Alert-dluGVkos.js +0 -0
  42. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Alert-dluGVkos.js.map +0 -0
  43. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Audit-Njung3HI.js +0 -0
  44. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Audit-Njung3HI.js.map +0 -0
  45. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Backups-DzPlFgrm.js +0 -0
  46. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Backups-DzPlFgrm.js.map +0 -0
  47. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Buckets-ByacGkU1.js +0 -0
  48. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Buckets-ByacGkU1.js.map +0 -0
  49. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Channels-BoIuTtam.js +0 -0
  50. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Channels-BoIuTtam.js.map +0 -0
  51. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/ChevronRight-CtQH1EQ1.js +0 -0
  52. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/ChevronRight-CtQH1EQ1.js.map +0 -0
  53. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/CodeViewer-Bqy7-wvH.js +0 -0
  54. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/CodeViewer-Bqy7-wvH.js.map +0 -0
  55. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Crons-B67vc39F.js +0 -0
  56. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Crons-B67vc39F.js.map +0 -0
  57. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/DashboardView-CUTFVL6k.js +0 -0
  58. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/DashboardView-CUTFVL6k.js.map +0 -0
  59. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/DataTable-COAAWEft.js +0 -0
  60. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/DataTable-COAAWEft.js.map +0 -0
  61. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/DescriptionsItem-P8JUDaBs.js +0 -0
  62. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/DescriptionsItem-P8JUDaBs.js.map +0 -0
  63. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/DrawerContent-TpYTFgF1.js +0 -0
  64. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/DrawerContent-TpYTFgF1.js.map +0 -0
  65. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Empty-cr2r7e2u.js +0 -0
  66. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Empty-cr2r7e2u.js.map +0 -0
  67. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/EmptyState-DeDck-OL.js +0 -0
  68. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/EmptyState-DeDck-OL.js.map +0 -0
  69. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Grid-hFkp9F4P.js +0 -0
  70. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Grid-hFkp9F4P.js.map +0 -0
  71. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Input-DppYTq9C.js +0 -0
  72. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Input-DppYTq9C.js.map +0 -0
  73. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Invoke-DW3Nveeh.js +0 -0
  74. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Invoke-DW3Nveeh.js.map +0 -0
  75. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/JsonField-DibyJgun.js +0 -0
  76. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/JsonField-DibyJgun.js.map +0 -0
  77. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/LoginView-BjLyE3Ds.css +0 -0
  78. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/LoginView-CoOjECT_.js +0 -0
  79. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/LoginView-CoOjECT_.js.map +0 -0
  80. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Logs-D9WYrnIT.js +0 -0
  81. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Logs-D9WYrnIT.js.map +0 -0
  82. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Logs-DS1XPa0h.css +0 -0
  83. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Migrations-DOSC2ddQ.js +0 -0
  84. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Migrations-DOSC2ddQ.js.map +0 -0
  85. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/ObjectBrowser-_5w8vOX8.js +0 -0
  86. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/ObjectBrowser-_5w8vOX8.js.map +0 -0
  87. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Queue-CywZs6vI.js +0 -0
  88. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Queue-CywZs6vI.js.map +0 -0
  89. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/RefreshTokens-Ccjr53jg.js +0 -0
  90. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/RefreshTokens-Ccjr53jg.js.map +0 -0
  91. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/RlsEditor-BSlH9vSc.js +0 -0
  92. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/RlsEditor-BSlH9vSc.js.map +0 -0
  93. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Routes-BiLXE49D.js +0 -0
  94. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Routes-BiLXE49D.js.map +0 -0
  95. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Routes-C-ianIGD.css +0 -0
  96. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/SchemaBrowser-DKy2_KQi.css +0 -0
  97. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/SchemaBrowser-XFvFbtDB.js +0 -0
  98. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/SchemaBrowser-XFvFbtDB.js.map +0 -0
  99. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Select-DIzZyRZb.js +0 -0
  100. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Select-DIzZyRZb.js.map +0 -0
  101. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Space-n5-XcguU.js +0 -0
  102. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Space-n5-XcguU.js.map +0 -0
  103. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/SqlEditor-b8pTsILY.js +0 -0
  104. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/SqlEditor-b8pTsILY.js.map +0 -0
  105. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/SqlWorkspace-BUS7IntH.js +0 -0
  106. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/SqlWorkspace-BUS7IntH.js.map +0 -0
  107. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/TableData-CQIagLKn.js +0 -0
  108. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/TableData-CQIagLKn.js.map +0 -0
  109. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Tag-D1fOKpTH.js +0 -0
  110. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Tag-D1fOKpTH.js.map +0 -0
  111. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Templates-BS-ugkdq.js +0 -0
  112. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Templates-BS-ugkdq.js.map +0 -0
  113. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Thing-CEAniuMg.js +0 -0
  114. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Thing-CEAniuMg.js.map +0 -0
  115. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Users-wzwajhlh.js +0 -0
  116. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Users-wzwajhlh.js.map +0 -0
  117. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/_plugin-vue_export-helper-DGA9ry_j.js +0 -0
  118. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/dist-VXIJLCYq.js +0 -0
  119. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/dist-VXIJLCYq.js.map +0 -0
  120. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/format-length-CGCY1rMh.js +0 -0
  121. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/format-length-CGCY1rMh.js.map +0 -0
  122. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/get-Ca6unauB.js +0 -0
  123. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/get-Ca6unauB.js.map +0 -0
  124. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/index-CeE6v959.js +0 -0
  125. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/index-CeE6v959.js.map +0 -0
  126. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/pinia-COXwfrOX.js +0 -0
  127. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/pinia-COXwfrOX.js.map +0 -0
  128. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/resources-Bt6thQCD.js +0 -0
  129. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/resources-Bt6thQCD.js.map +0 -0
  130. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/use-locale-mtgM0a3a.js +0 -0
  131. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/use-locale-mtgM0a3a.js.map +0 -0
  132. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/use-merged-state-BvhkaHNX.js +0 -0
  133. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/use-merged-state-BvhkaHNX.js.map +0 -0
  134. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/useConfirm-tMjvBFXR.js +0 -0
  135. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/useConfirm-tMjvBFXR.js.map +0 -0
  136. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/useResource-C_rJCY8C.js +0 -0
  137. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/useResource-C_rJCY8C.js.map +0 -0
  138. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/useTable-CnZc5zhi.js +0 -0
  139. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/useTable-CnZc5zhi.js.map +0 -0
  140. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/useTable-Dg0XlRlq.css +0 -0
  141. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/useToast-DsZKx0IX.js +0 -0
  142. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/useToast-DsZKx0IX.js.map +0 -0
  143. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/utils-sbXoq7Ir.js +0 -0
  144. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/utils-sbXoq7Ir.js.map +0 -0
  145. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/favicon.svg +0 -0
  146. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/icons.svg +0 -0
  147. {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/index.html +0 -0
  148. {supython-0.1.12 → supython-0.1.13}/src/supython/app.py +0 -0
  149. {supython-0.1.12 → supython-0.1.13}/src/supython/auth/__init__.py +0 -0
  150. {supython-0.1.12 → supython-0.1.13}/src/supython/auth/_email_job.py +0 -0
  151. {supython-0.1.12 → supython-0.1.13}/src/supython/auth/claims.py +0 -0
  152. {supython-0.1.12 → supython-0.1.13}/src/supython/auth/deps.py +0 -0
  153. {supython-0.1.12 → supython-0.1.13}/src/supython/auth/providers/__init__.py +0 -0
  154. {supython-0.1.12 → supython-0.1.13}/src/supython/auth/providers/github.py +0 -0
  155. {supython-0.1.12 → supython-0.1.13}/src/supython/auth/providers/google.py +0 -0
  156. {supython-0.1.12 → supython-0.1.13}/src/supython/auth/providers/oauth.py +0 -0
  157. {supython-0.1.12 → supython-0.1.13}/src/supython/auth/providers/registry.py +0 -0
  158. {supython-0.1.12 → supython-0.1.13}/src/supython/auth/ratelimit.py +0 -0
  159. {supython-0.1.12 → supython-0.1.13}/src/supython/backups/__init__.py +0 -0
  160. {supython-0.1.12 → supython-0.1.13}/src/supython/backups/_backup_job.py +0 -0
  161. {supython-0.1.12 → supython-0.1.13}/src/supython/backups/schemas.py +0 -0
  162. {supython-0.1.12 → supython-0.1.13}/src/supython/backups/service.py +0 -0
  163. {supython-0.1.12 → supython-0.1.13}/src/supython/body_size.py +0 -0
  164. {supython-0.1.12 → supython-0.1.13}/src/supython/cli.py +0 -0
  165. {supython-0.1.12 → supython-0.1.13}/src/supython/client/__init__.py +0 -0
  166. {supython-0.1.12 → supython-0.1.13}/src/supython/client/_auth.py +0 -0
  167. {supython-0.1.12 → supython-0.1.13}/src/supython/client/_client.py +0 -0
  168. {supython-0.1.12 → supython-0.1.13}/src/supython/client/_config.py +0 -0
  169. {supython-0.1.12 → supython-0.1.13}/src/supython/client/_functions.py +0 -0
  170. {supython-0.1.12 → supython-0.1.13}/src/supython/client/_storage.py +0 -0
  171. {supython-0.1.12 → supython-0.1.13}/src/supython/client/py.typed +0 -0
  172. {supython-0.1.12 → supython-0.1.13}/src/supython/db.py +0 -0
  173. {supython-0.1.12 → supython-0.1.13}/src/supython/db_admin.py +0 -0
  174. {supython-0.1.12 → supython-0.1.13}/src/supython/extensions.py +0 -0
  175. {supython-0.1.12 → supython-0.1.13}/src/supython/functions/__init__.py +0 -0
  176. {supython-0.1.12 → supython-0.1.13}/src/supython/functions/context.py +0 -0
  177. {supython-0.1.12 → supython-0.1.13}/src/supython/functions/loader.py +0 -0
  178. {supython-0.1.12 → supython-0.1.13}/src/supython/functions/router.py +0 -0
  179. {supython-0.1.12 → supython-0.1.13}/src/supython/functions/schemas.py +0 -0
  180. {supython-0.1.12 → supython-0.1.13}/src/supython/gen/__init__.py +0 -0
  181. {supython-0.1.12 → supython-0.1.13}/src/supython/gen/_introspect.py +0 -0
  182. {supython-0.1.12 → supython-0.1.13}/src/supython/gen/types_py.py +0 -0
  183. {supython-0.1.12 → supython-0.1.13}/src/supython/gen/types_ts.py +0 -0
  184. {supython-0.1.12 → supython-0.1.13}/src/supython/health.py +0 -0
  185. {supython-0.1.12 → supython-0.1.13}/src/supython/hooks.py +0 -0
  186. {supython-0.1.12 → supython-0.1.13}/src/supython/jobs/__init__.py +0 -0
  187. {supython-0.1.12 → supython-0.1.13}/src/supython/jobs/backends.py +0 -0
  188. {supython-0.1.12 → supython-0.1.13}/src/supython/jobs/context.py +0 -0
  189. {supython-0.1.12 → supython-0.1.13}/src/supython/jobs/cron.py +0 -0
  190. {supython-0.1.12 → supython-0.1.13}/src/supython/jobs/cron_inproc.py +0 -0
  191. {supython-0.1.12 → supython-0.1.13}/src/supython/jobs/decorators.py +0 -0
  192. {supython-0.1.12 → supython-0.1.13}/src/supython/jobs/registry.py +0 -0
  193. {supython-0.1.12 → supython-0.1.13}/src/supython/jobs/router.py +0 -0
  194. {supython-0.1.12 → supython-0.1.13}/src/supython/jobs/schemas.py +0 -0
  195. {supython-0.1.12 → supython-0.1.13}/src/supython/jobs/service.py +0 -0
  196. {supython-0.1.12 → supython-0.1.13}/src/supython/jobs/worker.py +0 -0
  197. {supython-0.1.12 → supython-0.1.13}/src/supython/jwks.py +0 -0
  198. {supython-0.1.12 → supython-0.1.13}/src/supython/keyset.py +0 -0
  199. {supython-0.1.12 → supython-0.1.13}/src/supython/logging_config.py +0 -0
  200. {supython-0.1.12 → supython-0.1.13}/src/supython/mail.py +0 -0
  201. {supython-0.1.12 → supython-0.1.13}/src/supython/mailer.py +0 -0
  202. {supython-0.1.12 → supython-0.1.13}/src/supython/migrate.py +0 -0
  203. {supython-0.1.12 → supython-0.1.13}/src/supython/migrations/0001_extensions_and_roles.sql +0 -0
  204. {supython-0.1.12 → supython-0.1.13}/src/supython/migrations/0002_auth_schema.sql +0 -0
  205. {supython-0.1.12 → supython-0.1.13}/src/supython/migrations/0004_auth_v0_2.sql +0 -0
  206. {supython-0.1.12 → supython-0.1.13}/src/supython/migrations/0005_storage_schema.sql +0 -0
  207. {supython-0.1.12 → supython-0.1.13}/src/supython/migrations/0006_realtime_schema.sql +0 -0
  208. {supython-0.1.12 → supython-0.1.13}/src/supython/migrations/0007_jobs_schema.sql +0 -0
  209. {supython-0.1.12 → supython-0.1.13}/src/supython/migrations/0008_jobs_last_error.sql +0 -0
  210. {supython-0.1.12 → supython-0.1.13}/src/supython/migrations/0009_auth_rate_limits.sql +0 -0
  211. {supython-0.1.12 → supython-0.1.13}/src/supython/migrations/0010_worker_heartbeat.sql +0 -0
  212. {supython-0.1.12 → supython-0.1.13}/src/supython/migrations/0011_admin_schema.sql +0 -0
  213. {supython-0.1.12 → supython-0.1.13}/src/supython/migrations/0012_auth_banned_until.sql +0 -0
  214. {supython-0.1.12 → supython-0.1.13}/src/supython/migrations/0013_email_templates.sql +0 -0
  215. {supython-0.1.12 → supython-0.1.13}/src/supython/migrations/0014_realtime_payload_warning.sql +0 -0
  216. {supython-0.1.12 → supython-0.1.13}/src/supython/migrations/0015_backups_schema.sql +0 -0
  217. {supython-0.1.12 → supython-0.1.13}/src/supython/passwords.py +0 -0
  218. {supython-0.1.12 → supython-0.1.13}/src/supython/realtime/__init__.py +0 -0
  219. {supython-0.1.12 → supython-0.1.13}/src/supython/realtime/broker.py +0 -0
  220. {supython-0.1.12 → supython-0.1.13}/src/supython/realtime/protocol.py +0 -0
  221. {supython-0.1.12 → supython-0.1.13}/src/supython/realtime/router.py +0 -0
  222. {supython-0.1.12 → supython-0.1.13}/src/supython/realtime/schemas.py +0 -0
  223. {supython-0.1.12 → supython-0.1.13}/src/supython/realtime/service.py +0 -0
  224. {supython-0.1.12 → supython-0.1.13}/src/supython/realtime/topics.py +0 -0
  225. {supython-0.1.12 → supython-0.1.13}/src/supython/realtime/websocket.py +0 -0
  226. {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/__init__.py +0 -0
  227. {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/init_project.py +0 -0
  228. {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/templates/Caddyfile.tmpl +0 -0
  229. {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/templates/README.md.tmpl +0 -0
  230. {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/templates/apps_hooks_welcome.py.tmpl +0 -0
  231. {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/templates/apps_jobs_example.py.tmpl +0 -0
  232. {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/templates/asgi.py.tmpl +0 -0
  233. {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/templates/docker-compose.prod.yml.tmpl +0 -0
  234. {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/templates/docker-compose.yml.tmpl +0 -0
  235. {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/templates/docker_postgres_Dockerfile.tmpl +0 -0
  236. {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/templates/docker_postgres_postgresql.conf.tmpl +0 -0
  237. {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/templates/env.example.tmpl +0 -0
  238. {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/templates/functions_README.md.tmpl +0 -0
  239. {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/templates/gitignore.tmpl +0 -0
  240. {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/templates/manage.py.tmpl +0 -0
  241. {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/templates/package_hooks_init.py.tmpl +0 -0
  242. {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/templates/package_init.py.tmpl +0 -0
  243. {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/templates/package_jobs_init.py.tmpl +0 -0
  244. {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/templates/pyproject.toml.tmpl +0 -0
  245. {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/templates/settings.py.tmpl +0 -0
  246. {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/templates/static/0001_create_todos.sql +0 -0
  247. {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/templates/static/functions_hello.py +0 -0
  248. {supython-0.1.12 → supython-0.1.13}/src/supython/secretset.py +0 -0
  249. {supython-0.1.12 → supython-0.1.13}/src/supython/security_headers.py +0 -0
  250. {supython-0.1.12 → supython-0.1.13}/src/supython/settings_module.py +0 -0
  251. {supython-0.1.12 → supython-0.1.13}/src/supython/storage/__init__.py +0 -0
  252. {supython-0.1.12 → supython-0.1.13}/src/supython/storage/backends.py +0 -0
  253. {supython-0.1.12 → supython-0.1.13}/src/supython/storage/router.py +0 -0
  254. {supython-0.1.12 → supython-0.1.13}/src/supython/storage/schemas.py +0 -0
  255. {supython-0.1.12 → supython-0.1.13}/src/supython/storage/service.py +0 -0
  256. {supython-0.1.12 → supython-0.1.13}/src/supython/storage/signing.py +0 -0
  257. {supython-0.1.12 → supython-0.1.13}/src/supython/tokens.py +0 -0
@@ -30,6 +30,33 @@ Each entry links the relevant `PROJECT.md` section and decision-log row
30
30
 
31
31
  ---
32
32
 
33
+ ## [0.1.13] — 2026-07-16
34
+
35
+ ### Added
36
+ - `POST /auth/v1/magiclink` accepts an optional `redirect_url`: when its origin
37
+ is in the new `MAGIC_LINK_REDIRECT_ALLOWLIST` setting, `GET
38
+ /auth/v1/magiclink/verify` 302-redirects the browser to it with the token
39
+ pair in the fragment (the same shape as the OAuth callback), instead of
40
+ returning JSON. Omitting `redirect_url` keeps the existing JSON response.
41
+ Lets a consumer send a magic link straight to its own SPA route (e.g. an
42
+ invite-accept page) rather than the API's bare verify endpoint.
43
+ - `POST /auth/v1/magiclink` accepts an optional `ttl` (seconds), clamped to
44
+ `[60, MAGIC_LINK_MAX_TTL]` (new setting, default 7 days). Lets a single
45
+ caller mint a longer-lived link (e.g. a multi-day operator invite) without
46
+ moving `MAGIC_LINK_TOKEN_TTL`, which every other magic-link request still
47
+ uses unchanged.
48
+ - Migration `0016_magic_link_redirect.sql`: nullable `auth.one_time_tokens.redirect_url`.
49
+
50
+ ### Security
51
+ - `redirect_url` is validated against an explicit origin allowlist before any
52
+ email is sent (empty allowlist ⇒ every redirect is rejected, fail-closed);
53
+ a non-allowlisted origin, embedded credentials (`user:pass@host`), or a
54
+ non-http(s) scheme all reject the request with `400 invalid_redirect`. The
55
+ target is stored server-side on the one-time token, not echoed through the
56
+ emailed link, so an intercepted link can't be repointed at a different origin.
57
+
58
+ ---
59
+
33
60
  ## [0.1.12] — 2026-07-01
34
61
 
35
62
  ### Fixed
@@ -318,6 +345,7 @@ v0.1–v0.7 plus a v1.1.x admin track; see §19 decision log
318
345
  ---
319
346
 
320
347
 
348
+ [0.1.13]: https://github.com/Tkeby/supython/releases/tag/v0.1.13
321
349
  [0.1.12]: https://github.com/Tkeby/supython/releases/tag/v0.1.12
322
350
  [0.1.11]: https://github.com/Tkeby/supython/releases/tag/v0.1.11
323
351
  [0.1.10]: https://github.com/Tkeby/supython/releases/tag/v0.1.10
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: supython
3
- Version: 0.1.12
3
+ Version: 0.1.13
4
4
  Summary: A lightweight Postgres-first BaaS framework for Python
5
5
  Project-URL: Homepage, https://github.com/Tkeby/supython
6
6
  Project-URL: Repository, https://github.com/Tkeby/supython
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "supython"
7
- version = "0.1.12"
7
+ version = "0.1.13"
8
8
  description = "A lightweight Postgres-first BaaS framework for Python"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -2,8 +2,18 @@ from typing import Annotated
2
2
  from uuid import UUID
3
3
 
4
4
  import jwt
5
- from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request, status
6
- from fastapi.responses import RedirectResponse
5
+ from fastapi import (
6
+ APIRouter,
7
+ Depends,
8
+ Header,
9
+ HTTPException,
10
+ Query,
11
+ Request,
12
+ Response,
13
+ status,
14
+ )
15
+ from fastapi.encoders import jsonable_encoder
16
+ from fastapi.responses import JSONResponse, RedirectResponse
7
17
 
8
18
  from .. import db, tokens
9
19
  from ..settings import get_settings
@@ -198,24 +208,54 @@ async def verify_recover(payload: RecoverVerifyRequest, request: Request) -> Tok
198
208
 
199
209
  @router.post("/magiclink", status_code=202, dependencies=[Depends(_rl_magiclink)])
200
210
  async def request_magic_link(payload: MagicLinkRequest) -> None:
201
- async with db.acquire() as conn:
202
- await service.request_magic_link(conn, payload.email)
211
+ try:
212
+ async with db.acquire() as conn:
213
+ await service.request_magic_link(
214
+ conn,
215
+ payload.email,
216
+ redirect_url=payload.redirect_url,
217
+ ttl=payload.ttl,
218
+ )
219
+ except service.AuthError as exc:
220
+ # Currently only invalid_redirect (400) — a bad email is filtered by
221
+ # EmailStr at the schema layer before this handler ever runs.
222
+ raise _auth_error(exc) from exc
203
223
 
204
224
 
205
225
  @router.get(
206
226
  "/magiclink/verify",
207
- response_model=TokenResponse,
208
227
  dependencies=[Depends(_rl_magiclink)],
209
228
  )
210
229
  async def verify_magic_link(
211
230
  token: Annotated[str, Query(description="Raw magic-link token from the email")],
212
- ) -> TokenResponse:
231
+ ) -> Response:
232
+ """Verify a magic-link token.
233
+
234
+ Legacy/JSON callers (no ``redirect_url`` at request time) get the same
235
+ ``TokenResponse`` body as always. A caller that requested a redirect gets a
236
+ 302 to that URL with the token pair in the fragment — the same shape as the
237
+ OAuth callback (`oauth_callback` below) — so a browser landing here from an
238
+ emailed link ends up on the SPA's own page with a session, not looking at
239
+ raw JSON.
240
+ """
213
241
  try:
214
242
  async with db.acquire() as conn:
215
- result = await service.verify_magic_link(conn, token)
243
+ user, access, refresh, ttl, redirect_url = await service.verify_magic_link(
244
+ conn, token
245
+ )
216
246
  except service.AuthError as exc:
217
247
  raise _auth_error(exc) from exc
218
- return _to_token_response(*result)
248
+ if redirect_url:
249
+ fragment = (
250
+ f"access_token={access}"
251
+ f"&refresh_token={refresh}"
252
+ f"&expires_in={ttl}"
253
+ f"&token_type=bearer"
254
+ )
255
+ return RedirectResponse(f"{redirect_url}#{fragment}", status_code=302)
256
+ return JSONResponse(
257
+ jsonable_encoder(_to_token_response(user, access, refresh, ttl))
258
+ )
219
259
 
220
260
 
221
261
  @router.post("/otp", status_code=202, dependencies=[Depends(_rl_otp)])
@@ -17,6 +17,8 @@ MAX_PASSWORD_LEN = 128
17
17
  # chars in practice). 4096 keeps a generous head-room for any future
18
18
  # encoding while still rejecting "1 MB token" abuse.
19
19
  MAX_TOKEN_LEN = 4096
20
+ # Redirect URLs are ordinary web URLs; 2048 is the de-facto browser cap.
21
+ MAX_REDIRECT_URL_LEN = 2048
20
22
 
21
23
  EmailField = Annotated[EmailStr, Field(max_length=MAX_EMAIL_LEN)]
22
24
  PasswordField = Annotated[
@@ -68,6 +70,17 @@ class RecoverVerifyRequest(BaseModel):
68
70
 
69
71
  class MagicLinkRequest(BaseModel):
70
72
  email: EmailField
73
+ # Where /auth/v1/magiclink/verify should send the browser after issuing
74
+ # tokens: a 302 to this URL with the token pair in the fragment (as OAuth
75
+ # does), instead of a JSON response. Must match MAGIC_LINK_REDIRECT_
76
+ # ALLOWLIST or the request is rejected 400. Omit for the JSON flow.
77
+ redirect_url: Annotated[
78
+ str | None, Field(default=None, max_length=MAX_REDIRECT_URL_LEN)
79
+ ] = None
80
+ # Per-request token lifetime in seconds, clamped to [60, MAGIC_LINK_MAX_TTL].
81
+ # Omit to use MAGIC_LINK_TOKEN_TTL. Lets a caller mint a longer-lived link
82
+ # (e.g. an operator invite) without changing the global default.
83
+ ttl: Annotated[int | None, Field(default=None, ge=1)] = None
71
84
 
72
85
 
73
86
  class OtpRequest(BaseModel):
@@ -6,6 +6,7 @@ import logging
6
6
  import secrets
7
7
  from datetime import UTC, datetime, timedelta
8
8
  from typing import Any
9
+ from urllib.parse import urlsplit
9
10
  from uuid import UUID
10
11
 
11
12
  import asyncpg
@@ -82,19 +83,26 @@ async def _store_one_time_token(
82
83
  user_id: UUID,
83
84
  token_type: str,
84
85
  ttl_seconds: int,
86
+ *,
87
+ redirect_url: str | None = None,
85
88
  ) -> str:
86
- """Generate, sha256-hash, and store a one-time token. Returns the raw token."""
89
+ """Generate, sha256-hash, and store a one-time token. Returns the raw token.
90
+
91
+ ``redirect_url`` is stored only for magic-link tokens (recover/otp pass
92
+ None ⇒ NULL); verify reads it back to decide between a 302 and JSON.
93
+ """
87
94
  raw = secrets.token_urlsafe(32)
88
95
  expires_at = datetime.now(UTC) + timedelta(seconds=ttl_seconds)
89
96
  await conn.execute(
90
97
  """
91
- insert into auth.one_time_tokens (user_id, type, token_hash, expires_at)
92
- values ($1, $2, $3, $4)
98
+ insert into auth.one_time_tokens (user_id, type, token_hash, expires_at, redirect_url)
99
+ values ($1, $2, $3, $4, $5)
93
100
  """,
94
101
  user_id,
95
102
  token_type,
96
103
  _sha256(raw),
97
104
  expires_at,
105
+ redirect_url,
98
106
  )
99
107
  return raw
100
108
 
@@ -110,7 +118,7 @@ async def _verify_one_time_token(
110
118
  if email is not None:
111
119
  row = await conn.fetchrow(
112
120
  """
113
- select ott.id as ott_id, u.id, u.email, u.created_at
121
+ select ott.id as ott_id, ott.redirect_url, u.id, u.email, u.created_at
114
122
  from auth.one_time_tokens ott
115
123
  join auth.users u on u.id = ott.user_id
116
124
  where ott.token_hash = $1
@@ -126,7 +134,7 @@ async def _verify_one_time_token(
126
134
  else:
127
135
  row = await conn.fetchrow(
128
136
  """
129
- select ott.id as ott_id, u.id, u.email, u.created_at
137
+ select ott.id as ott_id, ott.redirect_url, u.id, u.email, u.created_at
130
138
  from auth.one_time_tokens ott
131
139
  join auth.users u on u.id = ott.user_id
132
140
  where ott.token_hash = $1
@@ -354,17 +362,85 @@ async def verify_recover(
354
362
  return user, access, refresh, ttl
355
363
 
356
364
 
365
+ def _url_origin(url: str) -> str | None:
366
+ """Return the ``scheme://host[:port]`` origin of an absolute http(s) URL.
367
+
368
+ Returns None for anything that isn't a well-formed absolute http(s) URL, or
369
+ that carries embedded credentials (``user:pass@host``) — those have no place
370
+ in a redirect target and are a classic obfuscation vector. Origins are
371
+ lower-cased so comparison is case-insensitive on scheme/host.
372
+ """
373
+ try:
374
+ parts = urlsplit(url)
375
+ except ValueError:
376
+ return None
377
+ if parts.scheme not in ("http", "https") or not parts.netloc or "@" in parts.netloc:
378
+ return None
379
+ return f"{parts.scheme}://{parts.netloc}".lower()
380
+
381
+
382
+ def validate_magic_link_redirect(redirect_url: str, allowlist_csv: str) -> str:
383
+ """Return ``redirect_url`` when its origin is allowlisted, else raise.
384
+
385
+ ``allowlist_csv`` is the comma-separated ``MAGIC_LINK_REDIRECT_ALLOWLIST``
386
+ setting. An empty allowlist rejects every redirect (the feature is off and
387
+ fails closed). Matching is by origin only, so any path/query/fragment under
388
+ an allowlisted origin is accepted. Raises ``AuthError('invalid_redirect')``
389
+ (400) on any miss — the caller turns that into an HTTP 400.
390
+ """
391
+ origin = _url_origin(redirect_url)
392
+ if origin is None:
393
+ raise AuthError(
394
+ "invalid_redirect", "redirect_url must be an absolute http(s) URL", 400
395
+ )
396
+ allowed = {
397
+ o for entry in allowlist_csv.split(",") if (o := _url_origin(entry.strip()))
398
+ }
399
+ if origin not in allowed:
400
+ raise AuthError(
401
+ "invalid_redirect", "redirect_url origin is not allowlisted", 400
402
+ )
403
+ return redirect_url
404
+
405
+
406
+ def _clamp_magic_link_ttl(ttl: int | None, settings: Any) -> int:
407
+ """Resolve the effective magic-link TTL in seconds.
408
+
409
+ ``None`` ⇒ the ``magic_link_token_ttl`` default. A supplied value is clamped
410
+ to ``[60, magic_link_max_ttl]`` so a caller can neither mint a link that
411
+ expires too fast to click nor one that outlives the configured ceiling.
412
+ """
413
+ if ttl is None:
414
+ return settings.magic_link_token_ttl
415
+ return max(60, min(ttl, settings.magic_link_max_ttl))
416
+
417
+
357
418
  async def request_magic_link(
358
- conn: asyncpg.Connection, email: str
419
+ conn: asyncpg.Connection,
420
+ email: str,
421
+ *,
422
+ redirect_url: str | None = None,
423
+ ttl: int | None = None,
359
424
  ) -> None:
425
+ s = get_settings()
426
+ # Validate the redirect before the user lookup: a bad redirect is a client
427
+ # error regardless of whether the email exists, and checking it here keeps
428
+ # the email-enumeration silence below intact (unknown emails still 202).
429
+ if redirect_url is not None:
430
+ validate_magic_link_redirect(redirect_url, s.magic_link_redirect_allowlist)
360
431
  row = await conn.fetchrow(
361
432
  "select id from auth.users where email = $1", email
362
433
  )
363
434
  if not row:
364
435
  return
365
- s = get_settings()
366
436
  async with conn.transaction():
367
- raw = await _store_one_time_token(conn, row["id"], "magic_link", s.magic_link_token_ttl)
437
+ raw = await _store_one_time_token(
438
+ conn,
439
+ row["id"],
440
+ "magic_link",
441
+ _clamp_magic_link_ttl(ttl, s),
442
+ redirect_url=redirect_url,
443
+ )
368
444
  verify_url = f"{s.site_url}/auth/v1/magiclink/verify?token={raw}"
369
445
  await mail.dispatch(
370
446
  conn,
@@ -379,7 +455,7 @@ async def request_magic_link(
379
455
 
380
456
  async def verify_magic_link(
381
457
  conn: asyncpg.Connection, token: str
382
- ) -> tuple[UserResponse, str, str, int]:
458
+ ) -> tuple[UserResponse, str, str, int, str | None]:
383
459
  row = await _verify_one_time_token(conn, token, "magic_link")
384
460
  user = _row_to_user(row)
385
461
  async with conn.transaction():
@@ -388,7 +464,9 @@ async def verify_magic_link(
388
464
  row["ott_id"],
389
465
  )
390
466
  access, refresh, ttl = await _issue_pair(conn, user)
391
- return user, access, refresh, ttl
467
+ # redirect_url is None for legacy/JSON callers; the router 302-redirects
468
+ # (OAuth-style, tokens in the fragment) only when it is set.
469
+ return user, access, refresh, ttl, row["redirect_url"]
392
470
 
393
471
 
394
472
  async def request_otp(
@@ -0,0 +1,15 @@
1
+ -- Magic-link redirect target.
2
+ --
3
+ -- When a POST /auth/v1/magiclink request carries a redirect_url (validated
4
+ -- against MAGIC_LINK_REDIRECT_ALLOWLIST at request time), it is stored on the
5
+ -- one-time token here. GET /auth/v1/magiclink/verify then 302-redirects the
6
+ -- browser to that SPA URL with the token pair in the fragment — mirroring the
7
+ -- OAuth callback — instead of returning JSON. Storing the target server-side
8
+ -- (rather than echoing it in the emailed link) means an attacker who intercepts
9
+ -- the link cannot repoint it at a token-stealing origin.
10
+ --
11
+ -- Null ⇒ legacy behaviour: verify returns a JSON TokenResponse. Backfill-safe
12
+ -- (nullable, no default), so existing rows keep working.
13
+
14
+ alter table auth.one_time_tokens
15
+ add column if not exists redirect_url text;
@@ -82,6 +82,19 @@ class Settings(BaseSettings):
82
82
  recover_token_ttl: int = 3600
83
83
  magic_link_token_ttl: int = 15 * 60
84
84
  otp_token_ttl: int = 10 * 60
85
+
86
+ # Ceiling for a per-request magic-link lifetime. A POST /auth/v1/magiclink
87
+ # may pass its own `ttl` (e.g. a multi-day operator invite) without moving
88
+ # the magic_link_token_ttl default that ordinary sign-in links use; the
89
+ # requested value is clamped to [60, magic_link_max_ttl].
90
+ magic_link_max_ttl: int = 60 * 60 * 24 * 7 # 7 days
91
+
92
+ # Comma-separated origins (scheme://host[:port]) a magic-link `redirect_url`
93
+ # may target. Empty ⇒ redirects are disabled and verify keeps returning
94
+ # JSON. A redirect_url whose origin is not listed is rejected 400 at request
95
+ # time, so an attacker can't aim a victim's emailed link at a token-stealing
96
+ # site. Example: MAGIC_LINK_REDIRECT_ALLOWLIST=https://app.example.com,http://localhost:5173
97
+ magic_link_redirect_allowlist: str = ""
85
98
  auth_rate_limit_enabled: bool = True
86
99
  auth_rate_limit_window_seconds: int = 60
87
100
  auth_rate_limit_token_per_window: int = 10
File without changes
File without changes
File without changes
File without changes