supython 0.1.8__tar.gz → 0.1.10__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 (253) hide show
  1. {supython-0.1.8 → supython-0.1.10}/CHANGELOG.md +58 -0
  2. {supython-0.1.8 → supython-0.1.10}/PKG-INFO +11 -11
  3. {supython-0.1.8 → supython-0.1.10}/README.md +9 -10
  4. {supython-0.1.8 → supython-0.1.10}/pyproject.toml +2 -1
  5. {supython-0.1.8 → supython-0.1.10}/src/supython/app.py +2 -1
  6. {supython-0.1.8 → supython-0.1.10}/src/supython/cli.py +34 -3
  7. {supython-0.1.8 → supython-0.1.10}/src/supython/jobs/cron.py +31 -4
  8. {supython-0.1.8 → supython-0.1.10}/src/supython/jobs/decorators.py +37 -2
  9. {supython-0.1.8 → supython-0.1.10}/src/supython/settings.py +31 -0
  10. {supython-0.1.8 → supython-0.1.10}/.gitignore +0 -0
  11. {supython-0.1.8 → supython-0.1.10}/LICENSE +0 -0
  12. {supython-0.1.8 → supython-0.1.10}/SECURITY.md +0 -0
  13. {supython-0.1.8 → supython-0.1.10}/src/supython/__init__.py +0 -0
  14. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/__init__.py +0 -0
  15. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/api/__init__.py +0 -0
  16. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/api/auth.py +0 -0
  17. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/api/auth_templates.py +0 -0
  18. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/api/auth_users.py +0 -0
  19. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/api/db.py +0 -0
  20. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/api/functions.py +0 -0
  21. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/api/jobs.py +0 -0
  22. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/api/ops.py +0 -0
  23. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/api/realtime.py +0 -0
  24. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/api/service_auth.py +0 -0
  25. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/api/service_auth_templates.py +0 -0
  26. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/api/service_auth_users.py +0 -0
  27. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/api/service_db.py +0 -0
  28. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/api/service_functions.py +0 -0
  29. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/api/service_jobs.py +0 -0
  30. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/api/service_ops.py +0 -0
  31. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/api/service_realtime.py +0 -0
  32. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/api/service_storage.py +0 -0
  33. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/api/storage.py +0 -0
  34. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/api/system.py +0 -0
  35. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/audit.py +0 -0
  36. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/deps.py +0 -0
  37. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/errors.py +0 -0
  38. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/schemas.py +0 -0
  39. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/session.py +0 -0
  40. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/spa.py +0 -0
  41. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Alert-dluGVkos.js +0 -0
  42. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Alert-dluGVkos.js.map +0 -0
  43. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Audit-Njung3HI.js +0 -0
  44. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Audit-Njung3HI.js.map +0 -0
  45. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Backups-DzPlFgrm.js +0 -0
  46. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Backups-DzPlFgrm.js.map +0 -0
  47. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Buckets-ByacGkU1.js +0 -0
  48. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Buckets-ByacGkU1.js.map +0 -0
  49. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Channels-BoIuTtam.js +0 -0
  50. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Channels-BoIuTtam.js.map +0 -0
  51. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/ChevronRight-CtQH1EQ1.js +0 -0
  52. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/ChevronRight-CtQH1EQ1.js.map +0 -0
  53. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/CodeViewer-Bqy7-wvH.js +0 -0
  54. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/CodeViewer-Bqy7-wvH.js.map +0 -0
  55. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Crons-B67vc39F.js +0 -0
  56. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Crons-B67vc39F.js.map +0 -0
  57. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/DashboardView-CUTFVL6k.js +0 -0
  58. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/DashboardView-CUTFVL6k.js.map +0 -0
  59. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/DataTable-COAAWEft.js +0 -0
  60. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/DataTable-COAAWEft.js.map +0 -0
  61. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/DescriptionsItem-P8JUDaBs.js +0 -0
  62. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/DescriptionsItem-P8JUDaBs.js.map +0 -0
  63. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/DrawerContent-TpYTFgF1.js +0 -0
  64. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/DrawerContent-TpYTFgF1.js.map +0 -0
  65. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Empty-cr2r7e2u.js +0 -0
  66. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Empty-cr2r7e2u.js.map +0 -0
  67. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/EmptyState-DeDck-OL.js +0 -0
  68. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/EmptyState-DeDck-OL.js.map +0 -0
  69. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Grid-hFkp9F4P.js +0 -0
  70. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Grid-hFkp9F4P.js.map +0 -0
  71. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Input-DppYTq9C.js +0 -0
  72. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Input-DppYTq9C.js.map +0 -0
  73. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Invoke-DW3Nveeh.js +0 -0
  74. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Invoke-DW3Nveeh.js.map +0 -0
  75. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/JsonField-DibyJgun.js +0 -0
  76. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/JsonField-DibyJgun.js.map +0 -0
  77. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/LoginView-BjLyE3Ds.css +0 -0
  78. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/LoginView-CoOjECT_.js +0 -0
  79. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/LoginView-CoOjECT_.js.map +0 -0
  80. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Logs-D9WYrnIT.js +0 -0
  81. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Logs-D9WYrnIT.js.map +0 -0
  82. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Logs-DS1XPa0h.css +0 -0
  83. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Migrations-DOSC2ddQ.js +0 -0
  84. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Migrations-DOSC2ddQ.js.map +0 -0
  85. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/ObjectBrowser-_5w8vOX8.js +0 -0
  86. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/ObjectBrowser-_5w8vOX8.js.map +0 -0
  87. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Queue-CywZs6vI.js +0 -0
  88. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Queue-CywZs6vI.js.map +0 -0
  89. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/RefreshTokens-Ccjr53jg.js +0 -0
  90. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/RefreshTokens-Ccjr53jg.js.map +0 -0
  91. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/RlsEditor-BSlH9vSc.js +0 -0
  92. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/RlsEditor-BSlH9vSc.js.map +0 -0
  93. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Routes-BiLXE49D.js +0 -0
  94. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Routes-BiLXE49D.js.map +0 -0
  95. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Routes-C-ianIGD.css +0 -0
  96. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/SchemaBrowser-DKy2_KQi.css +0 -0
  97. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/SchemaBrowser-XFvFbtDB.js +0 -0
  98. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/SchemaBrowser-XFvFbtDB.js.map +0 -0
  99. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Select-DIzZyRZb.js +0 -0
  100. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Select-DIzZyRZb.js.map +0 -0
  101. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Space-n5-XcguU.js +0 -0
  102. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Space-n5-XcguU.js.map +0 -0
  103. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/SqlEditor-b8pTsILY.js +0 -0
  104. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/SqlEditor-b8pTsILY.js.map +0 -0
  105. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/SqlWorkspace-BUS7IntH.js +0 -0
  106. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/SqlWorkspace-BUS7IntH.js.map +0 -0
  107. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/TableData-CQIagLKn.js +0 -0
  108. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/TableData-CQIagLKn.js.map +0 -0
  109. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Tag-D1fOKpTH.js +0 -0
  110. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Tag-D1fOKpTH.js.map +0 -0
  111. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Templates-BS-ugkdq.js +0 -0
  112. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Templates-BS-ugkdq.js.map +0 -0
  113. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Thing-CEAniuMg.js +0 -0
  114. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Thing-CEAniuMg.js.map +0 -0
  115. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Users-wzwajhlh.js +0 -0
  116. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/Users-wzwajhlh.js.map +0 -0
  117. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/_plugin-vue_export-helper-DGA9ry_j.js +0 -0
  118. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/dist-VXIJLCYq.js +0 -0
  119. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/dist-VXIJLCYq.js.map +0 -0
  120. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/format-length-CGCY1rMh.js +0 -0
  121. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/format-length-CGCY1rMh.js.map +0 -0
  122. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/get-Ca6unauB.js +0 -0
  123. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/get-Ca6unauB.js.map +0 -0
  124. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/index-CeE6v959.js +0 -0
  125. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/index-CeE6v959.js.map +0 -0
  126. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/pinia-COXwfrOX.js +0 -0
  127. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/pinia-COXwfrOX.js.map +0 -0
  128. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/resources-Bt6thQCD.js +0 -0
  129. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/resources-Bt6thQCD.js.map +0 -0
  130. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/use-locale-mtgM0a3a.js +0 -0
  131. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/use-locale-mtgM0a3a.js.map +0 -0
  132. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/use-merged-state-BvhkaHNX.js +0 -0
  133. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/use-merged-state-BvhkaHNX.js.map +0 -0
  134. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/useConfirm-tMjvBFXR.js +0 -0
  135. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/useConfirm-tMjvBFXR.js.map +0 -0
  136. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/useResource-C_rJCY8C.js +0 -0
  137. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/useResource-C_rJCY8C.js.map +0 -0
  138. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/useTable-CnZc5zhi.js +0 -0
  139. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/useTable-CnZc5zhi.js.map +0 -0
  140. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/useTable-Dg0XlRlq.css +0 -0
  141. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/useToast-DsZKx0IX.js +0 -0
  142. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/useToast-DsZKx0IX.js.map +0 -0
  143. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/utils-sbXoq7Ir.js +0 -0
  144. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/assets/utils-sbXoq7Ir.js.map +0 -0
  145. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/favicon.svg +0 -0
  146. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/icons.svg +0 -0
  147. {supython-0.1.8 → supython-0.1.10}/src/supython/admin/static/index.html +0 -0
  148. {supython-0.1.8 → supython-0.1.10}/src/supython/auth/__init__.py +0 -0
  149. {supython-0.1.8 → supython-0.1.10}/src/supython/auth/_email_job.py +0 -0
  150. {supython-0.1.8 → supython-0.1.10}/src/supython/auth/claims.py +0 -0
  151. {supython-0.1.8 → supython-0.1.10}/src/supython/auth/deps.py +0 -0
  152. {supython-0.1.8 → supython-0.1.10}/src/supython/auth/providers/__init__.py +0 -0
  153. {supython-0.1.8 → supython-0.1.10}/src/supython/auth/providers/github.py +0 -0
  154. {supython-0.1.8 → supython-0.1.10}/src/supython/auth/providers/google.py +0 -0
  155. {supython-0.1.8 → supython-0.1.10}/src/supython/auth/providers/oauth.py +0 -0
  156. {supython-0.1.8 → supython-0.1.10}/src/supython/auth/providers/registry.py +0 -0
  157. {supython-0.1.8 → supython-0.1.10}/src/supython/auth/ratelimit.py +0 -0
  158. {supython-0.1.8 → supython-0.1.10}/src/supython/auth/router.py +0 -0
  159. {supython-0.1.8 → supython-0.1.10}/src/supython/auth/schemas.py +0 -0
  160. {supython-0.1.8 → supython-0.1.10}/src/supython/auth/service.py +0 -0
  161. {supython-0.1.8 → supython-0.1.10}/src/supython/backups/__init__.py +0 -0
  162. {supython-0.1.8 → supython-0.1.10}/src/supython/backups/_backup_job.py +0 -0
  163. {supython-0.1.8 → supython-0.1.10}/src/supython/backups/schemas.py +0 -0
  164. {supython-0.1.8 → supython-0.1.10}/src/supython/backups/service.py +0 -0
  165. {supython-0.1.8 → supython-0.1.10}/src/supython/body_size.py +0 -0
  166. {supython-0.1.8 → supython-0.1.10}/src/supython/client/__init__.py +0 -0
  167. {supython-0.1.8 → supython-0.1.10}/src/supython/client/_auth.py +0 -0
  168. {supython-0.1.8 → supython-0.1.10}/src/supython/client/_client.py +0 -0
  169. {supython-0.1.8 → supython-0.1.10}/src/supython/client/_config.py +0 -0
  170. {supython-0.1.8 → supython-0.1.10}/src/supython/client/_functions.py +0 -0
  171. {supython-0.1.8 → supython-0.1.10}/src/supython/client/_storage.py +0 -0
  172. {supython-0.1.8 → supython-0.1.10}/src/supython/client/py.typed +0 -0
  173. {supython-0.1.8 → supython-0.1.10}/src/supython/db.py +0 -0
  174. {supython-0.1.8 → supython-0.1.10}/src/supython/db_admin.py +0 -0
  175. {supython-0.1.8 → supython-0.1.10}/src/supython/extensions.py +0 -0
  176. {supython-0.1.8 → supython-0.1.10}/src/supython/functions/__init__.py +0 -0
  177. {supython-0.1.8 → supython-0.1.10}/src/supython/functions/context.py +0 -0
  178. {supython-0.1.8 → supython-0.1.10}/src/supython/functions/loader.py +0 -0
  179. {supython-0.1.8 → supython-0.1.10}/src/supython/functions/router.py +0 -0
  180. {supython-0.1.8 → supython-0.1.10}/src/supython/functions/schemas.py +0 -0
  181. {supython-0.1.8 → supython-0.1.10}/src/supython/gen/__init__.py +0 -0
  182. {supython-0.1.8 → supython-0.1.10}/src/supython/gen/_introspect.py +0 -0
  183. {supython-0.1.8 → supython-0.1.10}/src/supython/gen/types_py.py +0 -0
  184. {supython-0.1.8 → supython-0.1.10}/src/supython/gen/types_ts.py +0 -0
  185. {supython-0.1.8 → supython-0.1.10}/src/supython/health.py +0 -0
  186. {supython-0.1.8 → supython-0.1.10}/src/supython/hooks.py +0 -0
  187. {supython-0.1.8 → supython-0.1.10}/src/supython/jobs/__init__.py +0 -0
  188. {supython-0.1.8 → supython-0.1.10}/src/supython/jobs/backends.py +0 -0
  189. {supython-0.1.8 → supython-0.1.10}/src/supython/jobs/context.py +0 -0
  190. {supython-0.1.8 → supython-0.1.10}/src/supython/jobs/cron_inproc.py +0 -0
  191. {supython-0.1.8 → supython-0.1.10}/src/supython/jobs/registry.py +0 -0
  192. {supython-0.1.8 → supython-0.1.10}/src/supython/jobs/router.py +0 -0
  193. {supython-0.1.8 → supython-0.1.10}/src/supython/jobs/schemas.py +0 -0
  194. {supython-0.1.8 → supython-0.1.10}/src/supython/jobs/service.py +0 -0
  195. {supython-0.1.8 → supython-0.1.10}/src/supython/jobs/worker.py +0 -0
  196. {supython-0.1.8 → supython-0.1.10}/src/supython/jwks.py +0 -0
  197. {supython-0.1.8 → supython-0.1.10}/src/supython/keyset.py +0 -0
  198. {supython-0.1.8 → supython-0.1.10}/src/supython/logging_config.py +0 -0
  199. {supython-0.1.8 → supython-0.1.10}/src/supython/mail.py +0 -0
  200. {supython-0.1.8 → supython-0.1.10}/src/supython/mailer.py +0 -0
  201. {supython-0.1.8 → supython-0.1.10}/src/supython/migrate.py +0 -0
  202. {supython-0.1.8 → supython-0.1.10}/src/supython/migrations/0001_extensions_and_roles.sql +0 -0
  203. {supython-0.1.8 → supython-0.1.10}/src/supython/migrations/0002_auth_schema.sql +0 -0
  204. {supython-0.1.8 → supython-0.1.10}/src/supython/migrations/0003_demo_todos.sql +0 -0
  205. {supython-0.1.8 → supython-0.1.10}/src/supython/migrations/0004_auth_v0_2.sql +0 -0
  206. {supython-0.1.8 → supython-0.1.10}/src/supython/migrations/0005_storage_schema.sql +0 -0
  207. {supython-0.1.8 → supython-0.1.10}/src/supython/migrations/0006_realtime_schema.sql +0 -0
  208. {supython-0.1.8 → supython-0.1.10}/src/supython/migrations/0007_jobs_schema.sql +0 -0
  209. {supython-0.1.8 → supython-0.1.10}/src/supython/migrations/0008_jobs_last_error.sql +0 -0
  210. {supython-0.1.8 → supython-0.1.10}/src/supython/migrations/0009_auth_rate_limits.sql +0 -0
  211. {supython-0.1.8 → supython-0.1.10}/src/supython/migrations/0010_worker_heartbeat.sql +0 -0
  212. {supython-0.1.8 → supython-0.1.10}/src/supython/migrations/0011_admin_schema.sql +0 -0
  213. {supython-0.1.8 → supython-0.1.10}/src/supython/migrations/0012_auth_banned_until.sql +0 -0
  214. {supython-0.1.8 → supython-0.1.10}/src/supython/migrations/0013_email_templates.sql +0 -0
  215. {supython-0.1.8 → supython-0.1.10}/src/supython/migrations/0014_realtime_payload_warning.sql +0 -0
  216. {supython-0.1.8 → supython-0.1.10}/src/supython/migrations/0015_backups_schema.sql +0 -0
  217. {supython-0.1.8 → supython-0.1.10}/src/supython/passwords.py +0 -0
  218. {supython-0.1.8 → supython-0.1.10}/src/supython/realtime/__init__.py +0 -0
  219. {supython-0.1.8 → supython-0.1.10}/src/supython/realtime/broker.py +0 -0
  220. {supython-0.1.8 → supython-0.1.10}/src/supython/realtime/protocol.py +0 -0
  221. {supython-0.1.8 → supython-0.1.10}/src/supython/realtime/router.py +0 -0
  222. {supython-0.1.8 → supython-0.1.10}/src/supython/realtime/schemas.py +0 -0
  223. {supython-0.1.8 → supython-0.1.10}/src/supython/realtime/service.py +0 -0
  224. {supython-0.1.8 → supython-0.1.10}/src/supython/realtime/topics.py +0 -0
  225. {supython-0.1.8 → supython-0.1.10}/src/supython/realtime/websocket.py +0 -0
  226. {supython-0.1.8 → supython-0.1.10}/src/supython/scaffold/__init__.py +0 -0
  227. {supython-0.1.8 → supython-0.1.10}/src/supython/scaffold/init_project.py +0 -0
  228. {supython-0.1.8 → supython-0.1.10}/src/supython/scaffold/templates/Caddyfile.tmpl +0 -0
  229. {supython-0.1.8 → supython-0.1.10}/src/supython/scaffold/templates/README.md.tmpl +0 -0
  230. {supython-0.1.8 → supython-0.1.10}/src/supython/scaffold/templates/apps_hooks.py.tmpl +0 -0
  231. {supython-0.1.8 → supython-0.1.10}/src/supython/scaffold/templates/apps_jobs.py.tmpl +0 -0
  232. {supython-0.1.8 → supython-0.1.10}/src/supython/scaffold/templates/asgi.py.tmpl +0 -0
  233. {supython-0.1.8 → supython-0.1.10}/src/supython/scaffold/templates/docker-compose.prod.yml.tmpl +0 -0
  234. {supython-0.1.8 → supython-0.1.10}/src/supython/scaffold/templates/docker-compose.yml.tmpl +0 -0
  235. {supython-0.1.8 → supython-0.1.10}/src/supython/scaffold/templates/docker_postgres_Dockerfile.tmpl +0 -0
  236. {supython-0.1.8 → supython-0.1.10}/src/supython/scaffold/templates/docker_postgres_postgresql.conf.tmpl +0 -0
  237. {supython-0.1.8 → supython-0.1.10}/src/supython/scaffold/templates/env.example.tmpl +0 -0
  238. {supython-0.1.8 → supython-0.1.10}/src/supython/scaffold/templates/functions_README.md.tmpl +0 -0
  239. {supython-0.1.8 → supython-0.1.10}/src/supython/scaffold/templates/gitignore.tmpl +0 -0
  240. {supython-0.1.8 → supython-0.1.10}/src/supython/scaffold/templates/manage.py.tmpl +0 -0
  241. {supython-0.1.8 → supython-0.1.10}/src/supython/scaffold/templates/migrations/.gitkeep +0 -0
  242. {supython-0.1.8 → supython-0.1.10}/src/supython/scaffold/templates/package_init.py.tmpl +0 -0
  243. {supython-0.1.8 → supython-0.1.10}/src/supython/scaffold/templates/settings.py.tmpl +0 -0
  244. {supython-0.1.8 → supython-0.1.10}/src/supython/secretset.py +0 -0
  245. {supython-0.1.8 → supython-0.1.10}/src/supython/security_headers.py +0 -0
  246. {supython-0.1.8 → supython-0.1.10}/src/supython/settings_module.py +0 -0
  247. {supython-0.1.8 → supython-0.1.10}/src/supython/storage/__init__.py +0 -0
  248. {supython-0.1.8 → supython-0.1.10}/src/supython/storage/backends.py +0 -0
  249. {supython-0.1.8 → supython-0.1.10}/src/supython/storage/router.py +0 -0
  250. {supython-0.1.8 → supython-0.1.10}/src/supython/storage/schemas.py +0 -0
  251. {supython-0.1.8 → supython-0.1.10}/src/supython/storage/service.py +0 -0
  252. {supython-0.1.8 → supython-0.1.10}/src/supython/storage/signing.py +0 -0
  253. {supython-0.1.8 → supython-0.1.10}/src/supython/tokens.py +0 -0
@@ -31,6 +31,62 @@ Each entry links the relevant `PROJECT.md` section and decision-log row
31
31
 
32
32
  ---
33
33
 
34
+ ## [0.1.10] — 2026-06-12
35
+
36
+ ### Added
37
+
38
+ - `python-dotenv` is now a direct dependency (previously only transitive via
39
+ `pydantic-settings`), backing the new boot-time `.env` export.
40
+
41
+ ### Fixed
42
+
43
+ - `.env` is now exported into `os.environ` at the start of every boot path
44
+ (`create_app`, `supython worker run`, CLI subcommands) via the shared
45
+ `settings.export_env_file()` helper, **before** extensions load. Previously
46
+ pydantic-settings loaded `.env` into the typed `Settings` model only, so
47
+ dynamically-named secrets read via `os.environ.get(<name>)` (the `secret_ref`
48
+ convention) resolved to `None` under `supython dev` / `worker run` / CLI —
49
+ working in containers only because docker-compose's `env_file:` injected
50
+ `.env` first. The export uses `override=False`, so real environment variables
51
+ set by an orchestrator always win, and the path is sourced from the same
52
+ `SettingsConfigDict.env_file`. Downstream apps can delete their ad-hoc
53
+ `load_dotenv()` boot shims. (#1)
54
+
55
+ ---
56
+
57
+ ## [0.1.9] — 2026-05-29
58
+
59
+ ### Fixed
60
+
61
+ - `@cron(...)` now also registers the decorated function as the job
62
+ handler for `job_name`. Previously the decorator only wrote a
63
+ `CronDefinition`, so when pg_cron fired `jobs.enqueue(p_name :=
64
+ <job_name>)` the worker rejected the job with `unknown job:
65
+ <job_name>` on every tick. Pass `register_handler=False` to keep
66
+ the pre-fix behaviour (schedule fires a handler declared elsewhere
67
+ with `@job`).
68
+ - `supython cron list` and `supython cron sync` now load the user's
69
+ settings module and `EXTENSIONS` before reading the registry,
70
+ mirroring `worker run` and the FastAPI startup path. Without the
71
+ bootstrap, `cron list` always printed `no crons registered` and a
72
+ subsequent `cron sync` would silently wipe every row from
73
+ `jobs.cron_schedules` and unschedule every matching pg_cron entry.
74
+
75
+ ### Added
76
+
77
+ - `@cron(...)` accepts `register_handler` (default `True`) plus the
78
+ job kwargs forwarded to the auto-registered `JobDefinition`:
79
+ `max_attempts`, `backoff`, `backoff_base_s`, `backoff_max_s`,
80
+ `role`, `claims_from`, `accepts_payload`.
81
+ - `supython cron sync --allow-empty` (and the matching
82
+ `sync_pg_cron(conn, allow_empty=True)` argument). Without the flag,
83
+ `sync_pg_cron` now refuses to act when the in-process registry is
84
+ empty but `jobs.cron_schedules` is non-empty — guards against a
85
+ forgotten extension bootstrap silently wiping production schedules.
86
+ The empty-registry-and-empty-table cold-start case stays a no-op.
87
+
88
+ ---
89
+
34
90
  ## [0.1.0] — 2026-05-09
35
91
 
36
92
  The first public release. Everything currently on `main` collapses
@@ -201,4 +257,6 @@ v0.1–v0.7 plus a v1.1.x admin track; see §19 decision log
201
257
  ---
202
258
 
203
259
 
260
+ [0.1.10]: https://github.com/Tkeby/supython/releases/tag/v0.1.10
261
+ [0.1.9]: https://github.com/Tkeby/supython/releases/tag/v0.1.9
204
262
  [0.1.0]: https://github.com/Tkeby/supython/releases/tag/v0.1.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: supython
3
- Version: 0.1.8
3
+ Version: 0.1.10
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
@@ -35,6 +35,7 @@ Requires-Dist: itsdangerous>=2.2
35
35
  Requires-Dist: pydantic-settings>=2.6
36
36
  Requires-Dist: pydantic>=2.9
37
37
  Requires-Dist: pyjwt[crypto]>=2.10
38
+ Requires-Dist: python-dotenv>=1.0
38
39
  Requires-Dist: python-multipart>=0.0.20
39
40
  Requires-Dist: typer>=0.15
40
41
  Requires-Dist: uvicorn[standard]>=0.32
@@ -58,7 +59,7 @@ Description-Content-Type: text/markdown
58
59
 
59
60
  # supython
60
61
 
61
- > A lightweight, Postgres-first BaaS framework for Python. **v0.1.4 release**
62
+ > A lightweight, Postgres-first BaaS framework for Python.
62
63
 
63
64
  **the database owns the schema, Python owns the things SQL is bad at**.
64
65
  It leans on [PostgREST](https://postgrest.org)
@@ -69,7 +70,7 @@ storage, functions, workers, and an optional admin control plane.
69
70
  supython is for a specific person with a specific problem:
70
71
  > A developer who wants to build a CRUD-heavy web app (most apps are), who thinks in SQL, who wants Postgres to own authorization, and who wants auth + storage + custom logic without assembling the integration themselves.
71
72
 
72
- Shipped [v0.1.2]:
73
+
73
74
 
74
75
  **Core platform**
75
76
  - **Email/password auth** — signup, login, refresh-token rotation with **reuse detection**
@@ -106,7 +107,7 @@ Shipped [v0.1.2]:
106
107
  - **Secret rotation** — JWT keys, symmetric secrets, Postgres passwords; all with zero-downtime runbooks
107
108
  - **Multi-arch Docker image** — `linux/amd64` + `linux/arm64`, non-root user, `tini` PID 1, ~64 MB
108
109
 
109
- **Admin control plane** (shipped in v0.1.2)
110
+ **Admin control plane**
110
111
  - **Vue 3 + Vite SPA** at `/admin` — no runtime Node deps; pre-built static bundle in the wheel
111
112
  - **Database surface** — schema browser, table data with role switcher, SQL workspace (read-only default + write toggle), RLS policy editor with dry-run, migrations panel
112
113
  - **Auth surface** — user search, ban/unban/force-logout, refresh-token inspector, audit log, email template editing
@@ -393,7 +394,7 @@ supython dev
393
394
  | `JOBS_DRAIN_TIMEOUT_S` | `30.0` | Graceful shutdown drain (seconds) |
394
395
  | `JOBS_DEV_INPROCESS` | `false` | Spawn worker in-process during `supython dev` |
395
396
 
396
- ## v0.2 auth endpoints
397
+ ## Auth endpoints
397
398
 
398
399
  | Endpoint | Method | Purpose |
399
400
  |---|---|---|
@@ -436,7 +437,7 @@ denylist without breaking that contract. The mitigation is a **short
436
437
  long-lived (`REFRESH_TOKEN_TTL=30d`) so users don't get prompted to log
437
438
  in every few minutes — clients refresh transparently.
438
439
 
439
- ### Custom JWT claims [shipped v0.1.3]
440
+ ### Custom JWT claims
440
441
 
441
442
  Register a claims provider to inject application-specific claims into every
442
443
  access token minted by the auth endpoints. Each provider is an async callable
@@ -472,7 +473,7 @@ Notes:
472
473
  - Refresh re-collects claims, so a token rotated via `/auth/v1/refresh`
473
474
  reflects current state.
474
475
 
475
- ### Reading the caller in your routes [shipped v0.1.4]
476
+ ### Reading the caller in your routes
476
477
 
477
478
  Application code mounts its own routers alongside supython's. To gate an
478
479
  endpoint on a valid bearer token — or to read custom claims out of it —
@@ -818,7 +819,7 @@ unit tests always run in isolation.
818
819
  **CI:** runners with Docker run `supython test up && supython test run`;
819
820
  runners without Docker run `pytest tests/unit` for a meaningful subset.
820
821
 
821
- ## Roadmap [shipped v0.1.2]
822
+ ## Roadmap
822
823
 
823
824
  - ✅ Email/password auth, PostgREST contract, RLS demo
824
825
  - ✅ OAuth, password reset, magic link, OTP, reuse detection, email backend, test suite
@@ -829,12 +830,11 @@ runners without Docker run `pytest tests/unit` for a meaningful subset.
829
830
  - ✅ Production observable: structured JSON logs, `/livez`/`/readyz`/`/health`, security headers, input size guards, audit log completeness, OAuth PKCE, secret rotation runbooks
830
831
  - ✅ (partial) Multi-arch Docker images, admin control plane (Vue 3 SPA — database, auth, storage, functions, realtime, jobs, backups, log tail), CI buildx workflow; benchmarks + security audit pass + dependency budget CI remaining
831
832
  - *(deferred)* — Realtime v2 over logical replication
832
- - v0.1.2 Release — final sweep, tag, publish wheel, production deployment with no patches
833
833
  - ✅ **TypeScript SDK** — `@supython/sdk` wrapping `@supabase/postgrest-js` + `@supabase/realtime-js`
834
834
 
835
- ### Post v0.1.2
835
+ ### Future
836
836
 
837
- - **v1.1+** — Admin control plane polish (backend + frontend shipped in v0.1.2; tests + remaining DoD items deferred)
837
+ - **Admin control plane** polish (tests + remaining DoD items)
838
838
  - **Realtime v2** — logical replication (demand-driven; swap when trigger overhead or >8KB payload data warrants it)
839
839
  - **Prometheus `/metrics`** + **OpenTelemetry** — optional extras
840
840
 
@@ -1,6 +1,6 @@
1
1
  # supython
2
2
 
3
- > A lightweight, Postgres-first BaaS framework for Python. **v0.1.4 release**
3
+ > A lightweight, Postgres-first BaaS framework for Python.
4
4
 
5
5
  **the database owns the schema, Python owns the things SQL is bad at**.
6
6
  It leans on [PostgREST](https://postgrest.org)
@@ -11,7 +11,7 @@ storage, functions, workers, and an optional admin control plane.
11
11
  supython is for a specific person with a specific problem:
12
12
  > A developer who wants to build a CRUD-heavy web app (most apps are), who thinks in SQL, who wants Postgres to own authorization, and who wants auth + storage + custom logic without assembling the integration themselves.
13
13
 
14
- Shipped [v0.1.2]:
14
+
15
15
 
16
16
  **Core platform**
17
17
  - **Email/password auth** — signup, login, refresh-token rotation with **reuse detection**
@@ -48,7 +48,7 @@ Shipped [v0.1.2]:
48
48
  - **Secret rotation** — JWT keys, symmetric secrets, Postgres passwords; all with zero-downtime runbooks
49
49
  - **Multi-arch Docker image** — `linux/amd64` + `linux/arm64`, non-root user, `tini` PID 1, ~64 MB
50
50
 
51
- **Admin control plane** (shipped in v0.1.2)
51
+ **Admin control plane**
52
52
  - **Vue 3 + Vite SPA** at `/admin` — no runtime Node deps; pre-built static bundle in the wheel
53
53
  - **Database surface** — schema browser, table data with role switcher, SQL workspace (read-only default + write toggle), RLS policy editor with dry-run, migrations panel
54
54
  - **Auth surface** — user search, ban/unban/force-logout, refresh-token inspector, audit log, email template editing
@@ -335,7 +335,7 @@ supython dev
335
335
  | `JOBS_DRAIN_TIMEOUT_S` | `30.0` | Graceful shutdown drain (seconds) |
336
336
  | `JOBS_DEV_INPROCESS` | `false` | Spawn worker in-process during `supython dev` |
337
337
 
338
- ## v0.2 auth endpoints
338
+ ## Auth endpoints
339
339
 
340
340
  | Endpoint | Method | Purpose |
341
341
  |---|---|---|
@@ -378,7 +378,7 @@ denylist without breaking that contract. The mitigation is a **short
378
378
  long-lived (`REFRESH_TOKEN_TTL=30d`) so users don't get prompted to log
379
379
  in every few minutes — clients refresh transparently.
380
380
 
381
- ### Custom JWT claims [shipped v0.1.3]
381
+ ### Custom JWT claims
382
382
 
383
383
  Register a claims provider to inject application-specific claims into every
384
384
  access token minted by the auth endpoints. Each provider is an async callable
@@ -414,7 +414,7 @@ Notes:
414
414
  - Refresh re-collects claims, so a token rotated via `/auth/v1/refresh`
415
415
  reflects current state.
416
416
 
417
- ### Reading the caller in your routes [shipped v0.1.4]
417
+ ### Reading the caller in your routes
418
418
 
419
419
  Application code mounts its own routers alongside supython's. To gate an
420
420
  endpoint on a valid bearer token — or to read custom claims out of it —
@@ -760,7 +760,7 @@ unit tests always run in isolation.
760
760
  **CI:** runners with Docker run `supython test up && supython test run`;
761
761
  runners without Docker run `pytest tests/unit` for a meaningful subset.
762
762
 
763
- ## Roadmap [shipped v0.1.2]
763
+ ## Roadmap
764
764
 
765
765
  - ✅ Email/password auth, PostgREST contract, RLS demo
766
766
  - ✅ OAuth, password reset, magic link, OTP, reuse detection, email backend, test suite
@@ -771,12 +771,11 @@ runners without Docker run `pytest tests/unit` for a meaningful subset.
771
771
  - ✅ Production observable: structured JSON logs, `/livez`/`/readyz`/`/health`, security headers, input size guards, audit log completeness, OAuth PKCE, secret rotation runbooks
772
772
  - ✅ (partial) Multi-arch Docker images, admin control plane (Vue 3 SPA — database, auth, storage, functions, realtime, jobs, backups, log tail), CI buildx workflow; benchmarks + security audit pass + dependency budget CI remaining
773
773
  - *(deferred)* — Realtime v2 over logical replication
774
- - v0.1.2 Release — final sweep, tag, publish wheel, production deployment with no patches
775
774
  - ✅ **TypeScript SDK** — `@supython/sdk` wrapping `@supabase/postgrest-js` + `@supabase/realtime-js`
776
775
 
777
- ### Post v0.1.2
776
+ ### Future
778
777
 
779
- - **v1.1+** — Admin control plane polish (backend + frontend shipped in v0.1.2; tests + remaining DoD items deferred)
778
+ - **Admin control plane** polish (tests + remaining DoD items)
780
779
  - **Realtime v2** — logical replication (demand-driven; swap when trigger overhead or >8KB payload data warrants it)
781
780
  - **Prometheus `/metrics`** + **OpenTelemetry** — optional extras
782
781
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "supython"
7
- version = "0.1.8"
7
+ version = "0.1.10"
8
8
  description = "A lightweight Postgres-first BaaS framework for Python"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -32,6 +32,7 @@ dependencies = [
32
32
  "asyncpg>=0.30",
33
33
  "pydantic>=2.9",
34
34
  "pydantic-settings>=2.6",
35
+ "python-dotenv>=1.0",
35
36
  "email-validator>=2.2",
36
37
  "argon2-cffi>=23.1",
37
38
  "pyjwt[crypto]>=2.10",
@@ -27,7 +27,7 @@ from .body_size import BodySizeLimitMiddleware
27
27
  from .security_headers import SecurityHeadersMiddleware
28
28
  from .realtime import get_broker
29
29
  from .realtime.router import router as realtime_router
30
- from .settings import get_settings
30
+ from .settings import export_env_file, get_settings
31
31
  from .storage.router import router as storage_router
32
32
 
33
33
  logger = logging.getLogger(__name__)
@@ -96,6 +96,7 @@ async def _lifespan(app: FastAPI) -> AsyncIterator[None]:
96
96
 
97
97
 
98
98
  def create_app() -> FastAPI:
99
+ export_env_file()
99
100
  settings = get_settings()
100
101
  configure_logging(settings.log_level, json_format=settings.log_json)
101
102
 
@@ -22,7 +22,7 @@ from .db_admin import rotate_role_password
22
22
  from .gen import render_types_py, render_types_ts
23
23
  from .logging_config import configure_logging
24
24
  from .scaffold import scaffold
25
- from .settings import Settings, get_settings
25
+ from .settings import Settings, export_env_file, get_settings
26
26
 
27
27
  app = typer.Typer(
28
28
  help="supython — lightweight Postgres-first BaaS for Python",
@@ -115,6 +115,23 @@ def _run_async(coro): # type: ignore[no-untyped-def]
115
115
  return asyncio.run(coro)
116
116
 
117
117
 
118
+ def _bootstrap_user_modules() -> None:
119
+ """Import the user's settings module + extensions so decorators register.
120
+
121
+ Mirrors ``worker_run`` and ``create_app``. CLI subcommands that read the
122
+ in-process registry (e.g. ``cron list``, ``cron sync``) must call this
123
+ first, otherwise the registry is empty and a ``cron sync`` would wipe
124
+ every row from ``jobs.cron_schedules``.
125
+ """
126
+ from .extensions import load_extensions
127
+ from .settings_module import UserSettings, load_user_settings
128
+
129
+ export_env_file()
130
+ s = get_settings()
131
+ user = load_user_settings(s.settings_module) if s.settings_module else UserSettings()
132
+ load_extensions([*s.extensions, *user.extensions])
133
+
134
+
118
135
  @gen_cli.command("types")
119
136
  def gen_types(
120
137
  lang: str = typer.Option("py", "--lang", help="Target language (`py` or `ts`)."),
@@ -232,6 +249,7 @@ def worker_run(
232
249
  from .jobs.worker import Worker
233
250
  from .settings_module import UserSettings, load_user_settings
234
251
 
252
+ export_env_file()
235
253
  s = get_settings()
236
254
  configure_logging(s.log_level, json_format=s.log_json)
237
255
  s.jobs_queue_default = queue
@@ -418,6 +436,7 @@ def cron_list() -> None:
418
436
  """List registered cron schedules."""
419
437
  from .jobs.registry import get_registry
420
438
 
439
+ _bootstrap_user_modules()
421
440
  crons = list(get_registry().iter_crons())
422
441
  if not crons:
423
442
  typer.echo("no crons registered.")
@@ -427,17 +446,29 @@ def cron_list() -> None:
427
446
 
428
447
 
429
448
  @cron_cli.command("sync")
430
- def cron_sync() -> None:
449
+ def cron_sync(
450
+ allow_empty: bool = typer.Option(
451
+ False,
452
+ "--allow-empty",
453
+ help=(
454
+ "Allow sync to proceed when the in-process registry has no @cron "
455
+ "definitions but `jobs.cron_schedules` has rows. Without this "
456
+ "flag the sync refuses, because it would otherwise wipe every "
457
+ "row and unschedule every pg_cron entry."
458
+ ),
459
+ ),
460
+ ) -> None:
431
461
  """Sync registered crons with pg_cron."""
432
462
  from .jobs.cron import sync_pg_cron
433
463
 
464
+ _bootstrap_user_modules()
434
465
  s = get_settings()
435
466
 
436
467
  async def _run():
437
468
  conn = await asyncpg.connect(s.database_url)
438
469
  try:
439
470
  await conn.execute("set role service_role")
440
- await sync_pg_cron(conn)
471
+ await sync_pg_cron(conn, allow_empty=allow_empty)
441
472
  finally:
442
473
  await conn.close()
443
474
 
@@ -51,7 +51,11 @@ async def _as_login_role(conn: asyncpg.Connection) -> AsyncIterator[None]:
51
51
  await conn.execute(f'set {scope}role "{prev_role}"')
52
52
 
53
53
 
54
- async def sync_pg_cron(conn: asyncpg.Connection) -> None:
54
+ async def sync_pg_cron(
55
+ conn: asyncpg.Connection,
56
+ *,
57
+ allow_empty: bool = False,
58
+ ) -> None:
55
59
  """Upsert ``pg_cron`` schedule rows from the registry and remove stale ones.
56
60
 
57
61
  The caller is expected to have entered ``db.as_service_role()`` (or
@@ -62,18 +66,41 @@ async def sync_pg_cron(conn: asyncpg.Connection) -> None:
62
66
  initialise a session at tick time.
63
67
 
64
68
  When pg_cron is installed, errors from ``cron.schedule`` propagate
65
- to the caller.
69
+ to the caller.
66
70
 
67
71
  When pg_cron is NOT installed the metadata row is still upserted so
68
72
  the in-process scheduler (or a manual runner) can pick it up, and
69
73
  ``cron.schedule`` is skipped entirely.
74
+
75
+ Raises ``ValueError`` when the in-process registry has no ``@cron``
76
+ definitions but ``jobs.cron_schedules`` is non-empty. That mismatch
77
+ almost always means the caller forgot to import the user's
78
+ ``EXTENSIONS`` before syncing, and proceeding would wipe every row
79
+ (and unschedule every pg_cron entry). Pass ``allow_empty=True`` to
80
+ intentionally clear the table.
70
81
  """
71
82
  registry = get_registry()
83
+ cron_defns = list(registry.iter_crons())
84
+
72
85
  has_pg_cron = await conn.fetchval(
73
86
  "select exists(select 1 from pg_extension where extname = 'pg_cron')"
74
87
  )
75
88
 
76
- for cron_defn in registry.iter_crons():
89
+ if not cron_defns and not allow_empty:
90
+ existing_count = await conn.fetchval(
91
+ "select count(*) from jobs.cron_schedules"
92
+ )
93
+ if existing_count:
94
+ raise ValueError(
95
+ f"registry has no @cron definitions but jobs.cron_schedules "
96
+ f"holds {existing_count} row(s) — refusing to sync, since "
97
+ f"this would wipe the table and unschedule every pg_cron "
98
+ f"entry. Pass allow_empty=True to confirm, or check that "
99
+ f"the module containing your @cron decorators is listed in "
100
+ f"EXTENSIONS / SUPYTHON_SETTINGS_MODULE."
101
+ )
102
+
103
+ for cron_defn in cron_defns:
77
104
  await conn.execute(
78
105
  """
79
106
  insert into jobs.cron_schedules
@@ -123,7 +150,7 @@ async def sync_pg_cron(conn: asyncpg.Connection) -> None:
123
150
  command,
124
151
  )
125
152
 
126
- registered_names = {c.name for c in registry.iter_crons()}
153
+ registered_names = {c.name for c in cron_defns}
127
154
  existing = await conn.fetch("select name from jobs.cron_schedules")
128
155
  for row in existing:
129
156
  if row["name"] in registered_names:
@@ -59,18 +59,53 @@ def cron(
59
59
  job_version: int = 1,
60
60
  payload: dict | None = None,
61
61
  queue: str = "default",
62
+ register_handler: bool = True,
63
+ max_attempts: int = 3,
64
+ backoff: Backoff | str = Backoff.EXPONENTIAL,
65
+ backoff_base_s: float = 5.0,
66
+ backoff_max_s: float = 300.0,
67
+ role: str = "service_role",
68
+ claims_from: str | None = None,
69
+ accepts_payload: bool = True,
62
70
  ) -> Callable[..., Any]:
71
+ """Schedule the decorated function on a cron expression.
72
+
73
+ By default the decorated function is also registered as the handler for
74
+ ``job_name`` so pg_cron's tick → ``jobs.enqueue`` → worker dispatch
75
+ round-trip works without a separate ``@job`` declaration. Pass
76
+ ``register_handler=False`` when the schedule should fire a job whose
77
+ handler lives elsewhere (declared with ``@job``).
78
+ """
79
+ resolved_job_name = job_name or name
80
+
63
81
  def decorator(fn: Handler) -> Handler:
64
- get_registry().register_cron(
82
+ registry = get_registry()
83
+ registry.register_cron(
65
84
  CronDefinition(
66
85
  name=name,
67
86
  cron_expr=cron_expr,
68
- job_name=job_name or name,
87
+ job_name=resolved_job_name,
69
88
  job_version=job_version,
70
89
  payload=payload or {},
71
90
  queue=queue,
72
91
  )
73
92
  )
93
+ if register_handler:
94
+ registry.register_job(
95
+ JobDefinition(
96
+ name=resolved_job_name,
97
+ version=job_version,
98
+ handler=fn,
99
+ max_attempts=max_attempts,
100
+ backoff=backoff if isinstance(backoff, str) else backoff.value,
101
+ backoff_base_s=backoff_base_s,
102
+ backoff_max_s=backoff_max_s,
103
+ queue=queue,
104
+ role=role,
105
+ claims_from=claims_from,
106
+ accepts_payload=accepts_payload,
107
+ )
108
+ )
74
109
  return fn
75
110
 
76
111
  return decorator
@@ -2,6 +2,7 @@ from functools import lru_cache
2
2
  from pathlib import Path
3
3
  from typing import Annotated, Literal
4
4
 
5
+ from dotenv import load_dotenv
5
6
  from pydantic import Field, field_validator
6
7
  from pydantic_settings import BaseSettings, NoDecode, SettingsConfigDict
7
8
 
@@ -242,3 +243,33 @@ class Settings(BaseSettings):
242
243
  @lru_cache
243
244
  def get_settings() -> Settings:
244
245
  return Settings()
246
+
247
+
248
+ def export_env_file() -> None:
249
+ """Export the env file ``Settings`` reads into ``os.environ``.
250
+
251
+ pydantic-settings loads ``env_file`` into the typed ``Settings`` model only;
252
+ it never touches ``os.environ``. Code that resolves a *dynamically-named*
253
+ secret — one whose env var name is not known until runtime (e.g. read from a
254
+ DB column) and so cannot be a typed ``Settings`` field but must be read via
255
+ ``os.environ.get(<name>)`` — therefore sees nothing from ``.env`` when the
256
+ process is launched by supython itself (``supython dev``, ``worker run``, a
257
+ CLI subcommand). In a container it works only by accident: docker-compose's
258
+ ``env_file:`` injects ``.env`` into the real process environment first.
259
+
260
+ Mirror ``uvicorn --env-file``: load the same file with ``override=False`` so
261
+ variables already present in the real environment (set by the orchestrator)
262
+ always win over a checked-in ``.env``. The path is sourced from the same
263
+ ``SettingsConfigDict.env_file`` ``Settings`` uses, never re-hardcoded.
264
+ Idempotent — safe to call from every boot path; a missing file is a no-op.
265
+ """
266
+ env_file = Settings.model_config.get("env_file")
267
+ if not env_file:
268
+ return
269
+ encoding = Settings.model_config.get("env_file_encoding") or "utf-8"
270
+ paths = [env_file] if isinstance(env_file, str | Path) else list(env_file)
271
+ # pydantic gives the *last* listed file precedence; load_dotenv(override=
272
+ # False) gives the *first*-loaded value precedence — so walk in reverse to
273
+ # preserve that ordering while still letting the real environment win.
274
+ for path in reversed(paths):
275
+ load_dotenv(path, override=False, encoding=encoding)
File without changes
File without changes
File without changes